* build: update dependencies Update the AI SDK providers, Vue, Reka UI, Valibot, Zod, es-toolkit, CodeMirror, Storybook, Playwright, Hono and other dependencies to their current releases, consistently across workspaces. The Tauri plugin packages must match their Rust crates, and the new plugin crates require Tauri 2.12, so Cargo.lock, @tauri-apps/api and the Tauri CLI move to 2.12 as well. * build(harness): update the AI SDK harness packages @ai-sdk/harness 1.0.74 pinned ai 7.0.67, so the workspace carried a second copy of ai next to the root one; 1.0.138 depends on the same ai release. The Pi adapter no longer takes a model: HarnessAgent does. The settings were spread from untyped records, so the compiler could not reject the stale key and the chosen model would have been dropped; they are plain literals now. PiAuthOptions is now PiAuthenticationMode, and auth accepts an environment record. Pass the gateway key that way instead of writing it into the process-wide environment while a session is created. Derive the thinking level from the adapter's settings, which adds 'max'. * build: hold vue-tsc at 3.3.11 vue-tsc 3.3.12 no longer sees a v-slot binding inside a component that also has an event listener, so check:vue reports "Cannot find name 'control'" in MCPConnectionEditor and ProfileEditor. 3.3.11 checks them cleanly. * fix(ai): keep retryability for provider errors reported mid-stream From ai 7.0.80 a provider error after the response stream starts is a StreamProviderError rather than an APICallError, so classifyAIChatError lost its isRetryable. * feat(desktop): accept updates only when signed for their version Tauri CLI 2.12 records the app version in each updater signature, and updater 2.13 checks it against the version latest.json announces. With requireSignedVersion it also rejects signatures that carry no version, so a tampered manifest cannot pair a newer version number with an older, still validly signed bundle. Release assembly now fails when a signature does not name the version being released, instead of shipping one that installed apps would reject. * docs: note the dependency update's security fixes in the changelog * feat(ai): recommend the latest models The provider packages now know Claude Sonnet 5.5 and Opus 5.5 and the GPT-6 series. Make Sonnet 5.5 and GPT-6.1 Sol the defaults, list Opus 5.5, Fable 5.1, GPT-6 Astra and GPT-6 Luna, and replace the two free OpenRouter models that OpenRouter no longer serves. * build: align the fig package's valibot with the workspace
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { APICallError, StreamProviderError } 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 errors reported after the stream started', () => {
|
|
const streamError = new StreamProviderError({
|
|
message: 'Too many requests',
|
|
statusCode: 429,
|
|
isRetryable: true
|
|
})
|
|
expect(classifyAIChatError(streamError)).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'
|
|
})
|
|
})
|
|
})
|