diff --git a/src/hooks/session-recovery/detect-error-type.test.ts b/src/hooks/session-recovery/detect-error-type.test.ts
new file mode 100644
index 00000000..d20e7cc9
--- /dev/null
+++ b/src/hooks/session-recovery/detect-error-type.test.ts
@@ -0,0 +1,129 @@
+///
+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 = {}
+ 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 = {}
+ circular.self = circular
+
+ //#when
+ const result = extractMessageIndex(circular)
+
+ //#then
+ expect(result).toBeNull()
+ })
+})
diff --git a/src/hooks/session-recovery/detect-error-type.ts b/src/hooks/session-recovery/detect-error-type.ts
index f51c0d0d..3f2f9a1c 100644
--- a/src/hooks/session-recovery/detect-error-type.ts
+++ b/src/hooks/session-recovery/detect-error-type.ts
@@ -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
}