map-error.test.js 5.08 KB
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()
  })
})