From dd9eeaa6d6f1a1dc1888feb41a73cde13b1c952c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 14:21:13 +0900 Subject: [PATCH] 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. --- .../detect-error-type.test.ts | 129 ++++++++++++++++++ .../session-recovery/detect-error-type.ts | 70 +++++----- 2 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 src/hooks/session-recovery/detect-error-type.test.ts 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 000000000..d20e7cc9c --- /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 f51c0d0da..3f2f9a1ce 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 }