import { describe, it, expect } from 'vitest' import { mapHttpStatus, isAbortError, mapThrownError, mapKeyRouterError, mapResponsesFailure, mapChatFinishReason, } from '../src/map-error.js' describe('mapHttpStatus', () => { it('401/403 = auth, not retryable', () => { expect(mapHttpStatus(401)).toEqual({ reason: 'auth', retryable: false }) expect(mapHttpStatus(403)).toEqual({ reason: 'auth', retryable: false }) }) it('429 = rateLimit, retryable', () => { expect(mapHttpStatus(429)).toEqual({ reason: 'rateLimit', retryable: true }) }) it('408 / 5xx = transient, retryable', () => { expect(mapHttpStatus(408)).toEqual({ reason: 'transient', retryable: true }) expect(mapHttpStatus(500)).toEqual({ reason: 'transient', retryable: true }) expect(mapHttpStatus(503)).toEqual({ reason: 'transient', retryable: true }) }) it('other 4xx = badRequest, not retryable', () => { expect(mapHttpStatus(400)).toEqual({ reason: 'badRequest', retryable: false }) expect(mapHttpStatus(404)).toEqual({ reason: 'badRequest', retryable: false }) }) it('2xx/3xx = unknown (not an error path)', () => { expect(mapHttpStatus(200)).toEqual({ reason: 'unknown', retryable: false }) }) }) describe('isAbortError', () => { it('detects AbortError by name and code', () => { expect(isAbortError({ name: 'AbortError' })).toBe(true) expect(isAbortError({ code: 'ABORT_ERR' })).toBe(true) expect(isAbortError(new Error('nope'))).toBe(false) expect(isAbortError(null)).toBe(false) }) }) describe('mapThrownError', () => { it('abort = aborted, not retryable', () => { expect(mapThrownError({ name: 'AbortError' })).toEqual({ reason: 'aborted', retryable: false }) }) it('network errors = transient, retryable', () => { expect(mapThrownError({ name: 'TypeError' })).toEqual({ reason: 'transient', retryable: true }) expect(mapThrownError({ code: 'ECONNRESET' })).toEqual({ reason: 'transient', retryable: true }) expect(mapThrownError({ code: 'ETIMEDOUT' })).toEqual({ reason: 'transient', retryable: true }) }) it('unknown otherwise', () => { expect(mapThrownError(new Error('weird'))).toEqual({ reason: 'unknown', retryable: false }) }) it('KeyRouterError (by name) maps to transient, not unknown (F6)', () => { const kr = Object.assign(new Error('all keys cooling'), { name: 'KeyRouterError', reason: 'noKeyAvailable', retryable: true }) expect(mapThrownError(kr)).toEqual({ reason: 'transient', retryable: true }) }) it('non-retryable KeyRouterError maps to transient not-retryable (config hard error)', () => { const kr = Object.assign(new Error('config'), { name: 'KeyRouterError', reason: 'configMissing', retryable: false }) expect(mapThrownError(kr)).toEqual({ reason: 'transient', retryable: false }) }) }) describe('mapResponsesFailure — Responses response.failed error.code classification', () => { it('rate_limit_exceeded = rateLimit, retryable', () => { expect(mapResponsesFailure('rate_limit_exceeded')).toEqual({ reason: 'rateLimit', retryable: true }) }) it('server_error = transient, retryable', () => { expect(mapResponsesFailure('server_error')).toEqual({ reason: 'transient', retryable: true }) }) it('other/unknown codes = badRequest, not retryable', () => { expect(mapResponsesFailure('invalid_prompt')).toEqual({ reason: 'badRequest', retryable: false }) expect(mapResponsesFailure(undefined)).toEqual({ reason: 'badRequest', retryable: false }) }) }) describe('mapChatFinishReason — Chat Completions finish_reason classification', () => { it('complete terminal reasons are not errors', () => { expect(mapChatFinishReason(null)).toBeNull() expect(mapChatFinishReason('stop')).toBeNull() expect(mapChatFinishReason('tool_calls')).toBeNull() expect(mapChatFinishReason('function_call')).toBeNull() }) it('length is transient so router fallback can try the next model', () => { expect(mapChatFinishReason('length')).toEqual({ reason: 'transient', retryable: true }) }) it('content_filter finish is fallbackable provider output truncation', () => { expect(mapChatFinishReason('content_filter')).toEqual({ reason: 'contentFilter', retryable: true }) }) it('unknown terminal reasons are not retried blindly', () => { expect(mapChatFinishReason('weird')).toEqual({ reason: 'unknown', retryable: false }) }) }) describe('mapKeyRouterError (F6) — no keyrouter import, name/duck-type detection', () => { it('detects by name === KeyRouterError', () => { expect(mapKeyRouterError({ name: 'KeyRouterError', retryable: true })).toEqual({ reason: 'transient', retryable: true }) }) it('duck-types retryAfterMs → rateLimit', () => { expect(mapKeyRouterError({ retryAfterMs: 1000 })).toEqual({ reason: 'rateLimit', retryable: true }) }) it('duck-types kind field → transient', () => { expect(mapKeyRouterError({ kind: 'slotTimeout', retryable: true })).toEqual({ reason: 'transient', retryable: true }) }) it('returns null for unrelated errors', () => { expect(mapKeyRouterError(new Error('plain'))).toBeNull() expect(mapKeyRouterError(null)).toBeNull() }) })