Spaces:
Running
Running
| import { AgentApiError, createAgentErrorBody, normalizeAgentError } from './api-error-response'; | |
| import { RequestValidationError } from './image-request-utils'; | |
| import assert from 'node:assert/strict'; | |
| import { describe, it } from 'node:test'; | |
| describe('normalizeAgentError', () => { | |
| it('maps field validation errors into structured Agent errors', () => { | |
| const error = normalizeAgentError(new RequestValidationError(JSON.stringify({ fields: { n: 'bad' } }), 422)); | |
| assert.equal(error.code, 'validation_error'); | |
| assert.equal(error.status, 422); | |
| assert.equal(error.retryable, false); | |
| assert.deepEqual(error.details, { fields: { n: 'bad' } }); | |
| }); | |
| it('marks rate limits as retryable with upstream status', () => { | |
| const error = normalizeAgentError({ status: 429, message: 'rate limit' }); | |
| assert.equal(error.code, 'upstream_rate_limited'); | |
| assert.equal(error.retryable, true); | |
| assert.equal(error.upstreamStatus, 429); | |
| }); | |
| it('maps upstream image input errors to agent-correctable validation errors', () => { | |
| const error = normalizeAgentError({ | |
| status: 400, | |
| message: 'The image data you provided does not represent a valid image.' | |
| }); | |
| assert.equal(error.code, 'validation_error'); | |
| assert.equal(error.status, 422); | |
| assert.equal(error.retryable, false); | |
| assert.equal(error.upstreamStatus, 400); | |
| assert.deepEqual(error.details, { | |
| fields: { | |
| image_0: 'The image data you provided does not represent a valid image.' | |
| } | |
| }); | |
| }); | |
| it('maps upstream connection errors to retryable unavailable errors', () => { | |
| const error = normalizeAgentError( | |
| Object.assign(new Error('Connection error.'), { | |
| name: 'APIConnectionError' | |
| }) | |
| ); | |
| assert.equal(error.code, 'upstream_unavailable'); | |
| assert.equal(error.status, 502); | |
| assert.equal(error.retryable, true); | |
| assert.equal(error.retryAfterSeconds, 15); | |
| assert.equal(error.upstreamStatus, undefined); | |
| assert.equal(error.diagnostics?.transport_error, true); | |
| assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel'); | |
| }); | |
| it('adds sanitized upstream diagnostics without inventing an HTTP status', () => { | |
| const error = normalizeAgentError( | |
| Object.assign(new Error('Connection error.'), { | |
| name: 'APIConnectionError', | |
| headers: { | |
| 'cf-ray': 'abc-SJC', | |
| authorization: 'Bearer secret' | |
| } | |
| }), | |
| { | |
| elapsed_ms: 1234, | |
| selected_channel_id: 'channel-a', | |
| upstream_host: 'api.example.test' | |
| } | |
| ); | |
| const body = createAgentErrorBody(error, 'request-2'); | |
| assert.equal(body.error.upstream_status, undefined); | |
| assert.equal(body.error.diagnostics?.elapsed_ms, 1234); | |
| assert.equal(body.error.diagnostics?.selected_channel_id, 'channel-a'); | |
| assert.equal(body.error.diagnostics?.upstream_host, 'api.example.test'); | |
| assert.equal(body.error.diagnostics?.transport_error, true); | |
| assert.deepEqual(body.error.diagnostics?.response_headers, { 'cf-ray': 'abc-SJC' }); | |
| assert.equal(JSON.stringify(body).includes('secret'), false); | |
| }); | |
| it('filters caller-provided diagnostic response headers through the allowlist', () => { | |
| const error = normalizeAgentError(new Error('diagnostics'), { | |
| response_headers: { | |
| 'cf-ray': 'abc-SJC', | |
| authorization: 'Bearer secret', | |
| 'x-api-key': 'secret' | |
| } | |
| }); | |
| const body = createAgentErrorBody(error, 'request-3'); | |
| assert.deepEqual(body.error.diagnostics?.response_headers, { 'cf-ray': 'abc-SJC' }); | |
| assert.equal(JSON.stringify(body).includes('secret'), false); | |
| }); | |
| it('copies upstream status and retry timing into diagnostics', () => { | |
| const error = normalizeAgentError({ | |
| status: 524, | |
| message: 'timeout', | |
| headers: { | |
| 'retry-after': '7', | |
| server: 'cloudflare' | |
| } | |
| }); | |
| assert.equal(error.code, 'upstream_unavailable'); | |
| assert.equal(error.upstreamStatus, 524); | |
| assert.equal(error.retryAfterSeconds, 7); | |
| assert.equal(error.diagnostics?.upstream_status, 524); | |
| assert.equal(error.diagnostics?.retry_after_seconds, 7); | |
| assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel'); | |
| assert.deepEqual(error.diagnostics?.response_headers, { | |
| 'retry-after': '7', | |
| server: 'cloudflare' | |
| }); | |
| }); | |
| it('falls back when upstream retry-after headers are unsafe', () => { | |
| const error = normalizeAgentError({ | |
| status: 429, | |
| message: 'rate limit', | |
| headers: { | |
| 'retry-after': '999999999999999999999' | |
| } | |
| }); | |
| assert.equal(error.code, 'upstream_rate_limited'); | |
| assert.equal(error.retryAfterSeconds, 30); | |
| assert.equal(error.diagnostics?.retry_after_seconds, 30); | |
| }); | |
| }); | |
| describe('createAgentErrorBody', () => { | |
| it('keeps stable error shape for agents', () => { | |
| const body = createAgentErrorBody( | |
| new AgentApiError({ | |
| code: 'idempotency_conflict', | |
| message: 'conflict', | |
| status: 409 | |
| }), | |
| 'request-1' | |
| ); | |
| assert.deepEqual(body, { | |
| error: { | |
| code: 'idempotency_conflict', | |
| message: 'conflict', | |
| retryable: false, | |
| request_id: 'request-1' | |
| } | |
| }); | |
| }); | |
| }); | |