// @vitest-environment jsdom /** Model-list editing, endpoint interrogation, and hand-declared provider creation. */ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import Schema from '@deepseek-ai/schemastery' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' import { ModelsSection, providerCopy } from '../src/client/ModelsSection.tsx' import type { ModelsSectionInjected } from '../src/client/ModelsSection.tsx' import { CustomProviderCard } from '../src/client/CustomProviderCard.tsx' import { formatCapacity, parseCapacity } from '../src/client/DeepSeekModelsEditor.tsx' import { ModelsSettingsStore, deriveKeyRef, protocolChoices } from '../src/client/store.ts' import { en } from '../src/client/locales.ts' afterEach(cleanup) const t: ModelsSectionInjected['t'] = key => en[key] const PROTOCOLS = ['openai-completions', 'openai-responses', 'anthropic-messages'] /** The pi-ai profile shape as the host serializes it, including the layer-1 fields. */ const PiAiConfig = Schema.object({ providers: Schema.dict(Schema.object({ apiKey: Schema.string().role('secret'), apiKeyEnv: Schema.string().role('credential-ref'), displayName: Schema.string(), api: Schema.union(PROTOCOLS), baseURL: Schema.string(), models: Schema.array(Schema.object({ id: Schema.string().required(), name: Schema.string(), contextWindow: Schema.number(), maxTokens: Schema.number(), })), reasoning: Schema.union(['off', 'high']), })), }) let nextRpc = 0 function ok(value: T): RpcResponse { return { rpcId: `r-${nextRpc++}` as never, result: { ok: true, value } } } function fail(message: string, code: string): RpcResponse { return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code, message, details: {} } as never } } } function piAiNamespace( providers: Record, userProviders: Record = providers, baseProviders: Record = {}, ): SettingsNamespaceView { return { ns: 'llm-pi-ai', schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as unknown, // `value` is the effective section; `user` is only the layer this page // writes. They differ whenever a composition `base` supplies something. value: { providers }, base: { providers: baseProviders }, user: { providers: userProviders }, applies: 'live', secrets: [], revision: 3, } } function scriptedFace(options: { providers?: Record /** User layer, when it differs from the effective section. */ userProviders?: Record /** Composition layer, for a route a `cordis.yml` pins rather than the page. */ baseProviders?: Record /** Routes the adapter reports as hand-declared; the rest come back as shipped. */ declaredRoutes?: readonly string[] discover?: ReturnType mutate?: ReturnType set?: ReturnType } = {}) { const providers = options.providers ?? { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy.example/v1' }, } const namespace = piAiNamespace(providers, options.userProviders ?? providers, options.baseProviders ?? {}) const discover = options.discover ?? vi.fn(() => Promise.resolve(ok({ models: [] }))) const mutate = options.mutate ?? vi.fn(() => Promise.resolve(ok(namespace))) const set = options.set ?? vi.fn(() => Promise.resolve(ok({}))) const face = { llm: { providers: vi.fn(() => Promise.resolve(ok({ providers: Object.keys(providers).map(provider => ({ provider, displayName: provider, settingsNs: 'llm-pi-ai', settingsPath: ['providers', provider], active: true, declared: options.declaredRoutes?.includes(provider) ?? false, })), }))), models: vi.fn(() => Promise.resolve(ok({ groups: [], failures: [] }))), discoverModels: discover, }, settings: { describe: vi.fn(() => Promise.resolve(ok({ writable: true, namespaces: [namespace] }))), update: vi.fn(), replace: vi.fn(), mutate, }, credentials: { describe: vi.fn((payload: { refs: string[] }) => Promise.resolve(ok({ credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])), }))), set, unset: vi.fn(), }, } return { face, discover, mutate, set, namespace } } type WireFace = ConstructorParameters[0] /** The settings write one card produced, as the scripted face recorded it. */ interface MutateCall { ns: string expectedRevision?: number ops: { op: string; path: string[]; value?: unknown }[] } /** The first interrogation payload; fails the case when nothing was asked. */ function firstProbe(discover: ReturnType): unknown { const call = (discover.mock.calls as unknown as [unknown][])[0]?.[0] if (call === undefined) throw new Error('no interrogation was recorded') return call } /** The first recorded settings write; fails the case when nothing was written. */ function firstMutate(mutate: ReturnType): MutateCall { const call = mutate.mock.calls[0]?.[0] as MutateCall | undefined if (call === undefined) throw new Error('no settings write was recorded') return call } async function mountSection(options: Parameters[0] = {}) { const scripted = scriptedFace(options) const controller = new ModelsSettingsStore(scripted.face as unknown as WireFace) await controller.load() const injected: ModelsSectionInjected = { controller, useSnapshot: bindSnapshotSelector(controller.store), api: scripted.face as never, t, } render() return { ...scripted, controller } } /** Open the editor of one configured row and expand its customized fold. */ function openEditor(provider: string): void { const row = screen.getByText(provider).closest('li') if (row === null) throw new Error(`no row for ${provider}`) fireEvent.click(within_(row, en.edit)) const summary = document.querySelector('summary') if (summary === null) throw new Error('no customized fold') fireEvent.click(summary) } /** Open one model row's advanced fold, where the capacities live. */ function expandModel(index: number): void { fireEvent.click(screen.getByLabelText(`${en.modelAdvanced} ${index}`)) } /** The button carrying `label`, typed so its disabled/title state is readable. */ function buttonNamed(label: string): HTMLButtonElement { const found = screen.getByText(label) if (!(found instanceof HTMLButtonElement)) throw new Error(`"${label}" is not a button`) return found } /** Click the button with `label` inside `scope`. */ function within_(scope: HTMLElement, label: string): HTMLElement { const found = [...scope.querySelectorAll('button')].find(button => button.textContent === label) if (found === undefined) throw new Error(`no "${label}" button`) return found } describe('protocolChoices', () => { it('reads the protocols out of the namespace schema and nothing else', async () => { const { namespace } = scriptedFace() expect(protocolChoices(namespace)).toEqual(PROTOCOLS) expect(protocolChoices(undefined)).toEqual([]) const plain = { ...namespace, schema: JSON.parse(JSON.stringify(Schema.object({}).toJSON())) as unknown } expect(protocolChoices(plain)).toEqual([]) await Promise.resolve() }) }) describe('model list editing', () => { it('adds, edits, and removes rows without storing emptied optional fields', async () => { const { mutate } = await mountSection() openEditor('openai') fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) expandModel(1) fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 1`), { target: { value: '65536' } }) fireEvent.change(screen.getByLabelText(`${en.modelName} 1`), { target: { value: 'Acme' } }) // Clearing an optional field must drop it rather than store an empty value. fireEvent.change(screen.getByLabelText(`${en.modelName} 1`), { target: { value: '' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) expect(firstMutate(mutate)).toMatchObject({ ns: 'llm-pi-ai', expectedRevision: 3, ops: [{ op: 'set', path: ['providers', 'openai', 'models'], value: [{ id: 'acme-large', contextWindow: 65_536 }] }], }) }) it('names a duplicate model id in the edit flow too', async () => { const { mutate } = await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'dup' }] } }, }) openEditor('openai') fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 2`), { target: { value: 'dup' } }) // The create card refuses this in place; an edited route must not have to // learn it from the host's refusal instead. expect(screen.getByText(`${en.model} 2: ${en.modelIdDuplicate}`)).toBeTruthy() expect(buttonNamed(en.apply).disabled).toBe(true) expect(mutate).not.toHaveBeenCalled() }) it('reads K and M suffixes and keeps the text the user typed', async () => { const { mutate } = await mountSection() openEditor('openai') fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) expandModel(1) fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 1`), { target: { value: '1M' } }) fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 1`), { target: { value: '32K' } }) // The field keeps the spelling rather than snapping to the expansion, and // a plain count is not rewritten into a suffix mid-word either. expect(screen.getByLabelText(`${en.modelContextWindow} 1`).value).toBe('1M') fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 1`), { target: { value: '1000' } }) expect(screen.getByLabelText(`${en.modelMaxTokens} 1`).value).toBe('1000') fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) // What lands in settings is always a plain token count. expect(firstMutate(mutate).ops[0]?.value) .toEqual([{ id: 'm', contextWindow: 1_000_000, maxTokens: 1000 }]) }) it('refuses to apply while a capacity is unreadable', async () => { const { mutate } = await mountSection() openEditor('openai') fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) expandModel(1) fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 1`), { target: { value: 'abc' } }) // Silently dropping it would store a route sized differently from what the // field shows, so the text stays put and the write is refused instead. expect(screen.getByLabelText(`${en.modelMaxTokens} 1`).value).toBe('abc') expect(screen.getByText(`${en.model} 1: ${en.modelMaxTokensInvalid}`)).toBeTruthy() expect(buttonNamed(en.apply).disabled).toBe(true) expect(mutate).not.toHaveBeenCalled() }) it('spells a stored capacity back the way it is typed', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'kept', contextWindow: 1_000_000, maxTokens: 256_000 }], }, }, }) openEditor('openai') expandModel(1) // Opening a row reads the stored counts, which are plain integers; showing // them as such would make an already-configured route look unlike one the // user just typed, and re-applying would rewrite the field it read. expect(screen.getByLabelText(`${en.modelContextWindow} 1`).value).toBe('1M') expect(screen.getByLabelText(`${en.modelMaxTokens} 1`).value).toBe('256K') }) it('edits one row of several and lets a cleared capacity leave the profile', async () => { const { mutate } = await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }] } }, }) openEditor('openai') expandModel(2) fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 2`), { target: { value: '2048' } }) fireEvent.change(screen.getByLabelText(`${en.modelName} 2`), { target: { value: 'Second' } }) fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 2`), { target: { value: '4096' } }) // Clearing it back to empty must drop the field, not store a zero. fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 2`), { target: { value: '' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) expect(firstMutate(mutate).ops[0]?.value).toEqual([ { id: 'first' }, { id: 'second', name: 'Second', maxTokens: 2048 }, ]) }) it('shows the adapter defaults as inherited until an edit takes them over', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1' } } }) openEditor('openai') // The user layer names no models, so the list belongs to the adapter and // says so; taking it over is an explicit act, not a side effect of opening. expect(screen.getByText(en.modelsInherited)).toBeTruthy() expect(screen.queryByText(en.resetModels)).toBeNull() }) it('keeps expansion on the row it belongs to after an earlier one is removed', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }, { id: 'third' }], }, }, }) openEditor('openai') // Expansion is keyed by position, so removing an earlier row shifts the // rest down; without reindexing, row 3 would inherit row 2's open state. expandModel(2) fireEvent.click(screen.getByLabelText(`${en.removeModel} 1`)) // 'second' now sits at position 1 and keeps its capacities open; 'third' // moved to position 2 and stays folded. expect(screen.getByLabelText(`${en.modelId} 1`).value).toBe('second') expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).not.toBeNull() expect(screen.queryByLabelText(`${en.modelContextWindow} 2`)).toBeNull() }) it('leaves an earlier row expanded and forgets the removed row\u2019s own state', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }, { id: 'third' }], }, }, }) openEditor('openai') // A row before the removal keeps its own position and stays open. expandModel(1) fireEvent.click(screen.getByLabelText(`${en.removeModel} 2`)) expect(screen.getByLabelText(`${en.modelId} 1`).value).toBe('first') expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).not.toBeNull() // Removing the expanded row itself drops that state rather than handing it // to whichever row slides into the position. fireEvent.click(screen.getByLabelText(`${en.removeModel} 1`)) expect(screen.getByLabelText(`${en.modelId} 1`).value).toBe('third') expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).toBeNull() }) it('separates emptying the list from restoring the adapter defaults', async () => { const { mutate } = await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'kept' }] } }, }) openEditor('openai') // An empty override is a route that serves no models — a different intent // from handing the catalog back, which is what the reset affordance does. expect(screen.getByText(en.modelsCustomized)).toBeTruthy() fireEvent.click(screen.getByText(en.resetModels)) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) expect(firstMutate(mutate).ops) .toContainEqual({ op: 'unset', path: ['providers', 'openai', 'models'] }) }) }) describe('capacity spellings', () => { it.each([ ['', undefined], ['65536', 65_536], ['256K', 256_000], ['1m', 1_000_000], // A decimal multiple is exact in intent but not in binary floating point, // so an integral result snaps back instead of landing a few ULPs high. ['2.3M', 2_300_000], // Not an integral count: kept as written rather than silently rounded. ['1.0005K', 1000.5], ])('reads %j as %j', (text, expected) => { expect(parseCapacity(text)).toBe(expected) }) it.each(['abc', '12x', '1 000', '-5', ''])('refuses %j rather than guessing', (text) => { const parsed = parseCapacity(text) expect(parsed === undefined || Number.isNaN(parsed)).toBe(true) }) it.each([ [1_000_000, '1M'], [256_000, '256K'], [65_536, '65536'], // Never a spelling that would not survive being read back. [0, '0'], [1.5, '1.5'], ])('spells %j as %j', (value, expected) => { expect(formatCapacity(value)).toBe(expected) }) it('round-trips every spelling it produces', () => { for (const value of [1_000_000, 256_000, 65_536, 4096, 1000]) { expect(parseCapacity(formatCapacity(value))).toBe(value) } }) }) describe('endpoint interrogation', () => { it('asks the endpoint the form shows, with a key that is not yet stored', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'acme-large', contextWindow: 65_536 }] }))) await mountSection({ discover }) openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'typed-not-saved' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://edited.example/v1' } }) fireEvent.click(screen.getByText(en.fetchModels)) await waitFor(() => { expect(discover).toHaveBeenCalled() }) expect(firstProbe(discover)).toEqual({ settingsNs: 'llm-pi-ai', // The route is named, so an adapter that already describes it answers // from its own registry rather than the endpoint. provider: 'openai', baseURL: 'https://edited.example/v1', apiKey: 'typed-not-saved', }) }) it('carries the protocol the profile already names', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [] }))) await mountSection({ discover, providers: { openai: { baseURL: 'https://proxy.example/v1', api: 'openai-responses' } }, }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await waitFor(() => { expect(discover).toHaveBeenCalled() }) expect(firstProbe(discover)).toEqual({ settingsNs: 'llm-pi-ai', provider: 'openai', baseURL: 'https://proxy.example/v1', api: 'openai-responses', }) }) it('adopts only the picked candidates, keeping a row the user already tuned', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'kept', contextWindow: 999 }, { id: 'fresh', contextWindow: 4096, name: 'Fresh' }], }))) const { mutate } = await mountSection({ discover, providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'kept', contextWindow: 111 }] } }, }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(en.fetchTitle) // The already-configured row starts unchecked; the new one starts checked. const boxes = [...document.querySelectorAll('input[type="checkbox"]')] expect(boxes.map(box => box.checked)).toEqual([false, true]) fireEvent.click(screen.getByText(en.fetchAdopt)) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) expect(firstMutate(mutate).ops[0]?.value).toEqual([ { id: 'kept', contextWindow: 111 }, { id: 'fresh', contextWindow: 4096, name: 'Fresh' }, ]) }) it('keeps the rows editable when the provider cannot be interrogated', async () => { const discover = vi.fn(() => Promise.resolve( fail('https://proxy.example/v1/models answered 401; check the API key', 'model-discovery-failed'), )) await mountSection({ discover }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(/answered 401; check the API key/) // The failure is a detour, not a dead end: hand-entry is still offered. expect(screen.getByRole('button', { name: en.addModel })).toBeTruthy() }) it('reports an empty listing and a rejected transport', async () => { const empty = vi.fn(() => Promise.resolve(ok({ models: [] }))) await mountSection({ discover: empty }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(en.fetchEmpty) cleanup() const rejected = vi.fn(() => Promise.reject(new Error('carrier down'))) await mountSection({ discover: rejected }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText('carrier down') }) it('can be asked for a configured route even with no endpoint', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'from-registry' }] }))) await mountSection({ discover, providers: { openai: {} } }) openEditor('openai') // A route the adapter already describes needs no endpoint at all. expect(buttonNamed(en.fetchModels).disabled).toBe(false) fireEvent.click(screen.getByText(en.fetchModels)) await waitFor(() => { expect(discover).toHaveBeenCalled() }) expect(firstProbe(discover)).toEqual({ settingsNs: 'llm-pi-ai', provider: 'openai' }) }) it('keeps the create card asking only once it has an endpoint', () => { // A provider being declared has no route yet, so the endpoint is the only // thing an interrogation could go on. const scripted = scriptedFace() render( , ) expect(buttonNamed(en.fetchModels).disabled).toBe(true) expect(buttonNamed(en.fetchModels).title).toBe(en.fetchNeedsBaseUrl) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) expect(buttonNamed(en.fetchModels).disabled).toBe(false) fireEvent.click(screen.getByText(en.fetchModels)) // A provider being declared names no route, so only the endpoint travels. expect(firstProbe(scripted.discover)).toEqual({ settingsNs: 'llm-pi-ai', baseURL: 'https://acme.test/v1', api: 'openai-completions', }) }) it('folds a row\u2019s capacities away until they are asked for', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'only' }] } }, }) openEditor('openai') // The row shows what identifies a model; capacities are the exception. expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).toBeNull() expandModel(1) expect(screen.getByLabelText(`${en.modelContextWindow} 1`)).toBeTruthy() expandModel(1) expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).toBeNull() }) it('closes the picker without adopting anything on cancel', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'fresh' }] }))) const { mutate } = await mountSection({ discover }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) const dialog = await screen.findByRole('dialog') // The editor card carries a Cancel of its own; this one is the dialog's. fireEvent.click(within_(dialog, en.cancel)) await waitFor(() => { expect(screen.queryByText(en.fetchTitle)).toBeNull() }) expect(mutate).not.toHaveBeenCalled() }) it('toggles a candidate off and back on before adopting', async () => { const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'a' }, { id: 'b', maxTokens: 2048 }], }))) const { mutate } = await mountSection({ discover }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(en.fetchTitle) const boxes = [...document.querySelectorAll('input[type="checkbox"]')] const first = boxes[0] as HTMLInputElement fireEvent.click(first) fireEvent.click(first) fireEvent.click(screen.getByText(en.fetchAdopt)) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) // A disclosed output cap rides along with the candidate that has one. expect(firstMutate(mutate).ops[0]?.value).toEqual([{ id: 'a' }, { id: 'b', maxTokens: 2048 }]) }) }) describe('provider rows', () => { it('tags the routes the adapter declared, and only those', async () => { await mountSection({ providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' }, 'acme-gateway': { apiKeyEnv: 'ACME_GATEWAY_API_KEY', baseURL: 'https://acme.test/v1' }, }, declaredRoutes: ['acme-gateway'], }) const rowOf = (provider: string): HTMLElement => { const row = screen.getByText(provider).closest('li') if (row === null) throw new Error(`no row for ${provider}`) return row } expect(rowOf('acme-gateway').textContent).toContain(en.customTag) // `openai` carries a stored profile too — the tag follows the adapter's // catalog, not the presence of settings, so it stays off here. expect(rowOf('openai').textContent).not.toContain(en.customTag) }) it('shows no tag when the adapter draws no catalog distinction', async () => { const scripted = scriptedFace({ providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } }) scripted.face.llm.providers = vi.fn(() => Promise.resolve(ok({ providers: [{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true, }], }))) as never const controller = new ModelsSettingsStore(scripted.face as unknown as WireFace) await controller.load() render() // Absent is "unknown", never "shipped": an adapter that answers nothing // must not have its routes labelled either way. expect(screen.queryByText(en.customTag)).toBeNull() }) }) describe('hand-declared providers', () => { function mountCard( overrides: Partial[0]> = {}, wire: Parameters[0] = {}, ) { const scripted = scriptedFace(wire) const onClose = vi.fn() render( , ) return { ...scripted, onClose } } it('writes the whole profile and the key under the derived reference', async () => { const { mutate, set, onClose } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme-gateway' } }) fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: 'Acme Gateway' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'gw-key' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) expandModel(1) fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 1`), { target: { value: '65536' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) expect(firstMutate(mutate)).toEqual({ ns: 'llm-pi-ai', ops: [{ op: 'set', path: ['providers', 'acme-gateway'], value: { displayName: 'Acme Gateway', apiKeyEnv: 'ACME_GATEWAY_API_KEY', api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1', models: [{ id: 'acme-large', contextWindow: 65_536 }], }, }], // The section this card was drafted over: a route another tab declared // meanwhile makes this a conflict rather than an overwrite. expectedRevision: 7, }) expect(set).toHaveBeenCalledWith({ ref: 'ACME_GATEWAY_API_KEY', value: 'gw-key' }) }) it('scopes each card to fields a provider can actually own', async () => { // Reasoning effort is a per-MODEL capability and the // models under one provider disagree about it, so a provider-scoped // control could only be set to a value some of them reject — which would // take the whole provider out of the picker. The composer's model picker // owns the choice, and a switch there records provider+model+effort together. const fields = () => [...document.querySelectorAll('input,select')] .map(el => el.getAttribute('aria-label')).filter(Boolean) mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) expect(fields()).toEqual([en.customRoute, en.customDisplayName, en.baseUrl, en.customApi, en.keyInput]) cleanup() // A shipped route's models each carry their own protocol, so its editor // offers no route-level protocol to override them with. await mountSection({ providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } }) openEditor('openai') fireEvent.click(screen.getByText(en.customized)) expect(fields()).toEqual([en.keyInput, en.baseUrl]) cleanup() // A hand-declared route named its own protocol at creation, so editing it // reaches the same field the create card asked for. await mountSection({ providers: { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1' } }, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') expect(fields()).toEqual([en.keyInput, en.customDisplayName, en.baseUrl, en.customApi]) }) it('renames a declared route and falls back to its id when the name is cleared', async () => { const { mutate } = await mountSection({ providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions', baseURL: 'https://acme.test/v1' }, }, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') const name = screen.getByLabelText(en.customDisplayName) expect(name.value).toBe('Acme Gateway') // The route id, not the stored name: it is what the route will be called // the moment the field is cleared. expect(name.placeholder).toBe('acme-gateway') fireEvent.change(name, { target: { value: 'Acme 网关' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) }) expect(firstMutate(mutate).ops) .toEqual([{ op: 'set', path: ['providers', 'acme-gateway', 'displayName'], value: 'Acme 网关' }]) }) it('offers the composition name as what a cleared field falls back to', async () => { // A `cordis.yml` can pin a route the catalog does not ship, so a declared // route's profile is not always the page's own. The field edits the user // layer alone, and clearing it restores the layer beneath — the // composition name here, not the route id — so that is what it offers. await mountSection({ providers: { 'acme-gateway': { displayName: 'Acme (pinned)', api: 'openai-completions' } }, baseProviders: { 'acme-gateway': { displayName: 'Acme (pinned)', api: 'openai-completions' } }, userProviders: {}, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') const name = screen.getByLabelText(en.customDisplayName) expect(name.value).toBe('') expect(name.placeholder).toBe('Acme (pinned)') }) it('names the provider as the refreshed directory reports it after a rename', async () => { // The status line used to echo the target captured when the card opened, // which never lied while the name could not change. It can now. const { face } = await mountSection({ providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions' } }, declaredRoutes: ['acme-gateway'], }) // The reload after the write answers with the renamed route, exactly as // the adapter re-registers it. face.llm.providers = vi.fn(() => Promise.resolve(ok({ providers: [{ provider: 'acme-gateway', displayName: 'Acme 网关', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], active: true, declared: true, }], }))) openEditor('acme-gateway') fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: 'Acme 网关' } }) fireEvent.click(screen.getByText(en.apply)) const notice = await screen.findByRole('status') expect(notice.textContent).toBe(providerCopy(en.savedProvider, { provider: 'acme-gateway', displayName: 'Acme 网关', })) }) it('drops the stored name rather than storing an empty one the adapter refuses', async () => { // `llm-pi-ai` rejects an empty displayName outright, so clearing the field // must unset it — which is also what the user means: use the route id. const { mutate } = await mountSection({ providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions' } }, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: ' ' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) }) expect(firstMutate(mutate).ops) .toEqual([{ op: 'unset', path: ['providers', 'acme-gateway', 'displayName'] }]) }) it('edits the protocol a declared route was created with', async () => { const { mutate } = await mountSection({ providers: { 'acme-gateway': { apiKeyEnv: 'ACME_GATEWAY_API_KEY', api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1', models: [{ id: 'acme-large' }], }, }, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') const protocol = screen.getByLabelText(en.customApi) expect(protocol.value).toBe('openai-completions') fireEvent.change(protocol, { target: { value: 'anthropic-messages' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) }) // Only the protocol travels: every other stored field is unchanged, so no // op restates it. expect(firstMutate(mutate)).toEqual({ ns: 'llm-pi-ai', ops: [{ op: 'set', path: ['providers', 'acme-gateway', 'api'], value: 'anthropic-messages' }], expectedRevision: 3, }) }) it('selects nothing for a declared route whose profile names no protocol', async () => { // A route hand-written into settings.yaml with no model needs no protocol // to resolve, so the card can be opened over one. The select must not read // as if that route had picked its first choice. await mountSection({ providers: { 'acme-gateway': { baseURL: 'https://gateway.acme.example/v1' } }, declaredRoutes: ['acme-gateway'], }) openEditor('acme-gateway') expect(screen.getByLabelText(en.customApi).value).toBe('') }) it('retries only the key after the profile landed, and reports the provider on cancel', async () => { const set = vi.fn() .mockResolvedValueOnce(fail('credential store is read-only', 'credential-rejected')) .mockResolvedValueOnce(ok({})) const { mutate, onClose } = mountCard({}, { set }) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: ' gw-key ' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) // The profile landed; only the key failed. The card says so and stays open. await waitFor(() => { expect(screen.getByText('credential store is read-only')).toBeTruthy() }) expect(onClose).not.toHaveBeenCalled() expect(mutate).toHaveBeenCalledTimes(1) // The key is stored trimmed, matching the editor. expect(set).toHaveBeenNthCalledWith(1, { ref: 'ACME_API_KEY', value: 'gw-key' }) // The provider exists now, so the fields describing it are settled and // only the key can still be corrected. expect(screen.getByLabelText(en.customRoute).disabled).toBe(true) expect(screen.getByLabelText(en.baseUrl).disabled).toBe(true) expect(screen.getByLabelText(en.keyInput).disabled).toBe(false) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'gw-key-2' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) // Re-running the profile write would carry the revision this card's own // first write superseded, so the Host would answer settings-conflict and // the key could never be stored from here at all. expect(mutate).toHaveBeenCalledTimes(1) expect(set).toHaveBeenNthCalledWith(2, { ref: 'ACME_API_KEY', value: 'gw-key-2' }) }) it('reports the created provider when cancelled after its profile landed', async () => { const set = vi.fn().mockResolvedValue(fail('nope', 'credential-rejected')) const { onClose } = mountCard({}, { set }) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'gw-key' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(screen.getByText('nope')).toBeTruthy() }) // Walking away leaves a real provider behind; reporting no change would // leave the page without the row it now has. fireEvent.click(screen.getByText(en.cancel)) expect(onClose).toHaveBeenCalledWith(true) }) it('never contradicts a filled-in field with the next gate\u2019s copy', () => { mountCard() const routeField = screen.getByLabelText(en.customRoute) fireEvent.change(routeField, { target: { value: '2' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) // The route field explains itself right under the input; the shared line // must stay silent rather than falling through to "no models yet" while // the list above plainly has one. expect(screen.getByText(en.customRouteInvalid)).toBeTruthy() expect(screen.queryByText(en.customNeedsModels)).toBeNull() // Fixing the route hands the line back to the gate that is actually unmet. fireEvent.change(routeField, { target: { value: 'acme' } }) expect(screen.queryByText(en.customNeedsModels)).toBeNull() expect(buttonNamed(en.create).disabled).toBe(false) }) it('refuses a route id whose derived credential reference would be illegal', () => { mountCard() const routeField = screen.getByLabelText(en.customRoute) fireEvent.change(routeField, { target: { value: 'https://acme.test/v1' } }) // Without this check a digit-leading id passes the card and fails at the // credential seam with a raw regular expression: the // reference derives as `123_API_KEY`, and a credential reference is a // POSIX shell identifier, which cannot start with a digit. fireEvent.change(routeField, { target: { value: '123' } }) expect(screen.getByText(en.customRouteInvalid)).toBeTruthy() expect(buttonNamed(en.create).disabled).toBe(true) fireEvent.change(routeField, { target: { value: 'a1' } }) expect(screen.queryByText(en.customRouteInvalid)).toBeNull() }) it('styles a rejected route id as a fault and its guidance as a hint', () => { mountCard() const routeField = screen.getByLabelText(en.customRoute) // Same split the key field makes: what the user got wrong reads as a // fault, what they have yet to do reads as guidance. expect(screen.getByText(en.customRouteHint).className).toMatch(/advancedHint/) fireEvent.change(routeField, { target: { value: '2' } }) expect(screen.getByText(en.customRouteInvalid).className).toMatch(/error/) fireEvent.change(routeField, { target: { value: 'openai' } }) expect(screen.getByText(en.customRouteTaken).className).toMatch(/error/) }) it('derives a reference the credential seam accepts for every id it admits', () => { // The two rules have to stay in step; this is the relation, checked // directly rather than through the DOM. const CREDENTIAL_REF = /^[A-Za-z_][A-Za-z0-9_]*$/ for (const id of ['a', 'ds', 'a1', 'acme-gateway', 'x-1-y', 'zz9']) { expect(CREDENTIAL_REF.test(deriveKeyRef(id))).toBe(true) } }) it('names the blocked gate under the form, and nothing once it is satisfied', () => { mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) // Endpoint first: the gate names the one thing standing in the way. expect(screen.getByText(en.customNeedsBaseUrl)).toBeTruthy() fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) expect(screen.getByText(en.customNeedsModels)).toBeTruthy() // Satisfied: the shared line disappears rather than rendering empty. fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) expect(screen.queryByText(en.customNeedsBaseUrl)).toBeNull() expect(screen.queryByText(en.customNeedsModels)).toBeNull() expect(buttonNamed(en.create).disabled).toBe(false) }) it('refuses to create while a capacity is unreadable', () => { mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) expandModel(1) fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 1`), { target: { value: '64 KiB' } }) expect(screen.getByText(`${en.model} 1: ${en.modelContextInvalid}`)).toBeTruthy() expect(buttonNamed(en.create).disabled).toBe(true) }) it('keeps each half-typed capacity with its own row across a removal', () => { mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) for (const [at, id] of [[1, 'first'], [2, 'second'], [3, 'third']] as const) { fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} ${String(at)}`), { target: { value: id } }) expandModel(at) // Deliberately mid-word: the buffer exists so text like this survives. fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} ${String(at)}`), { target: { value: `${String(at)}.` } }) } // Removing the middle row: the one before keeps its position and text, the // one after moves down carrying its own, and the removed row's text goes. fireEvent.click(screen.getByLabelText(`${en.removeModel} 2`)) expect(screen.getByLabelText(`${en.modelId} 1`).value).toBe('first') expect(screen.getByLabelText(`${en.modelContextWindow} 1`).value).toBe('1.') expect(screen.getByLabelText(`${en.modelId} 2`).value).toBe('third') expect(screen.getByLabelText(`${en.modelContextWindow} 2`).value).toBe('3.') }) it('refuses two models sharing one id', () => { mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'same' } }) fireEvent.change(screen.getByLabelText(`${en.modelId} 2`), { target: { value: 'same' } }) // The adapter refuses a duplicate outright, so the form must not offer to // write one. expect(screen.getByText(`${en.model} 2: ${en.modelIdDuplicate}`)).toBeTruthy() expect(buttonNamed(en.create).disabled).toBe(true) fireEvent.change(screen.getByLabelText(`${en.modelId} 2`), { target: { value: 'other' } }) expect(buttonNamed(en.create).disabled).toBe(false) }) it('creates a model with no capacities, which the route\u2019s fallbacks size', async () => { const { mutate, onClose } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'bare' } }) // A listing that discloses nothing but ids is enough to create a working // provider; the adapter sizes what configuration leaves out. expect(buttonNamed(en.create).disabled).toBe(false) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) expect(firstMutate(mutate).ops[0]?.value).toMatchObject({ models: [{ id: 'bare' }] }) }) it('refuses to create until the route, endpoint, and a model are usable', () => { mountCard() expect(buttonNamed(en.create).disabled).toBe(true) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'Acme Gateway' } }) expect(screen.getByText(en.customRouteInvalid)).toBeTruthy() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'openai' } }) expect(screen.getByText(en.customRouteTaken)).toBeTruthy() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) expect(screen.getByText(en.customNeedsBaseUrl)).toBeTruthy() fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) expect(screen.getByText(en.customNeedsModels)).toBeTruthy() expect(buttonNamed(en.create).disabled).toBe(true) // A model row with no id is not a model. fireEvent.click(screen.getByRole('button', { name: en.addModel })) expect(buttonNamed(en.create).disabled).toBe(true) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) expect(buttonNamed(en.create).disabled).toBe(false) }) it('surfaces a refused write and a rejected transport without closing', async () => { const refused = vi.fn(() => Promise.resolve(fail('read-only settings', 'settings-rejected'))) const { onClose } = mountCard({ api: { ...scriptedFace({ mutate: refused }).face } as never }) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await screen.findByText('read-only settings') expect(onClose).not.toHaveBeenCalled() }) it('surfaces a rejected transport during create', async () => { const rejecting = vi.fn(() => Promise.reject(new Error('carrier down'))) const { onClose } = mountCard({ api: { ...scriptedFace({ mutate: rejecting }).face } as never }) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await screen.findByText('carrier down') expect(onClose).not.toHaveBeenCalled() }) it('reports a stored profile whose key write was refused', async () => { const set = vi.fn(() => Promise.resolve(fail('credential is read-only', 'credential-rejected'))) const { onClose } = mountCard({ api: { ...scriptedFace({ set }).face } as never }) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'k' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await screen.findByText('credential is read-only') expect(onClose).not.toHaveBeenCalled() }) it('creates with the chosen protocol and no display name', async () => { const { mutate, onClose } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.change(screen.getByLabelText(en.customApi), { target: { value: 'anthropic-messages' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) // No display name configured means none stored; the route id is the name. // No key typed means no reference either, matching the editor: the route // keeps its provider-native auth path instead of resolving a reference // nothing ever sets. The with-key case is covered above. expect(firstMutate(mutate).ops[0]?.value).toEqual({ api: 'anthropic-messages', baseURL: 'https://acme.test/v1', models: [{ id: 'm' }], }) }) it('offers no protocol when the namespace declares none', () => { mountCard({ protocols: [] }) expect(screen.getByLabelText(en.customApi).value).toBe('') }) it('closes without writing on cancel, and honors a read-only deployment', () => { const { onClose, mutate } = mountCard() fireEvent.click(screen.getByText(en.cancel)) expect(onClose).toHaveBeenCalledWith(false) expect(mutate).not.toHaveBeenCalled() cleanup() mountCard({ readOnly: true }) expect(screen.getByLabelText(en.customRoute).disabled).toBe(true) expect(buttonNamed(en.create).disabled).toBe(true) }) it('closes the create card when an existing row is opened for editing', async () => { await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1' } } }) fireEvent.click(screen.getByRole('button', { name: en.customAdd })) expect(screen.getByText(en.customTitle)).toBeTruthy() // Two cards at once would each be closable by the other: whichever one is // dismissed clears the shared state and discards the other's draft. openEditor('openai') expect(screen.queryByText(en.customTitle)).toBeNull() }) it('reaches the card from the section and returns to the button on cancel', async () => { await mountSection() fireEvent.click(screen.getByRole('button', { name: en.customAdd })) expect(screen.getByText(en.customTitle)).toBeTruthy() fireEvent.click(screen.getByText(en.cancel)) await waitFor(() => { expect(screen.queryByText(en.customTitle)).toBeNull() }) expect(screen.getByRole('button', { name: en.customAdd })).toBeTruthy() }) it('refuses an unusable key on the field and blocks creation', () => { const { mutate, set } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme-gateway' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-\u{1F600}' } }) // A hand-declared route reaches the same judgement as an edited one, so a // key that no header can carry never becomes a profile plus a bad secret. expect(screen.getByText(en.keyIllegalCharacters)).toBeTruthy() expect(buttonNamed(en.create).disabled).toBe(true) expect(mutate).not.toHaveBeenCalled() expect(set).not.toHaveBeenCalled() }) it('stays silent about the other gates when only the key is refused', () => { mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme-gateway' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-\u{1F600}' } }) // Route, endpoint, and models are all satisfied, so answering with the // next unmet gate would print a second, false fault beside the real one. expect(screen.getByText(en.keyIllegalCharacters)).toBeTruthy() expect(screen.queryByText(en.customNeedsModels)).toBeNull() expect(screen.queryByText(en.customNeedsBaseUrl)).toBeNull() }) it('tells a whitespace-only key what a blank field means on a create card', () => { const { mutate } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme-gateway' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: ' ' } }) // There is no stored key to keep here, so the blank case says the thing // that is true of a route being declared: it may authenticate elsewhere. expect(screen.getByText(en.keyBlankNew)).toBeTruthy() expect(screen.queryByText(en.keyBlank)).toBeNull() expect(buttonNamed(en.fetchModels).title).toBe(en.keyBlankNew) expect(buttonNamed(en.create).disabled).toBe(true) expect(mutate).not.toHaveBeenCalled() }) it('creates without a key when the route authenticates some other way', async () => { const { set, onClose } = mountCard() fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'ambient-gateway' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) expect(set).not.toHaveBeenCalled() }) }) describe('API key field', () => { it('submits with a blank key field without writing a credential', async () => { const { mutate, set } = await mountSection() openEditor('openai') // The field opens empty even for a provider whose key is stored, where it // means "keep that one" — so editing anything else must not require it. fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://moved.example/v1' } }) expect(buttonNamed(en.apply).disabled).toBe(false) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) expect(set).not.toHaveBeenCalled() }) it('clears a whitespace-only base URL instead of writing the spaces', async () => { const { mutate } = await mountSection() openEditor('openai') // The field renders this as empty, so the draft must agree: storing the // spaces would hand both adapters a non-empty string they accept as a URL. fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: ' ' } }) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(mutate).toHaveBeenCalled() }) const ops = firstMutate(mutate).ops expect(ops.some(op => op.op === 'set' && op.path.includes('baseURL'))).toBe(false) expect(ops.some(op => op.op === 'unset' && op.path.includes('baseURL'))).toBe(true) }) it('blocks submit and names the field when the key holds only whitespace', async () => { const { mutate, set } = await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: ' ' } }) expect(screen.getByText(en.keyBlank)).toBeTruthy() expect(buttonNamed(en.apply).disabled).toBe(true) expect(mutate).not.toHaveBeenCalled() expect(set).not.toHaveBeenCalled() }) it('blocks submit when the key contains characters no header can carry', async () => { const { set } = await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-\u{1F600}' } }) expect(screen.getByText(en.keyIllegalCharacters)).toBeTruthy() expect(buttonNamed(en.apply).disabled).toBe(true) expect(set).not.toHaveBeenCalled() }) it('blocks submit when a whole NAME=value line was pasted', async () => { await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'OPENAI_API_KEY=sk-abc' } }) expect(screen.getByText(en.keyIllegalCharacters)).toBeTruthy() expect(buttonNamed(en.apply).disabled).toBe(true) }) it('trims a padded key before storing it', async () => { const { set } = await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: ' sk-abc ' } }) expect(buttonNamed(en.apply).disabled).toBe(false) fireEvent.click(screen.getByText(en.apply)) await waitFor(() => { expect(set).toHaveBeenCalled() }) expect((set.mock.calls[0]?.[0] as { value: string }).value).toBe('sk-abc') }) it('blocks the interrogation too, rather than spending a round trip on a refused key', async () => { const { discover } = await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-\u{1F600}' } }) // The host would refuse this before building the header anyway; asking is // a round trip to be told what the field already says. expect(buttonNamed(en.fetchModels).disabled).toBe(true) expect(buttonNamed(en.fetchModels).title).toBe(en.keyIllegalCharacters) expect(discover).not.toHaveBeenCalled() }) it('carries the trimmed key into an interrogation, not the padded draft', async () => { const { discover } = await mountSection() openEditor('openai') fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: ' sk-abc ' } }) fireEvent.click(screen.getByRole('button', { name: en.fetchModels })) await waitFor(() => { expect(discover).toHaveBeenCalled() }) expect(firstProbe(discover)).toMatchObject({ apiKey: 'sk-abc' }) }) it('reloads the section after creating a hand-declared provider', async () => { const { controller, mutate } = await mountSection() const load = vi.spyOn(controller, 'load') fireEvent.click(screen.getByRole('button', { name: en.customAdd })) fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() }) await waitFor(() => { expect(load).toHaveBeenCalledOnce() }) expect(screen.queryByText(en.customTitle)).toBeNull() }) })