Skip to content

Commit 3cd4b19

Browse files
committed
test(recovery): add recovery error detection tests
- Add 26 tests for detectErrorType across all error scenarios - Add tests for isRecoverableError helper - Update plugin config tests for new sessionRecovery/autoResume defaults
1 parent a24fc9a commit 3cd4b19

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

test/plugin-config.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ describe('Plugin Configuration', () => {
4646
retryAllAccountsMaxRetries: Infinity,
4747
tokenRefreshSkewMs: 60_000,
4848
rateLimitToastDebounceMs: 60_000,
49+
sessionRecovery: true,
50+
autoResume: true,
4951
});
5052
expect(mockExistsSync).toHaveBeenCalledWith(
5153
path.join(os.homedir(), '.opencode', 'openai-codex-auth-config.json')
@@ -65,6 +67,8 @@ describe('Plugin Configuration', () => {
6567
retryAllAccountsMaxRetries: Infinity,
6668
tokenRefreshSkewMs: 60_000,
6769
rateLimitToastDebounceMs: 60_000,
70+
sessionRecovery: true,
71+
autoResume: true,
6872
});
6973
});
7074

@@ -81,6 +85,8 @@ describe('Plugin Configuration', () => {
8185
retryAllAccountsMaxRetries: Infinity,
8286
tokenRefreshSkewMs: 60_000,
8387
rateLimitToastDebounceMs: 60_000,
88+
sessionRecovery: true,
89+
autoResume: true,
8490
});
8591
});
8692

@@ -98,6 +104,8 @@ describe('Plugin Configuration', () => {
98104
retryAllAccountsMaxRetries: Infinity,
99105
tokenRefreshSkewMs: 60_000,
100106
rateLimitToastDebounceMs: 60_000,
107+
sessionRecovery: true,
108+
autoResume: true,
101109
});
102110
expect(consoleSpy).toHaveBeenCalled();
103111
consoleSpy.mockRestore();
@@ -119,6 +127,8 @@ describe('Plugin Configuration', () => {
119127
retryAllAccountsMaxRetries: Infinity,
120128
tokenRefreshSkewMs: 60_000,
121129
rateLimitToastDebounceMs: 60_000,
130+
sessionRecovery: true,
131+
autoResume: true,
122132
});
123133
expect(consoleSpy).toHaveBeenCalled();
124134
consoleSpy.mockRestore();

test/recovery.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, it, expect } from "vitest";
2+
import { detectErrorType, isRecoverableError } from "../lib/recovery";
3+
4+
describe("detectErrorType", () => {
5+
describe("tool_result_missing detection", () => {
6+
it("detects tool_use without tool_result error", () => {
7+
const error = {
8+
type: "invalid_request_error",
9+
message: "messages.105: `tool_use` ids were found without `tool_result` blocks immediately after: tool-call-59"
10+
};
11+
expect(detectErrorType(error)).toBe("tool_result_missing");
12+
});
13+
14+
it("detects tool_use/tool_result mismatch error", () => {
15+
const error = "Each `tool_use` block must have a corresponding `tool_result` block in the next message.";
16+
expect(detectErrorType(error)).toBe("tool_result_missing");
17+
});
18+
19+
it("detects error from string message", () => {
20+
const error = "tool_use without matching tool_result";
21+
expect(detectErrorType(error)).toBe("tool_result_missing");
22+
});
23+
});
24+
25+
describe("thinking_block_order detection", () => {
26+
it("detects thinking first block error", () => {
27+
const error = "thinking must be the first block in the message";
28+
expect(detectErrorType(error)).toBe("thinking_block_order");
29+
});
30+
31+
it("detects thinking must start with error", () => {
32+
const error = "Response must start with thinking block";
33+
expect(detectErrorType(error)).toBe("thinking_block_order");
34+
});
35+
36+
it("detects thinking preceeding error", () => {
37+
const error = "thinking block preceeding tool use is required";
38+
expect(detectErrorType(error)).toBe("thinking_block_order");
39+
});
40+
41+
it("detects thinking expected/found error", () => {
42+
const error = "Expected thinking block but found text";
43+
expect(detectErrorType(error)).toBe("thinking_block_order");
44+
});
45+
});
46+
47+
describe("thinking_disabled_violation detection", () => {
48+
it("detects thinking disabled error", () => {
49+
const error = "thinking is disabled for this model and cannot contain thinking blocks";
50+
expect(detectErrorType(error)).toBe("thinking_disabled_violation");
51+
});
52+
});
53+
54+
describe("non-recoverable errors", () => {
55+
it("returns null for prompt too long error", () => {
56+
const error = { message: "Prompt is too long" };
57+
expect(detectErrorType(error)).toBeNull();
58+
});
59+
60+
it("returns null for context length exceeded error", () => {
61+
const error = "context length exceeded";
62+
expect(detectErrorType(error)).toBeNull();
63+
});
64+
65+
it("returns null for generic errors", () => {
66+
expect(detectErrorType("Something went wrong")).toBeNull();
67+
expect(detectErrorType({ message: "Unknown error" })).toBeNull();
68+
expect(detectErrorType(null)).toBeNull();
69+
expect(detectErrorType(undefined)).toBeNull();
70+
});
71+
72+
it("returns null for rate limit errors", () => {
73+
const error = { message: "Rate limit exceeded. Retry after 5s" };
74+
expect(detectErrorType(error)).toBeNull();
75+
});
76+
});
77+
});
78+
79+
describe("isRecoverableError", () => {
80+
it("returns true for tool_result_missing", () => {
81+
const error = "tool_use without tool_result";
82+
expect(isRecoverableError(error)).toBe(true);
83+
});
84+
85+
it("returns true for thinking_block_order", () => {
86+
const error = "thinking must be the first block";
87+
expect(isRecoverableError(error)).toBe(true);
88+
});
89+
90+
it("returns true for thinking_disabled_violation", () => {
91+
const error = "thinking is disabled and cannot contain thinking";
92+
expect(isRecoverableError(error)).toBe(true);
93+
});
94+
95+
it("returns false for non-recoverable errors", () => {
96+
expect(isRecoverableError("Prompt is too long")).toBe(false);
97+
expect(isRecoverableError("context length exceeded")).toBe(false);
98+
expect(isRecoverableError("Generic error")).toBe(false);
99+
expect(isRecoverableError(null)).toBe(false);
100+
});
101+
});
102+
103+
describe("context error message patterns", () => {
104+
describe("prompt too long patterns", () => {
105+
const promptTooLongPatterns = [
106+
"Prompt is too long",
107+
"prompt is too long for this model",
108+
"The prompt is too long",
109+
];
110+
111+
it.each(promptTooLongPatterns)("'%s' is not a recoverable error", (msg) => {
112+
expect(isRecoverableError(msg)).toBe(false);
113+
expect(detectErrorType(msg)).toBeNull();
114+
});
115+
});
116+
117+
describe("context length exceeded patterns", () => {
118+
const contextLengthPatterns = [
119+
"context length exceeded",
120+
"context_length_exceeded",
121+
"maximum context length",
122+
"exceeds the maximum context window",
123+
];
124+
125+
it.each(contextLengthPatterns)("'%s' is not a recoverable error", (msg) => {
126+
expect(isRecoverableError(msg)).toBe(false);
127+
expect(detectErrorType(msg)).toBeNull();
128+
});
129+
});
130+
131+
describe("tool pairing error patterns", () => {
132+
const toolPairingPatterns = [
133+
"tool_use ids were found without tool_result blocks immediately after",
134+
"Each tool_use block must have a corresponding tool_result",
135+
"tool_use without matching tool_result",
136+
];
137+
138+
it.each(toolPairingPatterns)("'%s' is detected as tool_result_missing", (msg) => {
139+
expect(detectErrorType(msg)).toBe("tool_result_missing");
140+
expect(isRecoverableError(msg)).toBe(true);
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)