Download server/src/__tests__/lib/error-classify-transport.test.ts from automindy/fokemal: direct link, hf CLI and curl.
- Browser
- Download file 5.83 kB
-
https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/__tests__/lib/error-classify-transport.test.ts
- Command line
-
hf download hf://spaces/automindy/fokemal/server/src/__tests__/lib/error-classify-transport.test.ts
-
curl -L -o error-classify-transport.test.ts https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/__tests__/lib/error-classify-transport.test.ts
5.83 kB
| import { describe, it, expect } from 'vitest'; | |
| import { | |
| isRetryableError, | |
| isTransportError, | |
| newClientAbortError, | |
| newHedgeAbortError, | |
| } from '../../lib/error-classify.js'; | |
| /** Build `new Error(message)` with an undici-style `code`, wrapped in `depth` | |
| * generic wrappers whose own messages match NONE of the top-level substring | |
| * rules — the exact shape that used to slip through as fatal. */ | |
| function wrapped(depth: number, inner: Error): Error { | |
| let cur: Error = inner; | |
| for (let i = 0; i < depth; i++) { | |
| cur = new Error('Provider request failed', { cause: cur }); | |
| } | |
| return cur; | |
| } | |
| function coded(code: string, message = 'read ECONN'): Error { | |
| const err = new Error(message); | |
| (err as Error & { code?: string }).code = code; | |
| return err; | |
| } | |
| describe('transport errors carried in err.cause', () => { | |
| describe('codes anywhere in the chain are retryable', () => { | |
| for (const code of ['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']) { | |
| it(`${code} behind a generic wrapper`, () => { | |
| const err = wrapped(1, coded(code, 'socket failure')); | |
| expect(isTransportError(err)).toBe(true); | |
| expect(isRetryableError(err)).toBe(true); | |
| }); | |
| } | |
| for (const code of ['UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT']) { | |
| it(`undici ${code} behind a generic wrapper`, () => { | |
| const err = wrapped(1, coded(code, 'undici transport failure')); | |
| expect(isRetryableError(err)).toBe(true); | |
| }); | |
| } | |
| it('matches a transport code on the top-level error itself (depth 0)', () => { | |
| expect(isRetryableError(coded('ECONNRESET', 'boom'))).toBe(true); | |
| }); | |
| }); | |
| describe('message hints anywhere in the chain are retryable', () => { | |
| for (const message of [ | |
| 'socket hang up', | |
| 'Premature close', | |
| 'other side closed', | |
| 'Client network socket disconnected before secure TLS connection was established', | |
| ]) { | |
| it(`"${message}" behind a generic wrapper`, () => { | |
| const err = wrapped(1, new Error(message)); | |
| expect(isTransportError(err)).toBe(true); | |
| expect(isRetryableError(err)).toBe(true); | |
| }); | |
| } | |
| it('undici\'s bare "terminated" body error is retryable', () => { | |
| expect(isRetryableError(new Error('terminated'))).toBe(true); | |
| expect(isRetryableError(wrapped(2, new Error('terminated')))).toBe(true); | |
| }); | |
| // "terminated" is matched as a WHOLE message only: a terminated ACCOUNT is | |
| // fatal and must not be retried around the entire chain. | |
| it('does not treat "account has been terminated" as transport', () => { | |
| const err: any = new Error('API error 403: your account has been terminated'); | |
| // Retryable via the 403 rule, but NOT because of the transport walk. | |
| expect(isTransportError(err)).toBe(false); | |
| }); | |
| }); | |
| describe('walk is bounded and cycle-safe', () => { | |
| it('finds a cause nested at depth 5', () => { | |
| expect(isTransportError(wrapped(5, coded('ECONNRESET', 'socket failure')))).toBe(true); | |
| }); | |
| it('stops before an absurdly deep chain', () => { | |
| expect(isTransportError(wrapped(40, coded('ECONNRESET', 'socket failure')))).toBe(false); | |
| }); | |
| it('does not hang on a self-referential cause', () => { | |
| const err: any = new Error('Provider request failed'); | |
| err.cause = err; | |
| expect(isTransportError(err)).toBe(false); | |
| }); | |
| it('does not hang on a two-link cause cycle', () => { | |
| const a: any = new Error('Provider request failed'); | |
| const b: any = new Error('Also failed', { cause: a }); | |
| a.cause = b; | |
| expect(isTransportError(a)).toBe(false); | |
| }); | |
| it('tolerates non-object and null causes', () => { | |
| expect(isTransportError(null)).toBe(false); | |
| expect(isTransportError(undefined)).toBe(false); | |
| expect(isTransportError(new Error('nope', { cause: 'ECONNRESET' }))).toBe(false); | |
| expect(isTransportError(new Error('nope', { cause: undefined }))).toBe(false); | |
| }); | |
| }); | |
| describe('fatal classes stay non-retryable', () => { | |
| it('a 401 bad key is not made retryable by the cause walk', () => { | |
| const err: any = new Error('API error 401: Invalid API key'); | |
| err.status = 401; | |
| expect(isTransportError(err)).toBe(false); | |
| expect(isRetryableError(err)).toBe(false); | |
| }); | |
| // undici labels an aborted socket UND_ERR_ABORTED — the same family a real | |
| // mid-flight socket death carries — so the marked abort errors must win. | |
| it('a client abort wrapped in an undici UND_ERR_ABORTED stays non-retryable', () => { | |
| const abort = newClientAbortError(); | |
| const err = new Error('Provider request failed', { cause: abort }); | |
| (err as any).code = 'UND_ERR_ABORTED'; | |
| expect(isTransportError(err)).toBe(false); | |
| expect(isRetryableError(err)).toBe(false); | |
| }); | |
| it('a bare client abort stays non-retryable', () => { | |
| expect(isTransportError(newClientAbortError())).toBe(false); | |
| expect(isRetryableError(newClientAbortError())).toBe(false); | |
| }); | |
| it('the fallback time-budget hedge abort stays non-retryable', () => { | |
| const hedge = newHedgeAbortError(); | |
| (hedge as any).code = 'UND_ERR_ABORTED'; | |
| expect(isTransportError(hedge)).toBe(false); | |
| expect(isRetryableError(hedge)).toBe(false); | |
| }); | |
| it('an ordinary validation 400 is untouched', () => { | |
| const err: any = new Error('Bad Request: messages must be an array'); | |
| err.status = 400; | |
| expect(isTransportError(err)).toBe(false); | |
| expect(isRetryableError(err)).toBe(false); | |
| }); | |
| it('a generic wrapper with no transport evidence stays non-retryable', () => { | |
| expect(isTransportError(wrapped(3, new Error('model refused the prompt')))).toBe(false); | |
| }); | |
| }); | |
| }); | |