From d09c994b9138638379306c2e312e4460b58e2a22 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 3 Jan 2026 09:46:45 +0900 Subject: [PATCH] fix(session-recovery): detect 'final block cannot be thinking' error pattern (#420) --- src/hooks/session-recovery/index.test.ts | 203 +++++++++++++++++++++++ src/hooks/session-recovery/index.ts | 4 +- 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/hooks/session-recovery/index.test.ts diff --git a/src/hooks/session-recovery/index.test.ts b/src/hooks/session-recovery/index.test.ts new file mode 100644 index 0000000..15b9e17 --- /dev/null +++ b/src/hooks/session-recovery/index.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from "bun:test" +import { detectErrorType } from "./index" + +describe("detectErrorType", () => { + describe("thinking_block_order errors", () => { + it("should detect 'first block' error pattern", () => { + // #given an error about thinking being the first block + const error = { + message: "messages.0: thinking block must not be the first block", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'must start with' error pattern", () => { + // #given an error about message must start with something + const error = { + message: "messages.5: thinking must start with text or tool_use", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'preceeding' error pattern", () => { + // #given an error about preceeding block + const error = { + message: "messages.10: thinking requires preceeding text block", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'expected/found' error pattern", () => { + // #given an error about expected vs found + const error = { + message: "messages.3: thinking block expected text but found tool_use", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'final block cannot be thinking' error pattern", () => { + // #given an error about final block cannot be thinking + const error = { + message: + "messages.125: The final block in an assistant message cannot be thinking.", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'final block' variant error pattern", () => { + // #given an error mentioning final block with thinking + const error = { + message: + "messages.17: thinking in the final block is not allowed in assistant messages", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect 'cannot be thinking' error pattern", () => { + // #given an error using 'cannot be thinking' phrasing + const error = { + message: + "messages.219: The last block in an assistant message cannot be thinking content", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + }) + + describe("tool_result_missing errors", () => { + it("should detect tool_use/tool_result mismatch", () => { + // #given an error about tool_use without tool_result + const error = { + message: "tool_use block requires corresponding tool_result", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return tool_result_missing + expect(result).toBe("tool_result_missing") + }) + }) + + describe("thinking_disabled_violation errors", () => { + it("should detect thinking disabled violation", () => { + // #given an error about thinking being disabled + const error = { + message: + "thinking is disabled for this model and cannot contain thinking blocks", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_disabled_violation + expect(result).toBe("thinking_disabled_violation") + }) + }) + + describe("unrecognized errors", () => { + it("should return null for unrecognized error patterns", () => { + // #given an unrelated error + const error = { + message: "Rate limit exceeded", + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return null + expect(result).toBeNull() + }) + + it("should return null for empty error", () => { + // #given an empty error + const error = {} + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return null + expect(result).toBeNull() + }) + + it("should return null for null error", () => { + // #given a null error + const error = null + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return null + expect(result).toBeNull() + }) + }) + + describe("nested error objects", () => { + it("should detect error in data.error.message path", () => { + // #given an error with nested structure + const error = { + data: { + error: { + message: + "messages.163: The final block in an assistant message cannot be thinking.", + }, + }, + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + + it("should detect error in error.message path", () => { + // #given an error with error.message structure + const error = { + error: { + message: "messages.169: final block cannot be thinking", + }, + } + + // #when detectErrorType is called + const result = detectErrorType(error) + + // #then should return thinking_block_order + expect(result).toBe("thinking_block_order") + }) + }) +}) diff --git a/src/hooks/session-recovery/index.ts b/src/hooks/session-recovery/index.ts index 89fb460..68ddb3f 100644 --- a/src/hooks/session-recovery/index.ts +++ b/src/hooks/session-recovery/index.ts @@ -122,7 +122,7 @@ function extractMessageIndex(error: unknown): number | null { return match ? parseInt(match[1], 10) : null } -function detectErrorType(error: unknown): RecoveryErrorType { +export function detectErrorType(error: unknown): RecoveryErrorType { const message = getErrorMessage(error) if (message.includes("tool_use") && message.includes("tool_result")) { @@ -134,6 +134,8 @@ function detectErrorType(error: unknown): RecoveryErrorType { (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"