openpencil/tests/engine/app/ai/catalog.test.ts
Danila Poyarkov daef57d52d
build: update dependencies (#873)
* 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
2026-10-04 12:48:24 +00:00

147 lines
4.9 KiB
TypeScript

import { afterEach, describe, expect, test } from 'bun:test'
import {
listCatalogModels,
resetModelsDevCatalogForTests,
resolveModelsDevModel
} from '@/app/ai/models/catalog'
function catalogResponse(body: unknown): typeof fetch {
return (async () => new Response(JSON.stringify(body), { status: 200 })) as typeof fetch
}
afterEach(() => resetModelsDevCatalogForTests())
describe('models.dev catalog', () => {
test('resolves provider model capabilities and output limits', async () => {
const model = await resolveModelsDevModel(
'anthropic',
'claude-sonnet-4-6-20260301',
catalogResponse({
anthropic: {
models: {
'claude-sonnet-4-6': {
name: 'Claude Sonnet 4.6',
tool_call: true,
attachment: true,
limit: { output: 64_000 }
}
}
}
})
)
expect(model).toEqual({
id: 'claude-sonnet-4-6-20260301',
name: 'Claude Sonnet 4.6',
capabilities: ['tools', 'vision'],
recommendedMaxOutputTokens: 64_000,
status: 'active'
})
})
test('lists compatible models with curated recommendations first and latest models next', async () => {
const models = await listCatalogModels(
'openai',
catalogResponse({
openai: {
models: {
'gpt-6.1-sol': {
name: 'GPT-6.1 Sol from catalog',
tool_call: true,
attachment: true,
release_date: '2026-07-09',
modalities: { output: ['text'] },
limit: { output: 128_000 }
},
'gpt-6-preview': {
name: 'GPT-6 Preview',
tool_call: true,
release_date: '2026-09-01',
modalities: { output: ['text'] }
},
'gpt-image': {
name: 'GPT Image',
tool_call: false,
release_date: '2026-09-02',
modalities: { output: ['image'] }
},
missingOutput: { tool_call: true },
malformedOutput: { tool_call: true, modalities: { output: 'text' } },
imageOutput: { tool_call: true, modalities: { output: ['image'] } },
legacy: {
name: 'Legacy',
tool_call: true,
status: 'deprecated',
release_date: '2025-01-01'
}
}
}
})
)
expect(models[0]).toMatchObject({
id: 'gpt-6.1-sol',
name: 'GPT-6.1 Sol',
tag: 'Best',
capabilities: ['tools', 'vision'],
releaseDate: '2026-07-09'
})
expect(models.findIndex((model) => model.id === 'gpt-6-preview')).toBeGreaterThan(0)
expect(models.some((model) => model.id === 'gpt-image')).toBeFalse()
expect(models.some((model) => model.id === 'legacy')).toBeFalse()
for (const id of ['missingOutput', 'malformedOutput', 'imageOutput']) {
expect(models.some((model) => model.id === id)).toBeFalse()
}
})
test('falls back to curated models when the catalog request fails', async () => {
const failingFetch = (async () => new Response(null, { status: 503 })) as typeof fetch
const models = await listCatalogModels('anthropic', failingFetch)
expect(models[0]).toMatchObject({ id: 'claude-sonnet-5-5', tag: 'Best for design' })
})
test('retries a failed shared request and shares a successful request', async () => {
const originalFetch = globalThis.fetch
let requests = 0
globalThis.fetch = (async () => {
requests++
if (requests === 1) return new Response(null, { status: 503 })
return new Response(
JSON.stringify({
openai: { models: { retryModel: { name: 'Retry model', tool_call: true } } }
})
)
}) as typeof fetch
try {
expect(await resolveModelsDevModel('openai', 'retryModel')).toBeNull()
const [first, second] = await Promise.all([
resolveModelsDevModel('openai', 'retryModel'),
resolveModelsDevModel('openai', 'retryModel')
])
expect(first?.name).toBe('Retry model')
expect(second).toEqual(first)
expect(requests).toBe(2)
} finally {
globalThis.fetch = originalFetch
}
})
test('preserves vision support in offline curated models', async () => {
const failingFetch = (async () => new Response(null, { status: 503 })) as typeof fetch
const openai = await listCatalogModels('openai', failingFetch)
for (const id of ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna']) {
expect(openai.find((model) => model.id === id)?.capabilities).toContain('vision')
}
const zai = await listCatalogModels('zai', failingFetch)
expect(zai.find((model) => model.id === 'glm-5v-turbo')?.capabilities).toContain('vision')
})
test('returns null when the provider or model is unknown', async () => {
expect(
await resolveModelsDevModel('openai-compatible', 'local', catalogResponse({}))
).toBeNull()
})
})