test(session-recovery): add tests for detect-error-type resilience
Add test coverage for detectErrorType and extractMessageIndex with edge cases: circular references, non-standard proxy errors, null input. Wrap both functions in try/catch to prevent crashes from malformed error objects returned by non-standard providers like Antigravity.
This commit is contained in:
129
src/hooks/session-recovery/detect-error-type.test.ts
Normal file
129
src/hooks/session-recovery/detect-error-type.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { detectErrorType, extractMessageIndex } from "./detect-error-type"
|
||||
|
||||
describe("detectErrorType", () => {
|
||||
it("#given a tool_use/tool_result error #when detecting #then returns tool_result_missing", () => {
|
||||
//#given
|
||||
const error = { message: "tool_use block must be followed by tool_result" }
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("tool_result_missing")
|
||||
})
|
||||
|
||||
it("#given a thinking block order error #when detecting #then returns thinking_block_order", () => {
|
||||
//#given
|
||||
const error = { message: "thinking must be the first block in the response" }
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("thinking_block_order")
|
||||
})
|
||||
|
||||
it("#given a thinking disabled violation #when detecting #then returns thinking_disabled_violation", () => {
|
||||
//#given
|
||||
const error = { message: "thinking is disabled and cannot contain thinking blocks" }
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("thinking_disabled_violation")
|
||||
})
|
||||
|
||||
it("#given an unrecognized error #when detecting #then returns null", () => {
|
||||
//#given
|
||||
const error = { message: "some random error" }
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("#given a malformed error with circular references #when detecting #then returns null without crashing", () => {
|
||||
//#given
|
||||
const circular: Record<string, unknown> = {}
|
||||
circular.self = circular
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(circular)
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("#given a proxy error with non-standard structure #when detecting #then returns null without crashing", () => {
|
||||
//#given
|
||||
const proxyError = {
|
||||
data: "not-an-object",
|
||||
error: 42,
|
||||
nested: { deeply: { error: true } },
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(proxyError)
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("#given a null error #when detecting #then returns null", () => {
|
||||
//#given
|
||||
const error = null
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("#given an error with data.error containing message #when detecting #then extracts correctly", () => {
|
||||
//#given
|
||||
const error = {
|
||||
data: {
|
||||
error: {
|
||||
message: "tool_use block requires tool_result",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("tool_result_missing")
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractMessageIndex", () => {
|
||||
it("#given an error referencing messages.5 #when extracting #then returns 5", () => {
|
||||
//#given
|
||||
const error = { message: "Invalid value at messages.5: tool_result is required" }
|
||||
|
||||
//#when
|
||||
const result = extractMessageIndex(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe(5)
|
||||
})
|
||||
|
||||
it("#given a malformed error #when extracting #then returns null without crashing", () => {
|
||||
//#given
|
||||
const circular: Record<string, unknown> = {}
|
||||
circular.self = circular
|
||||
|
||||
//#when
|
||||
const result = extractMessageIndex(circular)
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -34,40 +34,48 @@ function getErrorMessage(error: unknown): string {
|
||||
}
|
||||
|
||||
export function extractMessageIndex(error: unknown): number | null {
|
||||
const message = getErrorMessage(error)
|
||||
const match = message.match(/messages\.(\d+)/)
|
||||
return match ? parseInt(match[1], 10) : null
|
||||
try {
|
||||
const message = getErrorMessage(error)
|
||||
const match = message.match(/messages\.(\d+)/)
|
||||
return match ? parseInt(match[1], 10) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function detectErrorType(error: unknown): RecoveryErrorType {
|
||||
const message = getErrorMessage(error)
|
||||
try {
|
||||
const message = getErrorMessage(error)
|
||||
|
||||
if (
|
||||
message.includes("assistant message prefill") ||
|
||||
message.includes("conversation must end with a user message")
|
||||
) {
|
||||
return "assistant_prefill_unsupported"
|
||||
if (
|
||||
message.includes("assistant message prefill") ||
|
||||
message.includes("conversation must end with a user message")
|
||||
) {
|
||||
return "assistant_prefill_unsupported"
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes("thinking") &&
|
||||
(message.includes("first block") ||
|
||||
message.includes("must start with") ||
|
||||
message.includes("preceeding") ||
|
||||
message.includes("final block") ||
|
||||
message.includes("cannot be thinking") ||
|
||||
(message.includes("expected") && message.includes("found")))
|
||||
) {
|
||||
return "thinking_block_order"
|
||||
}
|
||||
|
||||
if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
|
||||
return "thinking_disabled_violation"
|
||||
}
|
||||
|
||||
if (message.includes("tool_use") && message.includes("tool_result")) {
|
||||
return "tool_result_missing"
|
||||
}
|
||||
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes("thinking") &&
|
||||
(message.includes("first block") ||
|
||||
message.includes("must start with") ||
|
||||
message.includes("preceeding") ||
|
||||
message.includes("final block") ||
|
||||
message.includes("cannot be thinking") ||
|
||||
(message.includes("expected") && message.includes("found")))
|
||||
) {
|
||||
return "thinking_block_order"
|
||||
}
|
||||
|
||||
if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
|
||||
return "thinking_disabled_violation"
|
||||
}
|
||||
|
||||
if (message.includes("tool_use") && message.includes("tool_result")) {
|
||||
return "tool_result_missing"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user