mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
866 lines
38 KiB
TypeScript
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()
|
|
})
|
|
})
|