import { describe, expect, it } from "vitest"; import { extractClaudeRetryNotBefore, isClaudeTransientUpstreamError, isClaudePoisonedPreviousMessageIdError, isClaudeUnknownSessionError, } from "./parse.js"; describe("isClaudeTransientUpstreamError", () => { it("classifies the 'out of extra usage' subscription window failure as transient", () => { expect( isClaudeTransientUpstreamError({ errorMessage: "You're out of extra usage · resets 4pm (America/Chicago)", }), ).toBe(true); expect( isClaudeTransientUpstreamError({ parsed: { is_error: true, result: "You're out of extra usage. Resets at 4pm (America/Chicago).", }, }), ).toBe(true); }); it("classifies Anthropic API rate_limit_error and overloaded_error as transient", () => { expect( isClaudeTransientUpstreamError({ parsed: { is_error: true, errors: [{ type: "rate_limit_error", message: "Rate limit reached for requests." }], }, }), ).toBe(true); expect( isClaudeTransientUpstreamError({ parsed: { is_error: true, errors: [{ type: "overloaded_error", message: "Overloaded" }], }, }), ).toBe(true); expect( isClaudeTransientUpstreamError({ stderr: "HTTP 429: Too Many Requests", }), ).toBe(true); expect( isClaudeTransientUpstreamError({ stderr: "Bedrock ThrottlingException: slow down", }), ).toBe(true); }); it("classifies the subscription 5-hour / weekly limit wording", () => { expect( isClaudeTransientUpstreamError({ errorMessage: "Claude usage limit reached — weekly limit reached. Try again in 2 days.", }), ).toBe(true); expect( isClaudeTransientUpstreamError({ errorMessage: "5-hour limit reached.", }), ).toBe(true); }); it("does not classify login/auth failures as transient", () => { expect( isClaudeTransientUpstreamError({ stderr: "Please log in. Run `claude login` first.", }), ).toBe(false); }); it("does not classify max-turns or unknown-session as transient", () => { expect( isClaudeTransientUpstreamError({ parsed: { subtype: "error_max_turns", result: "Maximum turns reached." }, }), ).toBe(false); expect( isClaudeTransientUpstreamError({ parsed: { result: "No conversation found with session id abc-123", errors: [{ message: "No conversation found with session id abc-123" }], }, }), ).toBe(false); }); it("does not classify deterministic validation errors as transient", () => { expect( isClaudeTransientUpstreamError({ errorMessage: "Invalid request_error: Unknown parameter 'foo'.", }), ).toBe(false); }); it("does not classify poisoned previous_message_id errors as transient", () => { expect( isClaudeTransientUpstreamError({ parsed: { subtype: "success", is_error: true, result: "API Error: 400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`)", }, }), ).toBe(false); }); }); describe("isClaudePoisonedPreviousMessageIdError", () => { it("detects the previous_message_id 400 error in the result field", () => { expect( isClaudePoisonedPreviousMessageIdError({ subtype: "success", is_error: true, result: "API Error: 400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`)", }), ).toBe(true); }); it("detects the error in the errors array", () => { expect( isClaudePoisonedPreviousMessageIdError({ is_error: true, result: "", errors: [{ message: "400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`)" }], }), ).toBe(true); }); it("returns false for unrelated errors", () => { expect( isClaudePoisonedPreviousMessageIdError({ is_error: true, result: "No conversation found with session id abc-123", }), ).toBe(false); }); it("returns false for empty parsed result", () => { expect(isClaudePoisonedPreviousMessageIdError({})).toBe(false); }); }); describe("isClaudeUnknownSessionError", () => { it("detects the legacy 'no conversation found' message", () => { expect( isClaudeUnknownSessionError({ result: "Error: No conversation found with session id 1234", }), ).toBe(true); }); it("detects 'session ... not found' style errors", () => { expect( isClaudeUnknownSessionError({ errors: [{ message: "Session abc123 not found" }], }), ).toBe(true); }); it("detects '--resume requires a valid session' validation error from non-UUID input", () => { expect( isClaudeUnknownSessionError({ errors: [ { message: 'Error: --resume requires a valid session ID or session title when used with --print. Usage: claude -p --resume . Provided value "ses_268c2d0a5ffemYbEaeG7c86Uvo" is not a UUID and does not match any session title.', }, ], }), ).toBe(true); }); it("returns false for unrelated error text", () => { expect( isClaudeUnknownSessionError({ result: "Some other failure", errors: [{ message: "Network timeout" }], }), ).toBe(false); }); }); describe("extractClaudeRetryNotBefore", () => { it("parses the 'resets 4pm' hint in its explicit timezone", () => { const now = new Date("2026-04-22T15:15:00.000Z"); const extracted = extractClaudeRetryNotBefore( { errorMessage: "You're out of extra usage · resets 4pm (America/Chicago)" }, now, ); expect(extracted?.toISOString()).toBe("2026-04-22T21:00:00.000Z"); }); it("rolls forward past midnight when the reset time has already passed today", () => { const now = new Date("2026-04-22T23:30:00.000Z"); const extracted = extractClaudeRetryNotBefore( { errorMessage: "Usage limit reached. Resets at 3:15 AM (UTC)." }, now, ); expect(extracted?.toISOString()).toBe("2026-04-23T03:15:00.000Z"); }); it("returns null when no reset hint is present", () => { expect( extractClaudeRetryNotBefore({ errorMessage: "Overloaded. Try again later." }, new Date()), ).toBeNull(); }); });