|
|
@@ -1,6 +1,6 @@
|
|
|
// @vitest-environment jsdom
|
|
|
/** Model-list editing, endpoint interrogation, and hand-declared provider creation. */
|
|
|
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
|
+import { within, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import Schema from '@deepseek-ai/schemastery'
|
|
|
import { bindSnapshotSelector, RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
@@ -179,9 +179,9 @@ interface MutateCall {
|
|
|
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 [string, Record<string, unknown>][])[0]
|
|
|
+/** The latest interrogation payload; fails the case when nothing was asked. */
|
|
|
+function lastProbe(discover: ReturnType<typeof vi.fn>): unknown {
|
|
|
+ const call = (discover.mock.calls as unknown as [string, Record<string, unknown>][]).at(-1)
|
|
|
if (call === undefined) throw new Error('no interrogation was recorded')
|
|
|
return { settingsNs: call[0], ...call[1] }
|
|
|
}
|
|
|
@@ -263,7 +263,7 @@ describe('model list editing', () => {
|
|
|
})
|
|
|
openEditor('openai')
|
|
|
expandModel(1)
|
|
|
- fireEvent.change(screen.getByLabelText(`${en.modelImageInput} 1`), { target: { value: 'enabled' } })
|
|
|
+ fireEvent.click(within(screen.getByRole('group', { name: `${en.modelInputTypes} 1` })).getByRole('checkbox', { name: en.modelInputImage }))
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalled() })
|
|
|
expect(firstMutate(mutate).ops).toEqual([{
|
|
|
@@ -279,8 +279,8 @@ describe('model list editing', () => {
|
|
|
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.modelImageInput} 1`), { target: { value: 'enabled' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.contextWindow} 1`), { target: { value: '65536' } })
|
|
|
+ fireEvent.click(within(screen.getByRole('group', { name: `${en.modelInputTypes} 1` })).getByRole('checkbox', { name: en.modelInputImage }))
|
|
|
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: '' } })
|
|
|
@@ -317,14 +317,14 @@ describe('model list editing', () => {
|
|
|
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' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.contextWindow} 1`), { target: { value: '1M' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.maxTokens} 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')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.contextWindow} 1`).value).toBe('1M')
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.maxTokens} 1`), { target: { value: '1000' } })
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 1`).value).toBe('1000')
|
|
|
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalled() })
|
|
|
@@ -340,11 +340,11 @@ describe('model list editing', () => {
|
|
|
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' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.maxTokens} 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.getByLabelText<HTMLInputElement>(`${en.maxTokens} 1`).value).toBe('abc')
|
|
|
expect(screen.getByText(`${en.model} 1: ${en.modelMaxTokensInvalid}`)).toBeTruthy()
|
|
|
expect(buttonNamed(en.apply).disabled).toBe(true)
|
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
@@ -365,8 +365,8 @@ describe('model list editing', () => {
|
|
|
// 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')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.contextWindow} 1`).value).toBe('1M')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 1`).value).toBe('256K')
|
|
|
})
|
|
|
|
|
|
it('edits one row of several and lets a cleared capacity leave the profile', async () => {
|
|
|
@@ -376,11 +376,11 @@ describe('model list editing', () => {
|
|
|
openEditor('openai')
|
|
|
|
|
|
expandModel(2)
|
|
|
- fireEvent.change(screen.getByLabelText(`${en.modelMaxTokens} 2`), { target: { value: '2048' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.maxTokens} 2`), { target: { value: '2048' } })
|
|
|
fireEvent.change(screen.getByLabelText(`${en.modelName} 2`), { target: { value: 'Second' } })
|
|
|
- fireEvent.change(screen.getByLabelText(`${en.modelContextWindow} 2`), { target: { value: '4096' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.contextWindow} 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.change(screen.getByLabelText(`${en.contextWindow} 2`), { target: { value: '' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalled() })
|
|
|
@@ -420,8 +420,8 @@ describe('model list editing', () => {
|
|
|
// '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()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 1`)).not.toBeNull()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 2`)).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('leaves an earlier row expanded and forgets the removed row\u2019s own state', async () => {
|
|
|
@@ -439,13 +439,13 @@ describe('model list editing', () => {
|
|
|
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()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 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()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 1`)).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('separates emptying the list from restoring the adapter defaults', async () => {
|
|
|
@@ -505,6 +505,23 @@ describe('capacity spellings', () => {
|
|
|
})
|
|
|
|
|
|
describe('endpoint interrogation', () => {
|
|
|
+ it.each([[undefined], [[]], [['text']]])('renders catalog input inheritance for a stored declaration of %j', async (input) => {
|
|
|
+ const discover = vi.fn(() => Promise.resolve(ok([
|
|
|
+ { id: 'vision', inputModalities: ['text', 'image'] },
|
|
|
+ ])))
|
|
|
+ const model = { id: 'vision', ...input === undefined ? {} : { input } }
|
|
|
+ const { mutate } = await mountSection({ discover, providers: { openai: { models: [model] } } })
|
|
|
+ openEditor('openai')
|
|
|
+ expandModel(1)
|
|
|
+ const types = within(screen.getByRole('group', { name: `${en.modelInputTypes} 1` }))
|
|
|
+ await waitFor(() => { expect(types.getByRole<HTMLInputElement>('checkbox', { name: en.modelInputImage }).disabled).toBe(false) })
|
|
|
+ expect(types.getByRole<HTMLInputElement>('checkbox', { name: en.modelInputImage }).checked).toBe(input?.length !== 1)
|
|
|
+ expect(discover).toHaveBeenCalledExactlyOnceWith('llm-pi-ai', { provider: 'openai' })
|
|
|
+ fireEvent.click(screen.getByText(en.apply))
|
|
|
+ await waitFor(() => { expect(screen.queryByRole('button', { name: en.apply })).toBeNull() })
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
it('asks the endpoint the form shows, with a key that is not yet stored', async () => {
|
|
|
const discover = vi.fn(() => Promise.resolve(ok([{ id: 'acme-large', contextWindow: 65_536 }])))
|
|
|
await mountSection({ discover })
|
|
|
@@ -515,7 +532,7 @@ describe('endpoint interrogation', () => {
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
|
|
|
await waitFor(() => { expect(discover).toHaveBeenCalled() })
|
|
|
- expect(firstProbe(discover)).toEqual({
|
|
|
+ expect(lastProbe(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.
|
|
|
@@ -536,7 +553,7 @@ describe('endpoint interrogation', () => {
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
|
|
|
await waitFor(() => { expect(discover).toHaveBeenCalled() })
|
|
|
- expect(firstProbe(discover)).toEqual({
|
|
|
+ expect(lastProbe(discover)).toEqual({
|
|
|
settingsNs: 'llm-pi-ai',
|
|
|
provider: 'openai',
|
|
|
baseURL: 'https://proxy.example/v1',
|
|
|
@@ -546,8 +563,8 @@ describe('endpoint interrogation', () => {
|
|
|
|
|
|
it('adopts only the picked candidates, keeping a row the user already tuned', async () => {
|
|
|
const discover = vi.fn(() => Promise.resolve(ok([
|
|
|
- { id: 'kept', contextWindow: 999 },
|
|
|
- { id: 'fresh', contextWindow: 4096, maxTokens: 2048, name: 'Fresh' },
|
|
|
+ { id: 'kept', contextWindow: 999, inputModalities: ['text', 'image'] },
|
|
|
+ { id: 'fresh', contextWindow: 4096, maxTokens: 2048, name: 'Fresh', inputModalities: ['text', 'image'] },
|
|
|
])))
|
|
|
const { mutate } = await mountSection({
|
|
|
discover,
|
|
|
@@ -565,14 +582,15 @@ describe('endpoint interrogation', () => {
|
|
|
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelId} 2`).value).toBe('fresh')
|
|
|
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelName} 2`).value).toBe('Fresh')
|
|
|
expandModel(2)
|
|
|
- expect(screen.getByLabelText<HTMLInputElement>(`${en.modelContextWindow} 2`).value).toBe('4096')
|
|
|
- expect(screen.getByLabelText<HTMLInputElement>(`${en.modelMaxTokens} 2`).value).toBe('2048')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.contextWindow} 2`).value).toBe('4096')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 2`).value).toBe('2048')
|
|
|
|
|
|
+ expect(within(screen.getByRole('group', { name: `${en.modelInputTypes} 2` })).getByRole<HTMLInputElement>('checkbox', { name: en.modelInputImage }).checked).toBe(true)
|
|
|
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, maxTokens: 2048, name: 'Fresh' },
|
|
|
+ { id: 'fresh', contextWindow: 4096, maxTokens: 2048, name: 'Fresh', input: ['text', 'image'] },
|
|
|
])
|
|
|
})
|
|
|
|
|
|
@@ -608,7 +626,7 @@ describe('endpoint interrogation', () => {
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
|
|
|
await waitFor(() => { expect(discover).toHaveBeenCalled() })
|
|
|
- expect(firstProbe(discover)).toEqual({ settingsNs: 'llm-pi-ai', provider: 'openai' })
|
|
|
+ expect(lastProbe(discover)).toEqual({ settingsNs: 'llm-pi-ai', provider: 'openai' })
|
|
|
})
|
|
|
|
|
|
it('keeps the create card asking only once it has an endpoint', () => {
|
|
|
@@ -629,7 +647,7 @@ describe('endpoint interrogation', () => {
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
|
|
|
// A provider being declared names no route, so only the endpoint travels.
|
|
|
- expect(firstProbe(scripted.discover)).toEqual({
|
|
|
+ expect(lastProbe(scripted.discover)).toEqual({
|
|
|
settingsNs: 'llm-pi-ai',
|
|
|
baseURL: 'https://acme.test/v1',
|
|
|
api: 'openai-completions',
|
|
|
@@ -643,11 +661,11 @@ describe('endpoint interrogation', () => {
|
|
|
openEditor('openai')
|
|
|
|
|
|
// The row shows what identifies a model; capacities are the exception.
|
|
|
- expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).toBeNull()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 1`)).toBeNull()
|
|
|
expandModel(1)
|
|
|
- expect(screen.getByLabelText(`${en.modelContextWindow} 1`)).toBeTruthy()
|
|
|
+ expect(screen.getByLabelText(`${en.contextWindow} 1`)).toBeTruthy()
|
|
|
expandModel(1)
|
|
|
- expect(screen.queryByLabelText(`${en.modelContextWindow} 1`)).toBeNull()
|
|
|
+ expect(screen.queryByLabelText(`${en.contextWindow} 1`)).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('closes the picker without adopting anything on cancel', async () => {
|
|
|
@@ -808,7 +826,7 @@ describe('hand-declared providers', () => {
|
|
|
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.contextWindow} 1`), { target: { value: '65536' } })
|
|
|
fireEvent.click(screen.getByText(en.create))
|
|
|
|
|
|
await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) })
|
|
|
@@ -1130,7 +1148,7 @@ describe('hand-declared providers', () => {
|
|
|
expect(buttonNamed(en.fetchModels).disabled).toBe(false)
|
|
|
expect(buttonNamed(en.create).disabled).toBe(false)
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
- expect(firstProbe(discover)).toMatchObject({ baseURL })
|
|
|
+ expect(lastProbe(discover)).toMatchObject({ baseURL })
|
|
|
})
|
|
|
|
|
|
it('normalizes surrounding whitespace before interrogating and storing a base URL', async () => {
|
|
|
@@ -1144,7 +1162,7 @@ describe('hand-declared providers', () => {
|
|
|
expect(screen.queryByText(en.customBaseUrlInvalid)).toBeNull()
|
|
|
fireEvent.click(screen.getByText(en.fetchModels))
|
|
|
await waitFor(() => { expect(discover).toHaveBeenCalledTimes(1) })
|
|
|
- expect(firstProbe(discover)).toMatchObject({ baseURL: 'https://gateway.acme.example/v1' })
|
|
|
+ expect(lastProbe(discover)).toMatchObject({ baseURL: 'https://gateway.acme.example/v1' })
|
|
|
|
|
|
fireEvent.click(await screen.findByText(en.fetchAdopt))
|
|
|
await waitFor(() => { expect(buttonNamed(en.create).disabled).toBe(false) })
|
|
|
@@ -1196,7 +1214,7 @@ describe('hand-declared providers', () => {
|
|
|
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' } })
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.contextWindow} 1`), { target: { value: '64 KiB' } })
|
|
|
|
|
|
expect(screen.getByText(`${en.model} 1: ${en.modelContextInvalid}`)).toBeTruthy()
|
|
|
expect(buttonNamed(en.create).disabled).toBe(true)
|
|
|
@@ -1211,7 +1229,7 @@ describe('hand-declared providers', () => {
|
|
|
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)}`),
|
|
|
+ fireEvent.change(screen.getByLabelText(`${en.contextWindow} ${String(at)}`),
|
|
|
{ target: { value: `${String(at)}.` } })
|
|
|
}
|
|
|
|
|
|
@@ -1219,9 +1237,9 @@ describe('hand-declared providers', () => {
|
|
|
// 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.contextWindow} 1`).value).toBe('1.')
|
|
|
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelId} 2`).value).toBe('third')
|
|
|
- expect(screen.getByLabelText<HTMLInputElement>(`${en.modelContextWindow} 2`).value).toBe('3.')
|
|
|
+ expect(screen.getByLabelText<HTMLInputElement>(`${en.contextWindow} 2`).value).toBe('3.')
|
|
|
})
|
|
|
|
|
|
it('refuses two models sharing one id', () => {
|
|
|
@@ -1535,7 +1553,7 @@ describe('API key field', () => {
|
|
|
// a round trip to be told what the field already says.
|
|
|
expect(buttonNamed(en.fetchModels).disabled).toBe(true)
|
|
|
expect(buttonNamed(en.fetchModels).title).toBe(en.keyIllegalCharacters)
|
|
|
- expect(discover).not.toHaveBeenCalled()
|
|
|
+ expect(discover).toHaveBeenCalledExactlyOnceWith('llm-pi-ai', { provider: 'openai' })
|
|
|
})
|
|
|
|
|
|
it('carries the trimmed key into an interrogation, not the padded draft', async () => {
|
|
|
@@ -1546,7 +1564,7 @@ describe('API key field', () => {
|
|
|
fireEvent.click(screen.getByRole('button', { name: en.fetchModels }))
|
|
|
|
|
|
await waitFor(() => { expect(discover).toHaveBeenCalled() })
|
|
|
- expect(firstProbe(discover)).toMatchObject({ apiKey: 'sk-abc' })
|
|
|
+ expect(lastProbe(discover)).toMatchObject({ apiKey: 'sk-abc' })
|
|
|
})
|
|
|
|
|
|
it('reloads the section after creating a hand-declared provider', async () => {
|