map-error.test.js
5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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()
})
})