Files
deepseek-harness/packages/client/ui-models/tests/provider-form.spec.tsx
Yichen Jiang dd36db5434 test(ui-models): cover reading a stored capacity back out of a row
Every capacity assertion typed into the field first, so the path that reads a
stored count — the one an already-configured route takes every time it is
opened — was never exercised, and the coverage gate said so. A row now opens
on stored counts and is expected to spell them `1M` and `256K`, the same
vocabulary the field accepts.

`capacityText` takes the row it is rendering rather than looking it up again
by index, which retires the impossible empty-row branch that lookup needed.
The list editor's pre-flight model check is unreachable from the card that
disables submit on the same failure, and says so where it stands.
2026-08-06 15:18:18 +08:00

866 lines
38 KiB
TypeScript

// @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<T>(value: T): RpcResponse<T> {
return { rpcId: `r-${nextRpc++}` as never, result: { ok: true, value } }
}
function fail<T>(message: string, code: string): RpcResponse<T> {
return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code, message, details: {} } as never } }
}
function piAiNamespace(
providers: Record<string, unknown>,
userProviders: Record<string, unknown> = 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<string, unknown>
/** User layer, when it differs from the effective section. */
userProviders?: Record<string, unknown>
discover?: ReturnType<typeof vi.fn>
mutate?: ReturnType<typeof vi.fn>
set?: ReturnType<typeof vi.fn>
} = {}) {
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<typeof ModelsSettingsStore>[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<typeof vi.fn>): 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<typeof vi.fn>): 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<typeof scriptedFace>[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(<ModelsSection {...injected} />)
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<HTMLInputElement>(`${en.modelContextWindow} 1`).value).toBe('1M')
fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 1`), { target: { value: '1000' } })
expect(screen.getByLabelText<HTMLInputElement>(`${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<HTMLInputElement>(`${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<HTMLInputElement>(`${en.modelContextWindow} 1`).value).toBe('1M')
expect(screen.getByLabelText<HTMLInputElement>(`${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<HTMLInputElement>(`${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<HTMLInputElement>(`${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<HTMLInputElement>(`${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<HTMLInputElement>('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(
<CustomProviderCard
taken={[]} protocols={PROTOCOLS} revision={7} api={scripted.face as never}
t={t} readOnly={false} onClose={vi.fn()}
/>,
)
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<HTMLInputElement>('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<Parameters<typeof CustomProviderCard>[0]> = {}) {
const scripted = scriptedFace()
const onClose = vi.fn()
render(
<CustomProviderCard
taken={['openai']}
protocols={PROTOCOLS}
revision={7}
api={scripted.face as never}
t={t}
readOnly={false}
onClose={onClose}
{...overrides}
/>,
)
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<HTMLInputElement>(`${en.modelId} 1`).value).toBe('first')
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelContextWindow} 1`).value).toBe('1.')
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelId} 2`).value).toBe('third')
expect(screen.getByLabelText<HTMLInputElement>(`${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<HTMLSelectElement>(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<HTMLInputElement>(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()
})
})