/** * Unit tests for context-overflow error classification. * * The graph's emergency-prune retry relies on these helpers to decide * whether a provider failure warrants a truncated retry. False positives * cost one extra retry; false negatives surface an opaque failure to the * user. Both are cheaper than the previous inline substring matching, * which missed phrases like "request_too_large" (Anthropic 429-adjacent) * and could falsely trigger on rate-limit errors mentioning "too many". */ import { extractErrorMessage, isContextOverflowError, isLikelyContextOverflowError, } from '../errors'; describe('extractErrorMessage', () => { it('returns empty string for null/undefined', () => { expect(extractErrorMessage(null)).toBe(''); expect(extractErrorMessage(undefined)).toBe(''); }); it('returns the string directly', () => { expect(extractErrorMessage('something broke')).toBe('something broke'); }); it('reads Error.message', () => { expect(extractErrorMessage(new Error('boom'))).toBe('boom'); }); it('reads plain-object message/error fields', () => { expect(extractErrorMessage({ message: 'm' })).toBe('m'); expect(extractErrorMessage({ error: 'e' })).toBe('e'); expect(extractErrorMessage({ error: { message: 'nested' } })).toBe( 'nested' ); }); it('falls back to JSON stringify for unknown shapes', () => { expect(extractErrorMessage({ status: 500 })).toBe('{"status":500}'); }); }); describe('isContextOverflowError (strict)', () => { it('returns false for empty input', () => { expect(isContextOverflowError()).toBe(false); expect(isContextOverflowError('')).toBe(false); }); it('matches Anthropic prompt-too-long', () => { expect( isContextOverflowError('prompt is too long: 250000 tokens > 200000') ).toBe(true); }); it('matches OpenAI context_length_exceeded', () => { expect( isContextOverflowError( "This model's maximum context length is 128000 tokens. context_length_exceeded" ) ).toBe(true); }); it('matches Bedrock input-too-long', () => { expect( isContextOverflowError( 'ValidationException: Input is too long for requested model.' ) ).toBe(true); }); it('matches request_too_large', () => { expect(isContextOverflowError('Error code 413: request_too_large')).toBe( true ); }); it('is case-insensitive', () => { expect(isContextOverflowError('PROMPT IS TOO LONG')).toBe(true); }); it('rejects rate-limit errors even if they mention "too many"', () => { expect( isContextOverflowError('429 rate_limit_exceeded: too many requests') ).toBe(false); }); it('rejects auth / billing errors', () => { expect(isContextOverflowError('insufficient quota on billing plan')).toBe( false ); expect(isContextOverflowError('forbidden: missing permission')).toBe(false); }); it('does not match loose phrases like bare "too long"', () => { // Strict check should NOT fire on just "too long" — that's for the // loose variant. Keeps the retry budget tight. expect(isContextOverflowError('the response was too long')).toBe(false); }); }); describe('isLikelyContextOverflowError (loose)', () => { it('matches everything the strict check matches', () => { expect(isLikelyContextOverflowError('prompt is too long')).toBe(true); expect(isLikelyContextOverflowError('context_length_exceeded')).toBe(true); }); it('matches heuristic regex: bare "too long"', () => { expect(isLikelyContextOverflowError('response was too long')).toBe(true); }); it('matches heuristic regex: 413 status code', () => { expect(isLikelyContextOverflowError('HTTP 413 payload')).toBe(true); }); it('matches "context ... exceed" in either order', () => { expect(isLikelyContextOverflowError('your context exceeds limits')).toBe( true ); expect(isLikelyContextOverflowError('exceeds context window')).toBe(true); }); it('still rejects rate-limit / auth even on loose match', () => { expect( isLikelyContextOverflowError('rate limit: too many requests queued') ).toBe(false); expect( isLikelyContextOverflowError('authorization exceeds allowed quota') ).toBe(false); }); it('returns false for unrelated errors', () => { expect(isLikelyContextOverflowError('ECONNREFUSED')).toBe(false); expect(isLikelyContextOverflowError('unexpected token in JSON')).toBe( false ); }); });