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:
YeonGyu-Kim
2026-02-14 14:21:13 +09:00
parent daf011c616
commit dd9eeaa6d6
2 changed files with 168 additions and 31 deletions

View 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()
})
})

View File

@@ -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
}