// @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 'schemastery' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client' import { ModelsSection } 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, 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, ): 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: {}, user: { providers: userProviders }, applies: 'live', secrets: [], revision: 3, } } function scriptedFace(options: { providers?: Record /** User layer, when it differs from the effective section. */ userProviders?: Record 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) 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, })), }))), 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 } /** 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('hand-declared providers', () => { function mountCard(overrides: Partial[0]> = {}) { const scripted = scriptedFace() 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('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. expect(firstMutate(mutate).ops[0]?.value).toEqual({ apiKeyEnv: 'ACME_API_KEY', 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() }) })