openpencil/tests/engine/app/ai/failure.test.ts
Danila Poyarkov 61f11e7d24 fix(ai): surface actionable provider failures
- Classify authentication, access, model, rate-limit, network, and credit errors\n- Preserve the original provider error across the no-output stream consequence\n- Add optional toast actions for opening model settings\n- Consume handled chat send rejections to avoid global unhandled errors\n- Localize provider failure guidance and cover the authentication flow
2026-09-04 16:41:03 +03:00

71 lines
2.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { APICallError } from 'ai'
import {
classifyAIChatError,
classifyAIChatFinish,
isInsufficientCreditError
} from '@/app/ai/chat/failure'
describe('AI chat failures', () => {
test('recognizes provider credit and quota failures', () => {
expect(isInsufficientCreditError({ statusCode: 402 })).toBe(true)
expect(isInsufficientCreditError(new Error('Insufficient balance. Please top up.'))).toBe(true)
expect(isInsufficientCreditError(new Error('Quota exceeded for this account'))).toBe(true)
expect(isInsufficientCreditError(new Error('Rate limit reached'))).toBe(false)
})
test('classifies output-limit finishes', () => {
expect(classifyAIChatFinish('length')).toEqual({ reason: 'output-limit' })
expect(classifyAIChatFinish('stop')).toBeNull()
})
test('classifies actionable provider failures', () => {
const apiError = (statusCode: number, message: string) =>
new APICallError({
message,
url: 'https://provider.example/v1/chat',
requestBodyValues: {},
statusCode
})
expect(classifyAIChatError(apiError(401, 'API key expired.'))).toMatchObject({
reason: 'authentication',
statusCode: 401,
retryable: false
})
expect(classifyAIChatError(apiError(403, 'Forbidden'))).toMatchObject({
reason: 'forbidden',
statusCode: 403
})
expect(classifyAIChatError(apiError(404, 'Model not found'))).toMatchObject({
reason: 'model-not-found',
statusCode: 404
})
expect(classifyAIChatError(apiError(429, 'Too many requests'))).toMatchObject({
reason: 'rate-limit',
statusCode: 429,
retryable: true
})
})
test('classifies provider messages without exposing them as presentation copy', () => {
expect(classifyAIChatError(new Error('API key expired.')).reason).toBe('authentication')
expect(classifyAIChatError(new Error('Invalid API key.')).reason).toBe('authentication')
expect(classifyAIChatError(new Error('Failed to fetch provider')).reason).toBe('network')
expect(classifyAIChatError(new Error('Provider unavailable')).reason).toBe('request-failed')
})
test('retains request details for diagnostics', () => {
expect(classifyAIChatError(new Error('Payment required'))).toMatchObject({
reason: 'insufficient-credit',
detail: 'Payment required'
})
expect(classifyAIChatError(new Error('Provider unavailable'))).toMatchObject({
reason: 'request-failed',
detail: 'Provider unavailable'
})
})
})