mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
* fix(llm): generate provider metadata and clarify credential hints * fix(settings): shorten credential hints and mark detected sources in green * fix(llm): verify staged provider metadata and document regeneration * fix(provider-metadata): reconcile authentication hints and key controls * test(user-text): calibrate ratio headroom across unmatched workloads * test(gates): include provider metadata in hygiene expectations * fix(models): hide authentication hints for entered keys
2426 lines
117 KiB
TypeScript
2426 lines
117 KiB
TypeScript
// @vitest-environment jsdom
|
||
/** Section, setup-card, and hand-written editor behavior over a scripted wire face. */
|
||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
import Schema from '@deepseek-ai/schemastery'
|
||
import { Context } from '@deepseek-ai/cordis'
|
||
import { bindSnapshotSelector, RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
||
import type {
|
||
CredentialInfo, RemoteResult, SettingsNamespaceView,
|
||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
||
import {
|
||
ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile,
|
||
} from '../src/client/ModelsSection.tsx'
|
||
import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx'
|
||
import { ProviderEditor, pathOps } from '../src/client/ProviderEditor.tsx'
|
||
import type { ProviderEditorProps } from '../src/client/ProviderEditor.tsx'
|
||
import {
|
||
DeepSeekModelsEditor, formatCapacity, modelDrafts, parseCapacity, validateDeepSeekModels,
|
||
} from '../src/client/DeepSeekModelsEditor.tsx'
|
||
import { apiKeyFailure } from '../src/client/apiKey.ts'
|
||
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts'
|
||
import { deriveKeyRef, ModelsSettingsStore } from '../src/client/store.ts'
|
||
import { createModelsOperations } from '../src/client/operations.ts'
|
||
import type { ModelsOperations } from '../src/client/operations.ts'
|
||
import type { ProviderRow } from '../src/client/store.ts'
|
||
import { en } from '../src/client/locales.ts'
|
||
import { settingsSchema } from './settings-schema.client.ts'
|
||
|
||
afterEach(cleanup)
|
||
|
||
const t: ModelsSectionInjected['t'] = key => en[key]
|
||
const OPENAI_TARGET = { provider: 'openai', displayName: 'openai' }
|
||
const openaiCopy = (template: string): string => providerCopy(template, OPENAI_TARGET)
|
||
const DEEPSEEK_TARGET = { provider: 'deepseek-official', displayName: 'DeepSeek' }
|
||
const deepSeekCopy = (template: string): string => providerCopy(template, DEEPSEEK_TARGET)
|
||
|
||
/** Open one row's capacity disclosure (1-based, as the labels read). */
|
||
function expandRow(position: number): void {
|
||
fireEvent.click(screen.getByLabelText(`${en.modelAdvanced} ${String(position)}`))
|
||
}
|
||
|
||
/** The capacity inputs of every open row, in row order. */
|
||
function capacityInputs(label: string): HTMLInputElement[] {
|
||
return screen.getAllByLabelText<HTMLInputElement>(new RegExp(label))
|
||
}
|
||
|
||
const PiAiConfig = Schema.object({
|
||
providers: Schema.dict(Schema.object({
|
||
apiKeyEnv: Schema.string().role('credential-ref'),
|
||
api: Schema.union(['openai-completions', 'openai-responses', 'anthropic-messages']),
|
||
baseURL: Schema.string(),
|
||
reasoning: Schema.union(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
|
||
headers: Schema.dict(Schema.string()),
|
||
})),
|
||
})
|
||
|
||
const DeepSeekConfig = Schema.object({
|
||
apiKeyEnv: Schema.string().role('credential-ref'),
|
||
baseURL: Schema.string().pattern(/^https:\/\//),
|
||
reasoningEffort: Schema.union(['off', 'low', 'high', 'max']),
|
||
defaultContextWindow: Schema.number().step(1).min(1),
|
||
models: Schema.array(Schema.object({
|
||
id: Schema.string().required(),
|
||
name: Schema.string(),
|
||
description: Schema.string(),
|
||
contextWindow: Schema.number().step(1).min(1),
|
||
// The adapter declares its catalog as a schema default rather than a
|
||
// composition entry, which is what the restore-defaults path has to read.
|
||
})).default([
|
||
{
|
||
id: 'deepseek-v4-flash',
|
||
name: 'DeepSeek-V4-Flash',
|
||
description: '',
|
||
contextWindow: 1_000_000,
|
||
},
|
||
{
|
||
id: 'deepseek-v4-pro',
|
||
name: 'DeepSeek-V4-Pro',
|
||
description: '',
|
||
contextWindow: 1_000_000,
|
||
},
|
||
]),
|
||
})
|
||
|
||
const DEFAULT_DEEPSEEK_MODELS = [
|
||
{
|
||
id: 'deepseek-v4-flash',
|
||
name: 'DeepSeek-V4-Flash',
|
||
description: 'Preserved hidden detail',
|
||
contextWindow: 1_000_000,
|
||
},
|
||
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 },
|
||
]
|
||
|
||
function wireNamespaces(): SettingsNamespaceView[] {
|
||
return [
|
||
{
|
||
ns: 'llm-deepseek',
|
||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||
value: {
|
||
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
||
baseURL: 'https://base',
|
||
defaultContextWindow: 1_000_000,
|
||
maxTokens: 256_000,
|
||
models: DEFAULT_DEEPSEEK_MODELS,
|
||
},
|
||
base: { defaultContextWindow: 1_000_000, maxTokens: 256_000, models: DEFAULT_DEEPSEEK_MODELS },
|
||
user: { baseURL: 'https://base' },
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
},
|
||
{
|
||
ns: 'llm-plain',
|
||
schema: JSON.parse(JSON.stringify(Schema.object({
|
||
profiles: Schema.dict(Schema.object({ note: Schema.string() })),
|
||
}).toJSON())) as JsonValue,
|
||
value: {},
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
},
|
||
{
|
||
ns: 'llm-pi-ai',
|
||
schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
|
||
value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
||
user: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
},
|
||
{
|
||
ns: 'subagent-model-selection',
|
||
schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as JsonValue,
|
||
value: { enabled: false },
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 4,
|
||
},
|
||
{
|
||
ns: 'llm-deepseek-account',
|
||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||
value: {
|
||
baseURL: 'https://base',
|
||
defaultContextWindow: 1_000_000,
|
||
maxTokens: 256_000,
|
||
models: DEFAULT_DEEPSEEK_MODELS,
|
||
},
|
||
base: { defaultContextWindow: 1_000_000, maxTokens: 256_000, models: DEFAULT_DEEPSEEK_MODELS },
|
||
user: {},
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
},
|
||
]
|
||
}
|
||
|
||
/** Credentials answers over the Remote carrier, which has no envelope. */
|
||
function remoteOk<T>(value: T) {
|
||
return { ok: true as const, value }
|
||
}
|
||
/** The codes this page's scripted Host answers refuse with. */
|
||
type RefusalCode = 'credential/rejected' | 'gateway/internal' | 'settings/conflict' | 'settings/rejected'
|
||
|
||
/** One refusal per code, each carrying the details its own code declares. */
|
||
const REFUSALS: { [Code in RefusalCode]: (message: string) => RemoteError<Code> } = {
|
||
'credential/rejected': message => new RemoteError('credential/rejected', message, { ref: 'DEEPSEEK_API_KEY' }),
|
||
'gateway/internal': message => new RemoteError('gateway/internal', message, {}),
|
||
'settings/conflict': message =>
|
||
new RemoteError('settings/conflict', message, { ns: 'llm-pi-ai', expected: 4, actual: 5 }),
|
||
'settings/rejected': message => new RemoteError('settings/rejected', message, { ns: 'llm-pi-ai' }),
|
||
}
|
||
function remoteFail(message: string, code: RefusalCode = 'credential/rejected') {
|
||
return { ok: false as const, error: REFUSALS[code](message) }
|
||
}
|
||
|
||
function scriptedFace(overrides: {
|
||
update?: ReturnType<typeof vi.fn>
|
||
mutate?: ReturnType<typeof vi.fn>
|
||
set?: ReturnType<typeof vi.fn>
|
||
unset?: ReturnType<typeof vi.fn>
|
||
} = {}) {
|
||
const providerNamespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
||
const update = overrides.update ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
||
const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
||
const set = overrides.set ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||
const unset = overrides.unset ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||
const face = {
|
||
llm: {
|
||
listProviders: vi.fn((): ReturnType<PageContext['remote']['llm']['listProviders']> => Promise.resolve(remoteOk([
|
||
{ id: 'deepseek-official', name: 'DeepSeek' },
|
||
{ id: 'openai', name: 'openai' },
|
||
]))),
|
||
listConfigurableProviders: vi.fn(() => Promise.resolve(remoteOk([
|
||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
||
{
|
||
provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true,
|
||
defaultBaseURL: 'https://api.openai.com/v1', baseURLEnv: ['FIXTURE_ENDPOINT'], nativeAuth: { env: ['OPENAI_API_KEY'], oauth: false },
|
||
},
|
||
{
|
||
provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false,
|
||
defaultBaseURL: 'https://api.anthropic.com', baseURLEnv: ['FIXTURE_ENDPOINT'],
|
||
nativeAuth: { env: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true },
|
||
},
|
||
{ provider: 'zombie', displayName: 'zombie', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'zombie'], active: false },
|
||
{ provider: 'broken', displayName: 'broken', settingsNs: 'llm-pi-ai', settingsPath: ['nope', 'x'], active: false },
|
||
{ provider: 'plain', displayName: 'plain', settingsNs: 'llm-plain', settingsPath: ['profiles', 'plain'], active: false },
|
||
].map(({ active: _active, ...entry }) => entry)))),
|
||
discoverModels: vi.fn(() => Promise.resolve(remoteOk([]))),
|
||
},
|
||
settings: {
|
||
describe: vi.fn(() => Promise.resolve(remoteOk({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))),
|
||
update,
|
||
mutate,
|
||
},
|
||
credentials: {
|
||
// Typed as the Remote answer rather than the success branch alone: a
|
||
// case that scripts a refusal replaces this mock.
|
||
describe: vi.fn((refs: string[]): Promise<RemoteResult<Record<string, CredentialInfo>>> =>
|
||
Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, {
|
||
configured: ref === 'OPENAI_API_KEY',
|
||
...ref === 'OPENAI_API_KEY' ? { source: 'file' } : {},
|
||
writable: true,
|
||
}])),
|
||
))),
|
||
set,
|
||
unset,
|
||
},
|
||
}
|
||
return { face, update, mutate, set, unset }
|
||
}
|
||
|
||
type PageContext = ConstructorParameters<typeof ModelsSettingsStore>[0]
|
||
|
||
/**
|
||
* The page plugin's context, scripted down to the namespaces the page reaches.
|
||
* One context per face, as in production: an editor effect keyed by the context
|
||
* would otherwise re-probe on every render.
|
||
*/
|
||
const contexts = new WeakMap<object, PageContext>()
|
||
function ctxWith(face: object): PageContext {
|
||
const existing = contexts.get(face)
|
||
if (existing !== undefined) return existing
|
||
const ctx = Object.assign(new Context(), { remote: { ...face,
|
||
session: { initializeDefaultModel: async () => ({ ok: true, value: undefined }) },
|
||
} })
|
||
contexts.set(face, ctx)
|
||
return ctx
|
||
}
|
||
|
||
/**
|
||
* The cards' injected Host operations over the same script, bound once per face
|
||
* as the plugin body binds them: an editor effect keyed by this face would
|
||
* otherwise re-probe on every render.
|
||
*/
|
||
const operations = new WeakMap<object, ModelsOperations>()
|
||
function operationsWith(face: object): ModelsOperations {
|
||
const existing = operations.get(face)
|
||
if (existing !== undefined) return existing
|
||
const bound = createModelsOperations(ctxWith(face))
|
||
operations.set(face, bound)
|
||
return bound
|
||
}
|
||
|
||
/** The pi-ai namespace with one more route profile stored in the user layer. */
|
||
function piAiNamespaceWith(provider: string, profile: { [key: string]: JsonValue }): SettingsNamespaceView {
|
||
const namespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
||
const providers = { ...(namespace.value as { providers: object }).providers, [provider]: profile }
|
||
return { ...namespace, value: { providers }, user: { providers } }
|
||
}
|
||
|
||
/** Render one pi-ai route's editor with the directory and credential facts the page already holds. */
|
||
function renderPiAiEditor(
|
||
provider: string,
|
||
namespace: SettingsNamespaceView,
|
||
facts: Pick<ProviderEditorProps, 'defaultBaseURL' | 'baseURLEnv' | 'nativeAuth' | 'credential' | 'credentialChecks'> = {},
|
||
): void {
|
||
render(<ProviderEditor provider={provider} displayName={provider}
|
||
namespace={namespace} settingsPath={['providers', provider]} schema={settingsSchema}
|
||
operations={operationsWith(scriptedFace().face)} t={t} readOnly={false} onClose={vi.fn()} {...facts} />)
|
||
}
|
||
|
||
/** Hover the key label's help button and read its tooltip; null when the label has no button. */
|
||
function authHelpText(): string | null {
|
||
const button = screen.queryByRole('button', { name: en.authHelp })
|
||
if (button === null) return null
|
||
fireEvent.mouseEnter(button)
|
||
return screen.getByRole('tooltip').textContent
|
||
}
|
||
|
||
/** One recorded child-slot dispatch: seat name, owner share, kind options. */
|
||
type RenderSlotCall = [name: string, owner: Record<string, unknown>, opts?: { entryKey?: string }]
|
||
|
||
/** Child-slot dispatch stub: records every seat occurrence, renders nothing. */
|
||
function stubRenderSlot() {
|
||
return vi.fn((..._call: RenderSlotCall) => null)
|
||
}
|
||
|
||
/** The provider-card seat dispatches a stub recorded, as (route id, configured, keyConfigured, entryKey). */
|
||
function cardSeatCalls(
|
||
renderSlot: ReturnType<typeof stubRenderSlot>,
|
||
): Array<[string, boolean, boolean, string | undefined]> {
|
||
return renderSlot.mock.calls
|
||
.filter(call => call[0] === 'settings.models.provider-card')
|
||
.map(call => [
|
||
(call[1] as { provider: { provider: string } }).provider.provider,
|
||
(call[1] as { configured: boolean }).configured,
|
||
(call[1] as { keyConfigured: boolean }).keyConfigured,
|
||
call[2]?.entryKey,
|
||
])
|
||
}
|
||
|
||
async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
||
const { face, update, mutate, set, unset } = scripted
|
||
const ctx = ctxWith(face)
|
||
const mirror = new SettingsDescribeMirror(ctx)
|
||
const controller = new ModelsSettingsStore(ctx, settingsSchema, mirror)
|
||
await controller.load()
|
||
const renderSlot = stubRenderSlot()
|
||
const injected: ModelsSectionProps = {
|
||
controller,
|
||
useSnapshot: bindSnapshotSelector(controller.store),
|
||
operations: operationsWith(face),
|
||
schema: settingsSchema,
|
||
t,
|
||
renderSlot: renderSlot as unknown as ModelsSectionProps['renderSlot'],
|
||
}
|
||
const view = render(<ModelsSection {...injected} />)
|
||
return { view, ctx, face, update, mutate, set, unset, controller, mirror, renderSlot }
|
||
}
|
||
|
||
async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
||
return mountFace(scriptedFace(overrides))
|
||
}
|
||
|
||
/**
|
||
* Mount for a user who cannot reach any provider yet: no credential is stored
|
||
* anywhere, so the whole-section DeepSeek route owns the first-run setup card.
|
||
*/
|
||
async function mountFirstRun(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
||
const scripted = scriptedFace(overrides)
|
||
scripted.face.credentials.describe.mockImplementation((refs: string[]) =>
|
||
Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
||
)))
|
||
return mountFace(scripted)
|
||
}
|
||
|
||
/**
|
||
* Mount and open the DeepSeek editor. The shared fixture already has a usable
|
||
* openai route, so DeepSeek is an ordinary row whose card opens through Edit
|
||
* rather than by itself.
|
||
*/
|
||
async function mountDeepSeekCard(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
||
const mounted = await mountSection(overrides)
|
||
fireEvent.click(screen.getByRole('button', { name: deepSeekCopy(en.editProvider) }))
|
||
return mounted
|
||
}
|
||
|
||
describe('ModelsSection', () => {
|
||
it('keeps the credential state unconfirmed when its redacted read fails', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.credentials.describe.mockResolvedValue(remoteFail('credentials unavailable', 'gateway/internal'))
|
||
await mountFace(scripted)
|
||
const edit = screen.getByRole('button', { name: openaiCopy(en.editProvider) })
|
||
fireEvent.click(edit)
|
||
const row = within(edit.closest('li')!)
|
||
expect(row.getByText(en.keyFallbackUnchecked)).toBeTruthy()
|
||
expect(row.queryByText(en.keyFallbackMissing.replace('{hint}', 'OPENAI_API_KEY'))).toBeNull()
|
||
})
|
||
|
||
it('waits for the current snapshot when a setup surface remounts during refresh', async () => {
|
||
const { view, controller, face, mirror, mutate } = await mountFirstRun()
|
||
view.unmount()
|
||
const before = controller.store.getSnapshot().namespaces.get('llm-deepseek')!
|
||
mirror.acceptView({
|
||
...before,
|
||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://current.example', models: DEFAULT_DEEPSEEK_MODELS },
|
||
user: { baseURL: 'https://current.example' },
|
||
revision: 1,
|
||
})
|
||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||
const started = Promise.withResolvers<undefined>()
|
||
face.credentials.describe.mockImplementationOnce(() => { started.resolve(undefined); return read.promise })
|
||
let refreshing: Promise<void> | undefined
|
||
try {
|
||
await act(async () => { refreshing = controller.load(); await started.promise })
|
||
expect(controller.store.getSnapshot().namespaces.get('llm-deepseek')?.revision).toBe(0)
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
expect(screen.queryByRole('button', { name: en.add })).toBeNull()
|
||
} finally {
|
||
await act(async () => { read.resolve(remoteOk({})); await refreshing })
|
||
}
|
||
await screen.findByLabelText(en.keyInput)
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(url.value).toBe('https://current.example')
|
||
fireEvent.change(url, { target: { value: 'https://next.example' } })
|
||
fireEvent.click(screen.getByRole('button', { name: en.apply }))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledWith(
|
||
'llm-deepseek', [{ op: 'set', path: ['baseURL'], value: 'https://next.example' }], 1,
|
||
) })
|
||
await screen.findByRole('status')
|
||
})
|
||
|
||
it('waits for the current snapshot before remounting setup after a failed page load', async () => {
|
||
const { controller, face, mirror, mutate } = await mountFirstRun()
|
||
const oldKey = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(oldKey, { target: { value: 'sk-discarded-on-error' } })
|
||
face.llm.listProviders.mockResolvedValueOnce(remoteFail('directory unavailable', 'gateway/internal'))
|
||
await act(async () => { await controller.load() })
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
expect(screen.getByText(`${en.loadFailed}: directory unavailable`)).toBeTruthy()
|
||
const before = controller.store.getSnapshot().namespaces.get('llm-deepseek')!
|
||
mirror.acceptView({
|
||
...before,
|
||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://recovered.example', models: DEFAULT_DEEPSEEK_MODELS },
|
||
user: { baseURL: 'https://recovered.example' },
|
||
revision: 2,
|
||
})
|
||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||
const started = Promise.withResolvers<undefined>()
|
||
const describe = face.credentials.describe.getMockImplementation()!
|
||
face.credentials.describe.mockImplementation((refs) => {
|
||
if (refs.length > 1) { started.resolve(undefined); return read.promise }
|
||
return describe(refs)
|
||
})
|
||
const load = vi.spyOn(controller, 'load')
|
||
try {
|
||
fireEvent.click(screen.getByRole('button', { name: en.retry }))
|
||
await act(async () => { await started.promise })
|
||
expect(load).toHaveBeenCalledOnce()
|
||
expect(controller.store.getSnapshot()).toMatchObject({ status: 'loading' })
|
||
expect(controller.store.getSnapshot().namespaces.get('llm-deepseek')?.revision).toBe(0)
|
||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
} finally {
|
||
await act(async () => { read.resolve(remoteOk({})); await load.mock.results[0]?.value })
|
||
load.mockRestore()
|
||
}
|
||
const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(key).not.toBe(oldKey)
|
||
expect(key.value).toBe('')
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(url.value).toBe('https://recovered.example')
|
||
fireEvent.change(url, { target: { value: 'https://after-retry.example' } })
|
||
fireEvent.click(screen.getByRole('button', { name: en.apply }))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledWith(
|
||
'llm-deepseek', [{ op: 'set', path: ['baseURL'], value: 'https://after-retry.example' }], 2,
|
||
) })
|
||
await screen.findByRole('status')
|
||
})
|
||
|
||
it('preserves a mounted setup draft while its current refresh is pending', async () => {
|
||
const { controller, face } = await mountFirstRun()
|
||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(key, { target: { value: 'sk-unsaved-setup-draft' } })
|
||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||
face.credentials.describe.mockImplementationOnce(() => read.promise)
|
||
let refreshing: Promise<void> | undefined
|
||
try {
|
||
act(() => { refreshing = controller.load() })
|
||
expect(screen.getByLabelText(en.keyInput)).toBe(key)
|
||
expect(key.value).toBe('sk-unsaved-setup-draft')
|
||
} finally {
|
||
await act(async () => { read.resolve(remoteOk({})); await refreshing })
|
||
}
|
||
expect(screen.getByLabelText(en.keyInput)).toBe(key)
|
||
expect(key.value).toBe('sk-unsaved-setup-draft')
|
||
})
|
||
|
||
it('checks current readiness before opening an editor and preserves an open draft during refresh', async () => {
|
||
const { controller } = await mountSection()
|
||
const edit = screen.getByRole('button', { name: deepSeekCopy(en.editProvider) })
|
||
const ready = controller.store.getSnapshot()
|
||
const snapshot = vi.spyOn(controller.store, 'getSnapshot').mockReturnValue({ ...ready, status: 'loading' })
|
||
try {
|
||
fireEvent.click(edit)
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
} finally {
|
||
snapshot.mockRestore()
|
||
}
|
||
fireEvent.click(edit)
|
||
const key = await screen.findByLabelText(en.keyInput)
|
||
fireEvent.change(key, { target: { value: 'sk-unsaved-draft' } })
|
||
act(() => { controller.store.update((state) => { state.status = 'loading' }) })
|
||
expect((key as HTMLInputElement).value).toBe('sk-unsaved-draft')
|
||
expect((edit as HTMLButtonElement).disabled).toBe(false)
|
||
fireEvent.click(edit)
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
expect((edit as HTMLButtonElement).disabled).toBe(true)
|
||
})
|
||
|
||
it('checks current readiness before adding or starting removal from rendered rows', async () => {
|
||
const { controller, mutate, unset } = await mountSection()
|
||
const add = screen.getByRole('button', { name: en.add })
|
||
const remove = screen.getByRole('button', { name: openaiCopy(en.removeProvider) })
|
||
const ready = controller.store.getSnapshot()
|
||
const snapshot = vi.spyOn(controller.store, 'getSnapshot').mockReturnValue({ ...ready, status: 'loading' })
|
||
try {
|
||
fireEvent.click(add)
|
||
fireEvent.click(remove)
|
||
expect(screen.queryByLabelText(en.provider)).toBeNull()
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
expect(unset).not.toHaveBeenCalled()
|
||
} finally {
|
||
snapshot.mockRestore()
|
||
}
|
||
fireEvent.click(add)
|
||
expect(screen.getByLabelText(en.provider)).toBeTruthy()
|
||
})
|
||
|
||
it('blocks new Add and Remove actions while a joined refresh retains an open add draft', async () => {
|
||
const { controller, face } = await mountSection()
|
||
const add = screen.getByRole<HTMLButtonElement>('button', { name: en.add })
|
||
const remove = screen.getByRole<HTMLButtonElement>('button', { name: openaiCopy(en.removeProvider) })
|
||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||
face.credentials.describe.mockImplementationOnce(() => read.promise)
|
||
let refreshing: Promise<void> | undefined
|
||
try {
|
||
act(() => { refreshing = controller.load() })
|
||
expect(add.disabled).toBe(true)
|
||
expect(remove.disabled).toBe(true)
|
||
expect(screen.getByText('openai')).toBeTruthy()
|
||
fireEvent.click(add)
|
||
fireEvent.click(remove)
|
||
expect(screen.queryByLabelText(en.provider)).toBeNull()
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
} finally {
|
||
await act(async () => {
|
||
read.resolve(remoteOk({ OPENAI_API_KEY: { configured: true, writable: true } }))
|
||
await refreshing
|
||
})
|
||
}
|
||
fireEvent.click(add)
|
||
const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(key, { target: { value: 'sk-unsaved-add-draft' } })
|
||
act(() => { controller.store.update((state) => { state.status = 'loading' }) })
|
||
expect(key.value).toBe('sk-unsaved-add-draft')
|
||
fireEvent.click(screen.getByRole('button', { name: en.cancel }))
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
expect(screen.getByRole<HTMLButtonElement>('button', { name: en.add }).disabled).toBe(true)
|
||
})
|
||
|
||
it('waits for saved credential confirmation before removing its managed key', async () => {
|
||
const configured = new Set(['DEEPSEEK_API_KEY'])
|
||
const set = vi.fn((ref: string) => { configured.add(ref); return Promise.resolve(remoteOk(undefined)) })
|
||
const scripted = scriptedFace({ set })
|
||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||
const started = Promise.withResolvers<undefined>()
|
||
let holdJoin = false
|
||
const answer = (refs: string[]): RemoteResult<Record<string, CredentialInfo>> => remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, { configured: configured.has(ref), writable: true }])),
|
||
)
|
||
scripted.face.credentials.describe.mockImplementation((refs: string[]) => {
|
||
if (holdJoin && refs.length > 1) { started.resolve(undefined); return read.promise }
|
||
return Promise.resolve(answer(refs))
|
||
})
|
||
const { controller, unset, mutate } = await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||
const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(key, { target: { value: 'sk-new-managed-key' } })
|
||
holdJoin = true
|
||
try {
|
||
fireEvent.click(screen.getByRole('button', { name: en.apply }))
|
||
await act(async () => { await started.promise })
|
||
expect(configured.has('OPENAI_API_KEY')).toBe(true)
|
||
expect(controller.store.getSnapshot().rows.find(row => row.entry.provider === 'openai')?.credential?.configured).toBe(false)
|
||
const remove = screen.getByRole<HTMLButtonElement>('button', { name: openaiCopy(en.removeProvider) })
|
||
expect(remove.disabled).toBe(true)
|
||
fireEvent.click(remove)
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
holdJoin = false
|
||
await act(async () => { read.resolve(answer(['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'])) })
|
||
await screen.findByText(openaiCopy(en.savedProvider))
|
||
fireEvent.click(remove)
|
||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||
expect(dialog.textContent).toContain(openaiCopy(en.deleteDescriptionWithCredential))
|
||
fireEvent.click(within(dialog).getByRole('button', { name: openaiCopy(en.deleteConfirm) }))
|
||
await waitFor(() => { expect(unset).toHaveBeenCalledWith('OPENAI_API_KEY') })
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
await waitFor(() => { expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull() })
|
||
} finally {
|
||
holdJoin = false
|
||
await act(async () => { read.resolve(answer(['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'])) })
|
||
}
|
||
})
|
||
|
||
it('hides the add action when no settings namespace can open an editor', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.settings.describe.mockResolvedValue(remoteOk({ writable: true, hasDocument: false, namespaces: [] }))
|
||
await mountFace(scripted)
|
||
expect(screen.queryByRole('button', { name: en.add })).toBeNull()
|
||
})
|
||
|
||
it('offers only providers whose settings namespace can open an editor', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true, hasDocument: false,
|
||
namespaces: wireNamespaces().filter(view => view.ns !== 'llm-pi-ai'),
|
||
}))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
// Without the pi-ai namespace nothing can be hand-declared, so the card
|
||
// is the catalog form alone: no mode switch, no custom panel.
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.queryByRole('textbox', { name: en.customRoute })).toBeNull()
|
||
expect(screen.queryByRole('option', { name: 'anthropic' })).toBeNull()
|
||
expect(screen.getByRole('option', { name: 'plain' })).toBeTruthy()
|
||
})
|
||
|
||
it('shows a catalog diagnostic while keeping the provider editable', async () => {
|
||
const scripted = scriptedFace()
|
||
const failure = 'llm-pi-ai: provider "openai" model "111" needs an api'
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], error: failure },
|
||
]))
|
||
await mountFace(scripted)
|
||
expect(screen.getByRole('alert').textContent).toBe(failure)
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||
expect(await screen.findByLabelText(en.keyInput)).toBeTruthy()
|
||
expect(screen.getByRole('button', { name: en.add })).toBeTruthy()
|
||
})
|
||
|
||
it('renders nothing before the slot injects its dependencies', () => {
|
||
const uninjected = {} as ModelsSectionProps
|
||
render(<ModelsSection {...uninjected} />)
|
||
expect(document.body.textContent).toBe('')
|
||
})
|
||
|
||
it('shows a configuration diagnostic inside the first-run setup card', async () => {
|
||
const scripted = scriptedFace()
|
||
const failure = 'The provider configuration needs repair'
|
||
scripted.face.llm.listProviders.mockResolvedValue(remoteOk([
|
||
{ id: 'deepseek-official', name: 'DeepSeek' },
|
||
]))
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], error: failure },
|
||
]))
|
||
scripted.face.credentials.describe.mockResolvedValue(remoteOk({
|
||
DEEPSEEK_API_KEY: { configured: false, writable: true },
|
||
}))
|
||
await mountFace(scripted)
|
||
|
||
const card = screen.getByRole('listitem')
|
||
expect(within(card).getByRole('alert').textContent).toBe(failure)
|
||
expect(within(card).getByLabelText(en.keyInput)).toBeTruthy()
|
||
expect(within(card).queryByRole('button', { name: deepSeekCopy(en.editProvider) })).toBeNull()
|
||
})
|
||
|
||
it('dispatches the provider-card seat per rendered row, keyed by the owning namespace', async () => {
|
||
const { renderSlot } = await mountSection()
|
||
const cards = cardSeatCalls(renderSlot)
|
||
expect(cards).toContainEqual(['openai', true, true, 'llm-pi-ai'])
|
||
expect(cards).toContainEqual(['deepseek-official', true, false, 'llm-deepseek'])
|
||
// The footer seat renders once below the rows and the add controls.
|
||
expect(renderSlot.mock.calls.filter(call => call[0] === 'settings.models.footer')).toEqual([
|
||
['settings.models.footer', {}],
|
||
])
|
||
})
|
||
|
||
it('dispatches the provider-card seat inside the first-run setup card', async () => {
|
||
const { renderSlot } = await mountFirstRun()
|
||
expect(cardSeatCalls(renderSlot)).toContainEqual(['deepseek-official', true, false, 'llm-deepseek'])
|
||
})
|
||
|
||
it('dispatches the provider-card seat on the add-provider draft with its dormant row', async () => {
|
||
const { renderSlot } = await mountSection()
|
||
renderSlot.mockClear()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(cardSeatCalls(renderSlot)).toContainEqual(['anthropic', false, false, 'llm-pi-ai'])
|
||
})
|
||
|
||
it('derives the draft seat\'s key fact from the page\'s conventional reference', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, {
|
||
configured: ref === 'OPENAI_API_KEY' || ref === 'ANTHROPIC_API_KEY',
|
||
writable: true,
|
||
}])),
|
||
)))
|
||
const { renderSlot } = await mountFace(scripted)
|
||
renderSlot.mockClear()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
// The dormant row names no reference yet; the seat still reports the
|
||
// derived ANTHROPIC_API_KEY the editor itself displays as configured.
|
||
expect(cardSeatCalls(renderSlot)).toContainEqual(['anthropic', false, true, 'llm-pi-ai'])
|
||
})
|
||
|
||
it('skips the draft seat when a refresh drops the dormant row', async () => {
|
||
const { renderSlot, face, controller } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
const directory = [
|
||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true },
|
||
].map(({ active: _active, ...entry }) => entry)
|
||
face.llm.listConfigurableProviders.mockImplementation(() => Promise.resolve(remoteOk(directory)))
|
||
renderSlot.mockClear()
|
||
await act(async () => { await controller.load() })
|
||
// The draft card is still open while its row is gone from the directory.
|
||
expect(screen.getByLabelText(en.keyInput)).toBeTruthy()
|
||
expect(cardSeatCalls(renderSlot).some(([provider]) => provider === 'anthropic')).toBe(false)
|
||
})
|
||
it('renders the unkeyed whole-section provider as an open setup card in the first-run posture', async () => {
|
||
await mountFirstRun()
|
||
// Nothing is reachable yet, and DeepSeek has no configured credential and
|
||
// no stored apiKey → setup card.
|
||
expect(screen.getByText('DeepSeek')).toBeTruthy()
|
||
expect(screen.getByLabelText(en.keyInput)).toBeTruthy()
|
||
expect(screen.getByText('openai')).toBeTruthy()
|
||
expect(screen.queryByText('Active')).toBeNull()
|
||
expect(screen.queryByText('Inactive')).toBeNull()
|
||
expect(screen.getByText(en.add)).toBeTruthy()
|
||
})
|
||
|
||
it('leaves the unkeyed provider a plain row once another provider is usable', async () => {
|
||
await mountSection()
|
||
// openai's key is stored, so the user is not blocked and nothing on the
|
||
// page opens itself over them.
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
const configured = screen.getByRole('img', { name: en.credentialConfigured })
|
||
expect(configured.getAttribute('title')).toBe(en.credentialConfigured)
|
||
expect(configured.className).toContain('credentialDotConfigured')
|
||
expect(configured.closest('li')?.textContent).toContain('openai')
|
||
const missing = screen.getByRole('img', { name: en.credentialMissing })
|
||
expect(missing.closest('li')?.textContent).toContain('DeepSeek')
|
||
// The card is still one click away.
|
||
fireEvent.click(screen.getByRole('button', { name: deepSeekCopy(en.editProvider) }))
|
||
expect(screen.getByLabelText(en.keyInput)).toBeTruthy()
|
||
})
|
||
|
||
it('marks only a confirmed missing reference and leaves native or unavailable state unmarked', async () => {
|
||
const { face } = scriptedFace()
|
||
face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
||
)))
|
||
const controller = new ModelsSettingsStore(ctxWith(face), settingsSchema, new SettingsDescribeMirror(ctxWith(face)))
|
||
await controller.load()
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
|
||
const missing = screen.getByRole('img', { name: en.credentialMissing })
|
||
expect(missing.getAttribute('title')).toBe(en.credentialMissing)
|
||
expect(missing.className).toContain('credentialDotMissing')
|
||
expect(missing.closest('li')?.textContent).toContain('openai')
|
||
expect(screen.queryByRole('img', { name: en.credentialConfigured })).toBeNull()
|
||
expect(screen.getByText('zombie').closest('li')?.querySelector('[role="img"]')).toBeNull()
|
||
})
|
||
|
||
it('turns the setup card into a row once the credential reports configured', async () => {
|
||
const { face } = await mountFirstRun()
|
||
face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
|
||
)))
|
||
const controller = new ModelsSettingsStore(ctxWith(face), settingsSchema, new SettingsDescribeMirror(ctxWith(face)))
|
||
await controller.load()
|
||
cleanup()
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
// Now a row with an Edit button, not an open card.
|
||
expect(screen.getAllByText(en.edit).length).toBeGreaterThan(1)
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
})
|
||
|
||
it('decides setup need from the joined credential state and the first-run posture', () => {
|
||
const entry = { provider: 'p', displayName: 'p', settingsNs: 'llm-deepseek', settingsPath: [], active: true }
|
||
const row = (credential: ProviderRow['credential']): ProviderRow => ({
|
||
entry,
|
||
configured: true,
|
||
removable: false,
|
||
apiKeyEnv: 'X',
|
||
credential,
|
||
})
|
||
expect(needsSetup(row(undefined), false)).toBe(true)
|
||
expect(needsSetup(row({ configured: true, writable: true }), false)).toBe(false)
|
||
const nested = { ...row(undefined), entry: { ...entry, settingsPath: ['providers', 'x'] } }
|
||
expect(needsSetup(nested, false)).toBe(false)
|
||
// A user who can already reach some provider is not in the first-run
|
||
// posture, so nothing on the page opens itself.
|
||
expect(needsSetup(row(undefined), true)).toBe(false)
|
||
})
|
||
|
||
it('derives conventional credential references from route ids', () => {
|
||
expect(deriveKeyRef('anthropic')).toBe('ANTHROPIC_API_KEY')
|
||
expect(deriveKeyRef('minimax-cn')).toBe('MINIMAX_CN_API_KEY')
|
||
})
|
||
|
||
it('uses one stable provider identity in action copy', () => {
|
||
const target = { provider: 'deepseek-official', displayName: 'DeepSeek' }
|
||
expect(providerTargetLabel(target)).toBe('DeepSeek (deepseek-official)')
|
||
expect(providerCopy(en.deleteTitle, target)).toBe('Delete DeepSeek (deepseek-official)?')
|
||
expect(providerTargetLabel(OPENAI_TARGET)).toBe('openai')
|
||
})
|
||
|
||
it('names only changed fields instead of rebuilding the section', () => {
|
||
expect(pathOps(['providers', 'openai'], { baseURL: 'https://old', reasoning: 'high' }, { reasoning: 'high' }))
|
||
.toEqual([{ op: 'unset', path: ['providers', 'openai', 'baseURL'] }])
|
||
expect(pathOps([], { b: 1 }, { b: 2, d: 3 }))
|
||
.toEqual([{ op: 'set', path: ['b'], value: 2 }, { op: 'set', path: ['d'], value: 3 }])
|
||
expect(pathOps([], undefined, {})).toEqual([])
|
||
expect(pathOps([], { a: 1 }, { a: 1 })).toEqual([])
|
||
})
|
||
|
||
it('stores a typed key write-only from the setup card without touching settings', async () => {
|
||
const { set, mutate, face } = await mountFirstRun()
|
||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(key, { target: { value: ' sk-live ' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-live') })
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
// The saved key re-loads the join; the settings answer rides the shared
|
||
// mirror, so the reload shows as a directory read rather than a describe.
|
||
await waitFor(() => { expect(face.llm.listProviders.mock.calls.length).toBeGreaterThan(1) })
|
||
expect((await screen.findByRole('status')).textContent).toBe(
|
||
providerCopy(en.savedProvider, { provider: 'deepseek-official', displayName: 'DeepSeek' }),
|
||
)
|
||
fireEvent.click(screen.getByText(en.add))
|
||
expect(screen.queryByText(providerCopy(en.savedProvider, { provider: 'deepseek-official', displayName: 'DeepSeek' }))).toBeNull()
|
||
})
|
||
|
||
it('reuses the provider editor as a required credential-only onboarding form', async () => {
|
||
let finishSet: ((response: { ok: true; value: undefined }) => void) | undefined
|
||
const set = vi.fn(() => new Promise<{ ok: true; value: undefined }>((resolve) => {
|
||
finishSet = resolve
|
||
}))
|
||
const { face, mutate } = scriptedFace({ set })
|
||
const onClose = vi.fn()
|
||
const { ProviderEditor } = await import('../src/client/ProviderEditor.tsx')
|
||
|
||
render(<ProviderEditor
|
||
provider="deepseek-official"
|
||
displayName="DeepSeek"
|
||
hideTitle
|
||
namespace={wireNamespaces()[0]!}
|
||
schema={settingsSchema}
|
||
settingsPath={[]}
|
||
operations={operationsWith(face)}
|
||
t={t}
|
||
readOnly={false}
|
||
credentialOnly
|
||
credentialRequired
|
||
autoFocusCredential
|
||
cancelLabelKey="onboardingLater"
|
||
submitLabelKey="onboardingSave"
|
||
submitBusyLabelKey="onboardingSaving"
|
||
onClose={onClose}
|
||
/>)
|
||
|
||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
const save = screen.getByText<HTMLButtonElement>(en.onboardingSave)
|
||
expect(document.activeElement).toBe(key)
|
||
expect(key.required).toBe(true)
|
||
expect(save.disabled).toBe(true)
|
||
expect(screen.getByText(en.onboardingLater)).toBeTruthy()
|
||
expect(screen.queryByText(en.customized)).toBeNull()
|
||
expect(screen.queryByLabelText(en.baseUrl)).toBeNull()
|
||
|
||
fireEvent.change(key, { target: { value: ' ' } })
|
||
expect(screen.getByText(en.keyRequired)).toBeTruthy()
|
||
expect(key.getAttribute('aria-invalid')).toBe('true')
|
||
expect(save.disabled).toBe(true)
|
||
|
||
fireEvent.change(key, { target: { value: ' sk-onboarding ' } })
|
||
expect(screen.queryByText(en.keyRequired)).toBeNull()
|
||
expect(save.disabled).toBe(false)
|
||
fireEvent.click(save)
|
||
|
||
expect(await screen.findByText(en.onboardingSaving)).toBeTruthy()
|
||
expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-onboarding')
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
expect(onClose).not.toHaveBeenCalled()
|
||
|
||
if (finishSet === undefined) throw new Error('credential write did not start')
|
||
await act(async () => {
|
||
finishSet?.(remoteOk(undefined))
|
||
await Promise.resolve()
|
||
})
|
||
expect(onClose).toHaveBeenCalledWith(true)
|
||
})
|
||
|
||
it('applies customized deepseek fields as path ops', async () => {
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const baseURL = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
// The deepseek placeholder is pinned to the public endpoint, not the
|
||
// effective value (which may reflect a launch-environment override).
|
||
expect(baseURL.placeholder).toBe('https://api.deepseek.com/anthropic (Default to use DEEPSEEK_BASE_URL)')
|
||
fireEvent.change(baseURL, { target: { value: 'https://next2' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
// Only the field that actually changed: reasoningEffort was already
|
||
// 'high' in the loaded profile, so it produces no op.
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{ op: 'set', path: ['baseURL'], value: 'https://next2' }],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('materializes inherited models and adds an arbitrary DeepSeek id', async () => {
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
||
expect(screen.getAllByLabelText(new RegExp(en.modelId)).map(input => (input as HTMLInputElement).value))
|
||
.toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
|
||
|
||
fireEvent.click(screen.getByText(en.addModel))
|
||
const ids = screen.getAllByLabelText(new RegExp(en.modelId))
|
||
const names = screen.getAllByLabelText(new RegExp(en.modelName))
|
||
expandRow(3)
|
||
fireEvent.change(ids[2] as HTMLInputElement, { target: { value: 'private-preview' } })
|
||
fireEvent.change(names[2] as HTMLInputElement, { target: { value: 'Private Preview' } })
|
||
// Only row 3 is open, so its capacity is addressed by its own label.
|
||
fireEvent.change(screen.getByLabelText(`${en.contextWindow} 3`), { target: { value: '131072' } })
|
||
fireEvent.click(within(screen.getByRole('group', { name: `${en.modelInputTypes} 3` })).getByRole('checkbox', { name: en.modelInputImage }))
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{
|
||
op: 'set',
|
||
path: ['models'],
|
||
value: [
|
||
...DEFAULT_DEEPSEEK_MODELS,
|
||
{ id: 'private-preview', name: 'Private Preview', contextWindow: 131_072, inputModalities: ['text', 'image'] },
|
||
],
|
||
}],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('edits the DeepSeek endpoint and credential without a protocol selection', async () => {
|
||
const namespace: SettingsNamespaceView = {
|
||
...wireNamespaces()[0]!,
|
||
ns: 'llm-deepseek',
|
||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY', models: DEFAULT_DEEPSEEK_MODELS },
|
||
user: {},
|
||
}
|
||
const { face, mutate, set } = scriptedFace({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(namespace))),
|
||
})
|
||
const { ProviderEditor } = await import('../src/client/ProviderEditor.tsx')
|
||
render(<ProviderEditor
|
||
provider="deepseek-official"
|
||
displayName="DeepSeek"
|
||
namespace={namespace}
|
||
schema={settingsSchema}
|
||
settingsPath={[]}
|
||
operations={operationsWith(face)}
|
||
t={t}
|
||
readOnly={false}
|
||
onClose={vi.fn()}
|
||
/>)
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.baseUrl).placeholder)
|
||
.toBe('https://api.deepseek.com/anthropic (Default to use DEEPSEEK_BASE_URL)')
|
||
expect(screen.queryByLabelText(en.customApi)).toBeNull()
|
||
expect(screen.getByText(en.deepSeekEndpointHint)).toBeTruthy()
|
||
fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-messages-test' } })
|
||
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://messages.example/anthropic' } })
|
||
fireEvent.change(screen.getByLabelText(`${en.modelName} 1`), { target: { value: 'Messages Flash' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-messages-test') })
|
||
expect(mutate.mock.calls).toEqual([[
|
||
'llm-deepseek',
|
||
[
|
||
{ op: 'set', path: ['baseURL'], value: 'https://messages.example/anthropic' },
|
||
{ op: 'set', path: ['models'], value: [
|
||
{ ...DEFAULT_DEEPSEEK_MODELS[0], name: 'Messages Flash' },
|
||
DEFAULT_DEEPSEEK_MODELS[1],
|
||
] },
|
||
],
|
||
0,
|
||
]])
|
||
})
|
||
|
||
it('rejects duplicate DeepSeek model ids before writing', async () => {
|
||
const { mutate } = await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
fireEvent.click(screen.getByText(en.addModel))
|
||
const ids = screen.getAllByLabelText(new RegExp(en.modelId))
|
||
fireEvent.change(ids[2] as HTMLInputElement, { target: { value: 'deepseek-v4-flash' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
|
||
await screen.findByText(`Model 3: ${en.modelIdDuplicate}`)
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('validates every adapter-owned model catalog invariant', () => {
|
||
expect(modelDrafts(undefined)).toEqual([])
|
||
expect(modelDrafts([null, 'bad', { id: 'ok' }])).toEqual([{}, {}, { id: 'ok' }])
|
||
expect(validateDeepSeekModels([{}])).toEqual({ index: 0, key: 'modelIdRequired' })
|
||
expect(validateDeepSeekModels([{ id: 'same' }, { id: 'same' }]))
|
||
.toEqual({ index: 1, key: 'modelIdDuplicate' })
|
||
expect(validateDeepSeekModels([{ id: 'model', name: '' }]))
|
||
.toEqual({ index: 0, key: 'modelNameInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', contextWindow: null }]))
|
||
.toEqual({ index: 0, key: 'modelContextInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', contextWindow: 1.5 }]))
|
||
.toEqual({ index: 0, key: 'modelContextInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', contextWindow: 0 }]))
|
||
.toEqual({ index: 0, key: 'modelContextInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', contextWindow: 1 }])).toBeUndefined()
|
||
expect(validateDeepSeekModels([{ id: 'model', maxTokens: null }]))
|
||
.toEqual({ index: 0, key: 'modelMaxTokensInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', maxTokens: 1.5 }]))
|
||
.toEqual({ index: 0, key: 'modelMaxTokensInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', maxTokens: 0 }]))
|
||
.toEqual({ index: 0, key: 'modelMaxTokensInvalid' })
|
||
expect(validateDeepSeekModels([{ id: 'model', maxTokens: 8192 }])).toBeUndefined()
|
||
})
|
||
|
||
it('reads context windows written as counts, thousands, or millions', () => {
|
||
expect(parseCapacity('')).toBeUndefined()
|
||
expect(parseCapacity(' ')).toBeUndefined()
|
||
expect(parseCapacity('131072')).toBe(131_072)
|
||
expect(parseCapacity(' 256K ')).toBe(256_000)
|
||
expect(parseCapacity('256k')).toBe(256_000)
|
||
expect(parseCapacity('1M')).toBe(1_000_000)
|
||
expect(parseCapacity('1m')).toBe(1_000_000)
|
||
// 1M is 1000K, not 1024K: capacities are quoted in decimal.
|
||
expect(parseCapacity('1M')).toBe(parseCapacity('1000K'))
|
||
// 2.3 * 1e6 is a few ULPs high in binary floating point; an integral
|
||
// intent must not become a fractional count the validator rejects.
|
||
expect(parseCapacity('2.3M')).toBe(2_300_000)
|
||
expect(Number.isInteger(parseCapacity('1.5M'))).toBe(true)
|
||
// A genuinely fractional count survives as one, for the validator to reject.
|
||
expect(parseCapacity('0.0001K')).toBeCloseTo(0.1)
|
||
expect(parseCapacity('abc')).toBeNaN()
|
||
expect(parseCapacity('1G')).toBeNaN()
|
||
expect(parseCapacity('1M1')).toBeNaN()
|
||
})
|
||
|
||
it('spells a stored count in the shortest form that round-trips', () => {
|
||
expect(formatCapacity(1_000_000)).toBe('1M')
|
||
expect(formatCapacity(256_000)).toBe('256K')
|
||
expect(formatCapacity(1_500_000)).toBe('1500K')
|
||
expect(formatCapacity(131_072)).toBe('131072')
|
||
// Values the validator will reject are shown as-is rather than dressed up.
|
||
expect(formatCapacity(Number.NaN)).toBe('NaN')
|
||
expect(formatCapacity(0)).toBe('0')
|
||
for (const text of ['1M', '256K', '131072', '1500K']) {
|
||
expect(formatCapacity(parseCapacity(text) as number)).toBe(text)
|
||
}
|
||
})
|
||
|
||
it('accepts a suffixed context window and stores the plain count', async () => {
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expandRow(1)
|
||
expandRow(2)
|
||
const windows = capacityInputs(en.contextWindow)
|
||
// The inherited 1000000 reads back short.
|
||
expect((windows[0] as HTMLInputElement).value).toBe('1M')
|
||
|
||
// Keystrokes stay verbatim while the row has focus, so typing `1000` does
|
||
// not rewrite itself to `1K` mid-word.
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: '1000' } })
|
||
expect((windows[0] as HTMLInputElement).value).toBe('1000')
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: '1000K' } })
|
||
expect((windows[0] as HTMLInputElement).value).toBe('1000K')
|
||
// Blur settles the row to the canonical spelling of the same count.
|
||
fireEvent.blur(windows[0] as HTMLInputElement)
|
||
expect((windows[0] as HTMLInputElement).value).toBe('1M')
|
||
|
||
fireEvent.change(windows[1] as HTMLInputElement, { target: { value: '256K' } })
|
||
fireEvent.blur(windows[1] as HTMLInputElement)
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{
|
||
op: 'set',
|
||
path: ['models'],
|
||
value: [
|
||
{ ...DEFAULT_DEEPSEEK_MODELS[0], contextWindow: 1_000_000 },
|
||
{ ...DEFAULT_DEEPSEEK_MODELS[1], contextWindow: 256_000 },
|
||
],
|
||
}],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('keeps unreadable context-window text on screen and refuses the write', async () => {
|
||
const { mutate } = await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expandRow(1)
|
||
expandRow(2)
|
||
const windows = capacityInputs(en.contextWindow)
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: '1 gazillion' } })
|
||
// Blurring a row that is not the edited one leaves the buffer alone.
|
||
fireEvent.blur(windows[1] as HTMLInputElement)
|
||
fireEvent.blur(windows[0] as HTMLInputElement)
|
||
// The text the user typed is still there to correct.
|
||
expect((windows[0] as HTMLInputElement).value).toBe('1 gazillion')
|
||
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText(`Model 1: ${en.modelContextInvalid}`)
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it.each([
|
||
['the schema default', undefined],
|
||
['the composition entry', { models: [{ id: 'pinned-by-deployment' }] }],
|
||
])('restores %s the moment the override is dropped, not after a reload', async (_label, base) => {
|
||
// The regression: reset read the EFFECTIVE value, which still carries the
|
||
// stored override until the unset is applied — so the rows did not change
|
||
// and the catalog only looked restored after reopening the card.
|
||
const { face } = scriptedFace()
|
||
const stored = { models: [{ id: 'user-only-model', name: 'User Only' }] }
|
||
const overridden: SettingsNamespaceView = {
|
||
ns: 'llm-deepseek',
|
||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||
value: { ...stored, defaultContextWindow: 1_000_000 },
|
||
...base === undefined ? {} : { base },
|
||
user: stored,
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
}
|
||
const { ProviderEditor } = await import('../src/client/ProviderEditor.tsx')
|
||
render(<ProviderEditor
|
||
provider="deepseek-official"
|
||
displayName="DeepSeek"
|
||
namespace={overridden}
|
||
schema={settingsSchema}
|
||
settingsPath={[]}
|
||
operations={operationsWith(face)}
|
||
t={t}
|
||
readOnly={false}
|
||
onClose={() => {}}
|
||
/>)
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expect(screen.getByText(en.modelsCustomized)).toBeTruthy()
|
||
expect(screen.getAllByLabelText(new RegExp(en.modelId)).map(input => (input as HTMLInputElement).value))
|
||
.toEqual(['user-only-model'])
|
||
|
||
fireEvent.click(screen.getByText(en.resetModels))
|
||
|
||
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
||
expect(screen.getAllByLabelText(new RegExp(en.modelId)).map(input => (input as HTMLInputElement).value))
|
||
.toEqual(base === undefined ? ['deepseek-v4-flash', 'deepseek-v4-pro'] : ['pinned-by-deployment'])
|
||
})
|
||
|
||
it('keeps every row\'s unreadable text, not just the last one edited', async () => {
|
||
// The regression: one active buffer meant editing a second row displaced
|
||
// the first, which then fell back to rendering its stored NaN as `NaN` —
|
||
// losing the text the user was told they could still correct.
|
||
await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expandRow(1)
|
||
expandRow(2)
|
||
const windows = capacityInputs(en.contextWindow)
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: 'not a number' } })
|
||
fireEvent.blur(windows[0] as HTMLInputElement)
|
||
fireEvent.change(windows[1] as HTMLInputElement, { target: { value: '2M' } })
|
||
|
||
expect((windows[0] as HTMLInputElement).value).toBe('not a number')
|
||
expect((windows[1] as HTMLInputElement).value).toBe('2M')
|
||
})
|
||
|
||
it('re-keys the typed text around a removed row', async () => {
|
||
await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const windows = (): HTMLInputElement[] => capacityInputs(en.contextWindow)
|
||
const removeRow = (at: number): void => {
|
||
fireEvent.click(screen.getAllByLabelText(new RegExp(en.removeModel))[at] as HTMLElement)
|
||
}
|
||
// Three rows, with text parked on the outer two.
|
||
fireEvent.click(screen.getByText(en.addModel))
|
||
expandRow(1)
|
||
expandRow(2)
|
||
expandRow(3)
|
||
fireEvent.change(windows()[0] as HTMLInputElement, { target: { value: 'top text' } })
|
||
fireEvent.blur(windows()[0] as HTMLInputElement)
|
||
fireEvent.change(windows()[2] as HTMLInputElement, { target: { value: 'bottom text' } })
|
||
fireEvent.blur(windows()[2] as HTMLInputElement)
|
||
|
||
// Dropping the middle row leaves the row above untouched and carries the
|
||
// row below down with its own text, rather than stranding it.
|
||
removeRow(1)
|
||
expect(windows()).toHaveLength(2)
|
||
expect((windows()[0] as HTMLInputElement).value).toBe('top text')
|
||
expect((windows()[1] as HTMLInputElement).value).toBe('bottom text')
|
||
|
||
// Dropping a row that holds text takes that text with it; the survivor
|
||
// keeps its own rather than inheriting the deleted row's.
|
||
removeRow(0)
|
||
expect(windows()).toHaveLength(1)
|
||
expect((windows()[0] as HTMLInputElement).value).toBe('bottom text')
|
||
})
|
||
|
||
it('drops the typed text when reset replaces the rows it annotated', async () => {
|
||
// The regression: reset removed the override but left the buffer, so an
|
||
// inherited row displayed text no settings layer stores — and because an
|
||
// unreadable buffer never settles, it stayed there indefinitely.
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expandRow(1)
|
||
const windows = capacityInputs(en.contextWindow)
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: 'garbage' } })
|
||
fireEvent.blur(windows[0] as HTMLInputElement)
|
||
fireEvent.click(screen.getByText(en.resetModels))
|
||
|
||
// Reset collapses every row, so the restored capacity needs opening again.
|
||
expandRow(1)
|
||
const restored = capacityInputs(en.contextWindow)
|
||
expect((restored[0] as HTMLInputElement).value).toBe('1M')
|
||
|
||
// Reset put the draft back where it started, so Apply writes nothing at
|
||
// all rather than persisting whatever the stale text had parsed to.
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(screen.queryByText(en.apply)).toBeNull() })
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('edits an output cap per model and carries its text across a removal', async () => {
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expandRow(1)
|
||
expandRow(2)
|
||
// The profile's own cap is the placeholder both rows inherit.
|
||
expect(capacityInputs(en.maxTokens).map(input => input.placeholder)).toEqual(['256K', '256K'])
|
||
|
||
fireEvent.change(screen.getByLabelText(`${en.maxTokens} 2`), { target: { value: '64K' } })
|
||
fireEvent.blur(screen.getByLabelText(`${en.maxTokens} 2`))
|
||
expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 2`).value).toBe('64K')
|
||
|
||
// Dropping the row above carries the cap text down with its own row.
|
||
fireEvent.click(screen.getAllByLabelText(new RegExp(en.removeModel))[0] as HTMLElement)
|
||
expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 1`).value).toBe('64K')
|
||
// The disclosure closes on a second press.
|
||
expandRow(1)
|
||
expect(screen.queryByLabelText(`${en.maxTokens} 1`)).toBeNull()
|
||
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{
|
||
op: 'set',
|
||
path: ['models'],
|
||
value: [{ ...DEFAULT_DEEPSEEK_MODELS[1], maxTokens: 64_000 }],
|
||
}],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('settles a pasted id and refuses whitespace that would never match', async () => {
|
||
await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const ids = screen.getAllByLabelText<HTMLInputElement>(new RegExp(en.modelId))
|
||
fireEvent.change(ids[0] as HTMLInputElement, { target: { value: ' deepseek-v4-flash ' } })
|
||
fireEvent.blur(ids[0] as HTMLInputElement)
|
||
expect((ids[0] as HTMLInputElement).value).toBe('deepseek-v4-flash')
|
||
// A settled id needs no second trim.
|
||
fireEvent.blur(ids[0] as HTMLInputElement)
|
||
expect((ids[0] as HTMLInputElement).value).toBe('deepseek-v4-flash')
|
||
|
||
// An id that is only whitespace is as absent as an empty one, and a padded
|
||
// id is a duplicate of its trimmed twin.
|
||
expect(validateDeepSeekModels([{ id: ' ' }])).toEqual({ index: 0, key: 'modelIdRequired' })
|
||
expect(validateDeepSeekModels([{ id: 'model' }, { id: 'model ' }]))
|
||
.toEqual({ index: 1, key: 'modelIdDuplicate' })
|
||
})
|
||
|
||
it('renders malformed draft fallbacks without inventing catalog values', () => {
|
||
render(<DeepSeekModelsEditor
|
||
models={[{}]}
|
||
overridden={false}
|
||
defaultContextWindow={undefined}
|
||
defaultMaxTokens={undefined}
|
||
t={t}
|
||
disabled={true}
|
||
onChange={vi.fn()}
|
||
onReset={vi.fn()}
|
||
/>)
|
||
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelId} 1`).value).toBe('')
|
||
expandRow(1)
|
||
expect(screen.getByLabelText<HTMLInputElement>(`${en.contextWindow} 1`).placeholder)
|
||
.toBe(en.contextWindowPlaceholder)
|
||
expect(screen.getByLabelText<HTMLInputElement>(`${en.maxTokens} 1`).placeholder)
|
||
.toBe(en.maxTokensPlaceholder)
|
||
})
|
||
|
||
it('can empty and reset the model override, then clear optional fields without dropping hidden data', async () => {
|
||
const { mutate } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
fireEvent.click(screen.getAllByLabelText(new RegExp(en.removeModel))[0] as HTMLElement)
|
||
fireEvent.click(screen.getByLabelText(new RegExp(en.removeModel)))
|
||
expect(screen.getByText(en.modelsEmpty)).toBeTruthy()
|
||
fireEvent.click(screen.getByText(en.resetModels))
|
||
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
||
|
||
const names = screen.getAllByLabelText(new RegExp(en.modelName))
|
||
expandRow(1)
|
||
const windows = capacityInputs(en.contextWindow)
|
||
fireEvent.change(names[0] as HTMLInputElement, { target: { value: '' } })
|
||
fireEvent.change(windows[0] as HTMLInputElement, { target: { value: '' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{
|
||
op: 'set',
|
||
path: ['models'],
|
||
value: [
|
||
{ id: 'deepseek-v4-flash', description: 'Preserved hidden detail' },
|
||
DEFAULT_DEEPSEEK_MODELS[1],
|
||
],
|
||
}],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('clears an inherited override with an unset op, never a whole-section replace', async () => {
|
||
// A whole-section replace would clobber sibling overrides to clear one field.
|
||
const { mutate } = await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(url.value).toBe('https://base')
|
||
fireEvent.change(url, { target: { value: '' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
// This editor clears one field through an unset op so it cannot clobber
|
||
// sibling overrides with a whole-section replacement.
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-deepseek',
|
||
[{ op: 'unset', path: ['baseURL'] }],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('pins the deepseek placeholder and clears typed input back to inherited', async () => {
|
||
const { face } = scriptedFace()
|
||
const bare: SettingsNamespaceView = {
|
||
ns: 'llm-deepseek',
|
||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||
value: {},
|
||
autoGenerate: true, applies: 'live',
|
||
secrets: [],
|
||
revision: 0,
|
||
}
|
||
const { ProviderEditor } = await import('../src/client/ProviderEditor.tsx')
|
||
render(<ProviderEditor
|
||
provider="deepseek-official"
|
||
displayName="DeepSeek"
|
||
namespace={bare}
|
||
schema={settingsSchema}
|
||
settingsPath={[]}
|
||
operations={operationsWith(face)}
|
||
t={t}
|
||
readOnly={false}
|
||
onClose={() => {}}
|
||
/>)
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const baseURL = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(baseURL.placeholder).toBe('https://api.deepseek.com/anthropic (Default to use DEEPSEEK_BASE_URL)')
|
||
fireEvent.change(baseURL, { target: { value: 'https://x' } })
|
||
expect(baseURL.value).toBe('https://x')
|
||
fireEvent.change(baseURL, { target: { value: '' } })
|
||
expect(baseURL.value).toBe('')
|
||
})
|
||
|
||
it('saves credentials without changing the default model', async () => {
|
||
const { ctx, set } = await mountDeepSeekCard()
|
||
const initialize = vi.spyOn(ctx.remote.session, 'initializeDefaultModel')
|
||
fireEvent.change(await screen.findByLabelText(en.keyInput), { target: { value: 'test-key' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(screen.queryByText(en.apply)).toBeNull() })
|
||
expect(set).toHaveBeenCalledOnce()
|
||
expect(initialize).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('reveals only the current key draft and masks it again on change', async () => {
|
||
const { set } = await mountDeepSeekCard()
|
||
const input = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(input.type).toBe('password')
|
||
expect(input.value).toBe('')
|
||
expect(screen.queryByRole('button', { name: en.keyReveal })).toBeNull()
|
||
fireEvent.change(input, { target: { value: 'test-key' } })
|
||
const reveal = screen.getByRole('button', { name: en.keyReveal })
|
||
expect(reveal.getAttribute('aria-pressed')).toBe('false')
|
||
fireEvent.click(reveal)
|
||
expect(input.type).toBe('text')
|
||
const hide = screen.getByRole('button', { name: en.keyHide })
|
||
expect(hide.getAttribute('aria-pressed')).toBe('true')
|
||
fireEvent.click(hide)
|
||
expect(input.type).toBe('password')
|
||
fireEvent.click(screen.getByRole('button', { name: en.keyReveal }))
|
||
fireEvent.change(input, { target: { value: '' } })
|
||
expect(input.type).toBe('password')
|
||
expect(screen.queryByRole('button', { name: en.keyReveal })).toBeNull()
|
||
fireEvent.change(input, { target: { value: 'test-key' } })
|
||
expect(input.type).toBe('password')
|
||
expect(set).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('rejects an invalid draft before writing', async () => {
|
||
const { mutate } = await mountDeepSeekCard()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'not-a-url' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText(/baseURL/)
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('edits a pi-ai profile with the curated fields only', async () => {
|
||
const { mutate } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||
// The field names the profile's reference; a stored value stays hidden.
|
||
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(editorKey.placeholder).toBe(en.keyPlaceholderSaved)
|
||
// pi-ai carries Base URL too: the stored override shows as the value and
|
||
// the effective profile endpoint, ahead of the catalog's, as its placeholder.
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(url.value).toBe('https://proxy')
|
||
expect(url.placeholder).toBe('https://proxy')
|
||
fireEvent.change(url, { target: { value: 'https://proxy/v2' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
// Only the edited field travels: apiKeyEnv and headers were already stored
|
||
// with these values, so no op restates them.
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-pi-ai',
|
||
[{ op: 'set', path: ['providers', 'openai', 'baseURL'], value: 'https://proxy/v2' }],
|
||
0,
|
||
])
|
||
})
|
||
|
||
it('adds a dormant provider with a derived reference and stores its key', async () => {
|
||
const { mutate, set } = await mountSection()
|
||
fireEvent.click(screen.getByText(en.add))
|
||
const pick = await screen.findByLabelText<HTMLSelectElement>(en.provider)
|
||
expect([...pick.options].map(option => option.value)).toEqual(['anthropic', 'broken', 'plain'])
|
||
expect(pick.value).toBe('anthropic')
|
||
// A dormant route shows its catalog endpoint and the first variable its
|
||
// provider reads; a typed key still stores under the derived reference.
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.baseUrl).placeholder).toBe('https://api.anthropic.com')
|
||
const addKey = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(addKey.placeholder).toBe(en.keyPlaceholderFallback.replace('{hint}', 'ANTHROPIC_AUTH_TOKEN'))
|
||
fireEvent.change(addKey, { target: { value: 'sk-ant' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-pi-ai',
|
||
[{ op: 'set', path: ['providers', 'anthropic', 'apiKeyEnv'], value: 'ANTHROPIC_API_KEY' }],
|
||
0,
|
||
])
|
||
await waitFor(() => { expect(set).toHaveBeenCalledWith('ANTHROPIC_API_KEY', 'sk-ant') })
|
||
})
|
||
|
||
it('keeps pi-ai provider-native authentication when no key is entered', async () => {
|
||
const { mutate, set } = await mountSection()
|
||
fireEvent.click(screen.getByText(en.add))
|
||
await screen.findByLabelText(en.provider)
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-pi-ai',
|
||
[{ op: 'set', path: ['providers', 'anthropic'], value: {} }],
|
||
0,
|
||
])
|
||
expect(set).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('retries only the credential after refreshed settings already committed', async () => {
|
||
const committed = wireNamespaces()[2]!
|
||
const afterSettings: SettingsNamespaceView = {
|
||
...committed,
|
||
value: { providers: {
|
||
...(committed.value as { providers: object }).providers,
|
||
anthropic: { apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
||
} },
|
||
user: { providers: {
|
||
...(committed.user as { providers: object }).providers,
|
||
anthropic: { apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
||
} },
|
||
revision: 1,
|
||
}
|
||
const mutate = vi.fn(() => Promise.resolve(remoteOk(afterSettings)))
|
||
const set = vi.fn()
|
||
.mockResolvedValueOnce(remoteFail('credential store unavailable'))
|
||
.mockResolvedValueOnce(remoteOk(undefined))
|
||
const { face, controller, mirror } = await mountSection({ mutate, set })
|
||
fireEvent.click(screen.getByText(en.add))
|
||
await screen.findByLabelText(en.provider)
|
||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.keyInput), { target: { value: 'sk-ant' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText('credential store unavailable')
|
||
expect(mutate).toHaveBeenCalledOnce()
|
||
face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true,
|
||
hasDocument: false,
|
||
namespaces: wireNamespaces().map(namespace => namespace.ns === 'llm-pi-ai' ? afterSettings : namespace),
|
||
}))
|
||
// The refreshed settings answer reaches the page through the mirror's own
|
||
// refresh (the document commit's invalidation in production).
|
||
await act(async () => {
|
||
await mirror.load()
|
||
await controller.load()
|
||
})
|
||
expect(controller.store.getSnapshot().namespaces.get('llm-pi-ai')?.revision).toBe(1)
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(set).toHaveBeenCalledTimes(2) })
|
||
expect(mutate).toHaveBeenCalledOnce()
|
||
expect(set).toHaveBeenLastCalledWith('ANTHROPIC_API_KEY', 'sk-ant')
|
||
})
|
||
|
||
it('switches the add card target and degrades unknown or broken targets loudly', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByText(en.add))
|
||
const pick = await screen.findByLabelText<HTMLSelectElement>(en.provider)
|
||
fireEvent.change(pick, { target: { value: 'broken' } })
|
||
await screen.findByText(/unresolvable settings path/)
|
||
fireEvent.change(pick, { target: { value: 'plain' } })
|
||
await waitFor(() => {
|
||
expect(screen.getAllByText(content => content.includes(en.advancedHint)).length).toBeGreaterThan(0)
|
||
})
|
||
// The hint-only card cannot apply anything, and offers no key field.
|
||
expect(screen.getByText<HTMLButtonElement>(en.apply).disabled).toBe(true)
|
||
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
||
})
|
||
|
||
it('surfaces a rejected settings write and never stores the key after it', async () => {
|
||
const { set } = await mountSection({
|
||
mutate: vi.fn(() => Promise.resolve(remoteFail('llm-pi-ai: unknown pi-ai provider "bogus"', 'settings/rejected'))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.add))
|
||
await screen.findByLabelText(en.provider)
|
||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.keyInput), { target: { value: 'sk-x' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText(/unknown pi-ai provider/)
|
||
expect(set).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('renders the card without the stored-key hint when the credential probe is refused', async () => {
|
||
const { face } = scriptedFace()
|
||
face.credentials.describe = vi.fn(() => Promise.resolve(remoteFail('no credential provider')))
|
||
const controller = new ModelsSettingsStore(ctxWith(face), settingsSchema, new SettingsDescribeMirror(ctxWith(face)))
|
||
await controller.load()
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(key.placeholder).toBe(en.keyPlaceholderFallback.replace('{hint}', 'DEEPSEEK_API_KEY'))
|
||
})
|
||
|
||
it('tells the user to reopen when another writer moved the namespace first', async () => {
|
||
// The stale-draft overwrite: two tabs open the same card, the other saves,
|
||
// and this one must be refused rather than replay its opening snapshot.
|
||
const { set } = await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteFail('changed since it was read', 'settings/conflict'))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.baseUrl), { target: { value: 'https://mine' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText(en.conflict)
|
||
expect(set).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('keeps the card usable after a refused write', async () => {
|
||
await mountDeepSeekCard({
|
||
mutate: vi.fn(() => Promise.resolve(remoteFail('the host refused', 'settings/rejected'))),
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.baseUrl), { target: { value: 'https://next' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText('the host refused')
|
||
// Not stuck in `applying…`: the finally cleared busy, so Apply is live again.
|
||
expect(screen.getByText(en.apply)).toBeTruthy()
|
||
})
|
||
|
||
it('surfaces a refused credential write on the card', async () => {
|
||
await mountFirstRun({
|
||
set: vi.fn(() => Promise.resolve(remoteFail(
|
||
'credentials-local: invalid document at /home/user/.dsh/.credentials.yaml: BAD_INDENT at line 3, column 1',
|
||
))),
|
||
})
|
||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
fireEvent.change(key, { target: { value: 'sk-live' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await screen.findByText(/invalid document at \/home\/user\/\.dsh\/\.credentials\.yaml/)
|
||
expect(screen.queryByRole('status')).toBeNull()
|
||
})
|
||
|
||
it('keeps a failed credential describe silent and the input usable', async () => {
|
||
const { face, set } = await mountSection()
|
||
face.credentials.describe.mockImplementation(() => Promise.resolve(remoteFail('down', 'gateway/internal')))
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(editorKey.placeholder).toBe(en.keyPlaceholderSaved)
|
||
fireEvent.change(editorKey, { target: { value: 'sk-live' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(set).toHaveBeenCalledTimes(1) })
|
||
})
|
||
|
||
it('requires confirmation before removing a user-added provider', async () => {
|
||
const { mutate, unset } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||
expect(dialog.textContent).toContain(openaiCopy(en.deleteDescriptionWithCredential))
|
||
expect(document.activeElement).toBe(within(dialog).getByRole('button', { name: en.cancel }))
|
||
expect(unset).not.toHaveBeenCalled()
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
fireEvent.click(within(dialog).getByRole('button', { name: en.cancel }))
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||
fireEvent.click(within(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) }))
|
||
.getByRole('button', { name: en.close }))
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||
fireEvent.click(within(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) }))
|
||
.getByRole('button', { name: openaiCopy(en.deleteConfirm) }))
|
||
await waitFor(() => { expect(unset).toHaveBeenCalledWith('OPENAI_API_KEY') })
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||
expect(unset.mock.invocationCallOrder[0]).toBeLessThan(mutate.mock.invocationCallOrder[0] as number)
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-pi-ai',
|
||
[{ op: 'unset', path: ['providers', 'openai'] }],
|
||
undefined,
|
||
])
|
||
})
|
||
|
||
it('blocks duplicate deletion while the confirmed removal is pending', async () => {
|
||
let resolveRemoval!: (response: { ok: true; value: SettingsNamespaceView }) => void
|
||
const mutate = vi.fn(() => new Promise<{ ok: true; value: SettingsNamespaceView }>((resolve) => {
|
||
resolveRemoval = resolve
|
||
}))
|
||
await mountSection({ mutate })
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||
const confirm = within(dialog).getByRole<HTMLButtonElement>('button', { name: openaiCopy(en.deleteConfirm) })
|
||
fireEvent.click(confirm)
|
||
fireEvent.click(confirm)
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
||
expect(confirm.disabled).toBe(true)
|
||
expect(within(dialog).getByRole<HTMLButtonElement>('button', { name: en.cancel }).disabled).toBe(true)
|
||
expect(within(dialog).getByRole('button', { name: openaiCopy(en.deleting) })).toBe(confirm)
|
||
fireEvent.click(within(dialog).getByRole('button', { name: en.close }))
|
||
expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
||
expect(mutate).toHaveBeenCalledOnce()
|
||
await act(async () => { resolveRemoval(remoteOk(wireNamespaces()[2]!)) })
|
||
await waitFor(() => {
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
})
|
||
})
|
||
|
||
it('renders the load failure with a retry control', async () => {
|
||
const face = scriptedFace()
|
||
face.face.llm.listProviders.mockResolvedValue(remoteFail('directory down', 'gateway/internal'))
|
||
const controller = new ModelsSettingsStore(
|
||
ctxWith(face.face), settingsSchema, new SettingsDescribeMirror(ctxWith(face.face)))
|
||
await controller.load()
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face.face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
expect(screen.getByText(/directory down/)).toBeTruthy()
|
||
fireEvent.click(screen.getByText(en.retry))
|
||
await waitFor(() => { expect(screen.queryByText(/directory down/)).toBeNull() })
|
||
})
|
||
|
||
it('shows the read-only notice and disables mutations for a read-only provider', async () => {
|
||
const { face } = await mountSection()
|
||
face.settings.describe.mockImplementation(() => Promise.resolve(remoteOk({
|
||
writable: false,
|
||
hasDocument: false,
|
||
namespaces: wireNamespaces(),
|
||
})))
|
||
const controller = new ModelsSettingsStore(ctxWith(face), settingsSchema, new SettingsDescribeMirror(ctxWith(face)))
|
||
await controller.load()
|
||
cleanup()
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
expect(screen.getByText(en.readOnly)).toBeTruthy()
|
||
expect(screen.getAllByText<HTMLButtonElement>(en.remove).every(button => button.disabled)).toBe(true)
|
||
expect(screen.getByText<HTMLButtonElement>(en.add).disabled).toBe(true)
|
||
})
|
||
|
||
it('toggles the row editor closed on a second edit click and on cancel', async () => {
|
||
const { mutate } = await mountSection()
|
||
const edit = screen.getByRole('button', { name: openaiCopy(en.editProvider) })
|
||
fireEvent.click(edit)
|
||
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
||
fireEvent.click(edit)
|
||
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
||
fireEvent.click(edit)
|
||
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
||
fireEvent.click(screen.getByText(en.cancel))
|
||
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('opens the add card on the catalog mode with both modes offered', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
const modes = screen.getByRole('tablist', { name: en.addMode })
|
||
expect(within(modes).getByRole('tab', { name: en.addCatalog }).getAttribute('aria-selected')).toBe('true')
|
||
expect(within(modes).getByRole('tab', { name: en.addCustom }).getAttribute('aria-selected')).toBe('false')
|
||
expect(screen.getByText(en.addCatalogHint)).toBeTruthy()
|
||
expect(screen.queryByText(en.addCustomHint)).toBeNull()
|
||
expect(screen.getByRole('combobox', { name: en.provider })).toBeTruthy()
|
||
// The custom panel is mounted but hidden, so its fields are not reachable.
|
||
expect(screen.queryByRole('textbox', { name: en.customRoute })).toBeNull()
|
||
})
|
||
|
||
it('switches between the modes without losing either draft', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-catalog' } })
|
||
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
expect(screen.getByText(en.addCustomHint)).toBeTruthy()
|
||
expect(screen.queryByText(en.addCatalogHint)).toBeNull()
|
||
expect(screen.queryByRole('combobox', { name: en.provider })).toBeNull()
|
||
fireEvent.change(screen.getByRole('textbox', { name: en.customRoute }), { target: { value: 'acme' } })
|
||
|
||
// Both panels stay mounted from here on, so the shown one is queried by name.
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCatalog }))
|
||
const catalog = screen.getByRole('tabpanel', { name: en.addCatalog })
|
||
expect(within(catalog).getByLabelText<HTMLInputElement>(en.keyInput).value).toBe('sk-catalog')
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
expect(screen.getByRole<HTMLInputElement>('textbox', { name: en.customRoute }).value).toBe('acme')
|
||
})
|
||
|
||
it('offers the same key reveal control on a custom provider', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
const panel = within(screen.getByRole('tabpanel', { name: en.addCustom }))
|
||
const input = panel.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(panel.queryByRole('button', { name: en.keyReveal })).toBeNull()
|
||
fireEvent.change(input, { target: { value: 'custom-test-key' } })
|
||
fireEvent.click(panel.getByRole('button', { name: en.keyReveal }))
|
||
expect(input.type).toBe('text')
|
||
fireEvent.click(panel.getByRole('button', { name: en.keyHide }))
|
||
expect(input.type).toBe('password')
|
||
})
|
||
|
||
it('opens on the custom mode when every catalog provider is already configured', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
|
||
]))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
const catalog = screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog })
|
||
expect(catalog.disabled).toBe(true)
|
||
expect(screen.getByRole('tab', { name: en.addCustom }).getAttribute('aria-selected')).toBe('true')
|
||
expect(screen.getByRole('textbox', { name: en.customRoute })).toBeTruthy()
|
||
})
|
||
|
||
it('shows the custom form alone when no directory row can be adopted', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([]))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.queryByRole('combobox', { name: en.provider })).toBeNull()
|
||
expect(screen.getByRole('textbox', { name: en.customRoute })).toBeTruthy()
|
||
})
|
||
|
||
it('disables the add action when neither mode can proceed', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
|
||
]))
|
||
// A pi-ai schema naming no protocol leaves nothing to declare either.
|
||
const protocolless = JSON.parse(JSON.stringify(
|
||
Schema.object({ providers: Schema.dict(Schema.object({})) }).toJSON(),
|
||
)) as JsonValue
|
||
scripted.face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true, hasDocument: false,
|
||
namespaces: wireNamespaces().map(view => view.ns === 'llm-pi-ai' ? { ...view, schema: protocolless } : view),
|
||
}))
|
||
await mountFace(scripted)
|
||
expect(screen.getByRole<HTMLButtonElement>('button', { name: en.add }).disabled).toBe(true)
|
||
})
|
||
|
||
it('shows the custom panel when a refresh drops every configurable row mid-card', async () => {
|
||
const { face, controller } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.getByRole('combobox', { name: en.provider })).toBeTruthy()
|
||
// The directory empties while the card is open: the only mode left is the
|
||
// custom one, whose panel was never visited.
|
||
face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([]))
|
||
await act(async () => { await controller.load() })
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.getByRole('textbox', { name: en.customRoute })).toBeTruthy()
|
||
})
|
||
|
||
it('picks a catalog target when the catalog becomes addable after the card opened', async () => {
|
||
const scripted = scriptedFace()
|
||
const exhausted = [
|
||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
|
||
]
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk(exhausted))
|
||
const { controller } = await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog }).disabled).toBe(true)
|
||
// A dormant route appears (another client deleted its profile, say).
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
...exhausted,
|
||
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'] },
|
||
]))
|
||
await act(async () => { await controller.load() })
|
||
const catalog = screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog })
|
||
expect(catalog.disabled).toBe(false)
|
||
fireEvent.click(catalog)
|
||
expect(screen.getByRole<HTMLSelectElement>('combobox', { name: en.provider }).value).toBe('anthropic')
|
||
expect(within(screen.getByRole('tabpanel', { name: en.addCatalog })).getByLabelText(en.keyInput)).toBeTruthy()
|
||
})
|
||
|
||
it('drops the custom panel when the pi-ai namespace disappears mid-card', async () => {
|
||
const { face, controller, mirror } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
expect(screen.getByRole('textbox', { name: en.customRoute })).toBeTruthy()
|
||
face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true, hasDocument: false,
|
||
namespaces: wireNamespaces().filter(view => view.ns !== 'llm-pi-ai'),
|
||
}))
|
||
// A namespace change reaches the page through the mirror's own refresh.
|
||
await act(async () => {
|
||
await mirror.load()
|
||
await controller.load()
|
||
})
|
||
// Nothing can be declared any more, so the form is gone rather than left
|
||
// to create a route the Host would refuse; the catalog form stands alone.
|
||
expect(screen.queryByRole('textbox', { name: en.customRoute })).toBeNull()
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.getByRole('combobox', { name: en.provider })).toBeTruthy()
|
||
})
|
||
|
||
it('forgets the catalog target when the custom form closes, so a later refresh opens no row editor', async () => {
|
||
const { face, controller, mirror } = await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.getByRole<HTMLSelectElement>('combobox', { name: en.provider }).value).toBe('anthropic')
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
fireEvent.click(within(screen.getByRole('tabpanel', { name: en.addCustom })).getByText(en.cancel))
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
// The draft's provider gets configured elsewhere: its row must arrive closed.
|
||
const withAnthropic = (layer: unknown): JsonValue => ({
|
||
providers: { ...(layer as { providers: object }).providers, anthropic: { apiKeyEnv: 'ANTHROPIC_API_KEY' } },
|
||
})
|
||
face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true, hasDocument: false,
|
||
namespaces: wireNamespaces().map(view => view.ns === 'llm-pi-ai'
|
||
? { ...view, value: withAnthropic(view.value), user: withAnthropic(view.user) }
|
||
: view),
|
||
}))
|
||
await act(async () => {
|
||
await mirror.load()
|
||
await controller.load()
|
||
})
|
||
expect(screen.getByRole('button', { name: providerCopy(en.editProvider, { provider: 'anthropic', displayName: 'anthropic' }) })).toBeTruthy()
|
||
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
||
})
|
||
|
||
it('locks the mode switch while the catalog form has a write in flight', async () => {
|
||
let settle: (() => void) | undefined
|
||
const pending = new Promise<void>((resolve) => { settle = resolve })
|
||
const providerNamespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
||
const mutate = vi.fn(() => pending.then(() => remoteOk(providerNamespace)))
|
||
await mountSection({ mutate })
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-slow' } })
|
||
fireEvent.click(screen.getByText(en.apply))
|
||
await waitFor(() => { expect(screen.getByRole<HTMLButtonElement>('tab', { name: en.addCustom }).disabled).toBe(true) })
|
||
expect(screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog }).disabled).toBe(true)
|
||
await act(async () => { settle!(); await pending })
|
||
// The apply closes the card once it lands; nothing was switched underneath it.
|
||
await waitFor(() => { expect(screen.queryByRole('tablist')).toBeNull() })
|
||
expect(mutate).toHaveBeenCalledOnce()
|
||
})
|
||
|
||
it('locks the mode switch while the custom form is asking the endpoint for models', async () => {
|
||
const scripted = scriptedFace()
|
||
let settle: (() => void) | undefined
|
||
const pending = new Promise<void>((resolve) => { settle = resolve })
|
||
scripted.face.llm.discoverModels.mockImplementation(() => pending.then(() => remoteOk([])))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
const custom = within(screen.getByRole('tabpanel', { name: en.addCustom }))
|
||
fireEvent.change(custom.getByRole('textbox', { name: en.baseUrl }), { target: { value: 'https://acme.test/v1' } })
|
||
fireEvent.click(custom.getByRole('button', { name: en.fetchModels }))
|
||
await waitFor(() => { expect(screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog }).disabled).toBe(true) })
|
||
await act(async () => { settle!(); await pending })
|
||
await waitFor(() => { expect(screen.getByRole<HTMLButtonElement>('tab', { name: en.addCatalog }).disabled).toBe(false) })
|
||
})
|
||
|
||
it('pairs each tab with its panel through ids', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
const catalogTab = screen.getByRole('tab', { name: en.addCatalog })
|
||
const catalogPanel = screen.getByRole('tabpanel', { name: en.addCatalog })
|
||
expect(catalogTab.getAttribute('aria-controls')).toBe(catalogPanel.id)
|
||
expect(catalogPanel.getAttribute('aria-labelledby')).toBe(catalogTab.id)
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
const customTab = screen.getByRole('tab', { name: en.addCustom })
|
||
const customPanel = screen.getByRole('tabpanel', { name: en.addCustom })
|
||
expect(customTab.getAttribute('aria-controls')).toBe(customPanel.id)
|
||
expect(customPanel.getAttribute('aria-labelledby')).toBe(customTab.id)
|
||
})
|
||
|
||
it('titles a single-mode card with its mode instead of leaving an orphan tabpanel', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([]))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.queryByRole('tabpanel')).toBeNull()
|
||
expect(screen.getByText(en.addCustom)).toBeTruthy()
|
||
expect(screen.getByRole('textbox', { name: en.customRoute })).toBeTruthy()
|
||
})
|
||
|
||
it('explains a locked mode through its hover title', async () => {
|
||
const scripted = scriptedFace()
|
||
scripted.face.llm.listConfigurableProviders.mockResolvedValue(remoteOk([
|
||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
|
||
]))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.getByRole('tab', { name: en.addCatalog }).getAttribute('title')).toBe(en.addCatalogExhausted)
|
||
expect(screen.getByRole('tab', { name: en.addCustom }).getAttribute('title')).toBeNull()
|
||
})
|
||
|
||
it('explains a mode with no protocol to declare through its hover title', async () => {
|
||
const scripted = scriptedFace()
|
||
const protocolless = JSON.parse(JSON.stringify(
|
||
Schema.object({ providers: Schema.dict(Schema.object({})) }).toJSON(),
|
||
)) as JsonValue
|
||
scripted.face.settings.describe.mockResolvedValue(remoteOk({
|
||
writable: true, hasDocument: false,
|
||
namespaces: wireNamespaces().map(view => view.ns === 'llm-pi-ai' ? { ...view, schema: protocolless } : view),
|
||
}))
|
||
await mountFace(scripted)
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
expect(screen.getByRole('tab', { name: en.addCustom }).getAttribute('title')).toBe(en.addCustomUnavailable)
|
||
expect(screen.getByRole('tab', { name: en.addCatalog }).getAttribute('title')).toBeNull()
|
||
})
|
||
|
||
it('collapses the add card from the custom mode on cancel', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||
fireEvent.click(screen.getByRole('tab', { name: en.addCustom }))
|
||
fireEvent.click(within(screen.getByRole('tabpanel', { name: en.addCustom })).getByText(en.cancel))
|
||
expect(screen.queryByRole('tablist')).toBeNull()
|
||
expect(screen.getByRole('button', { name: en.add })).toBeTruthy()
|
||
})
|
||
|
||
it('cancels the add card back to the add button', async () => {
|
||
await mountSection()
|
||
fireEvent.click(screen.getByText(en.add))
|
||
await screen.findByLabelText(en.provider)
|
||
fireEvent.click(screen.getByText(en.cancel))
|
||
await screen.findByText(en.add)
|
||
expect(screen.queryByLabelText(en.provider)).toBeNull()
|
||
})
|
||
|
||
it('collapses the setup card on cancel without disturbing another open card', async () => {
|
||
// The regression: the setup card shared the row/add/declare close handler,
|
||
// so cancelling it discarded the add card's draft while staying open itself.
|
||
await mountFirstRun()
|
||
expect(screen.getAllByLabelText(en.keyInput)).toHaveLength(1)
|
||
fireEvent.click(screen.getByText(en.add))
|
||
await screen.findByLabelText(en.provider)
|
||
expect(screen.getAllByLabelText(en.keyInput)).toHaveLength(2)
|
||
|
||
// The setup card is the first one on the page, above the add block.
|
||
fireEvent.click(screen.getAllByText(en.cancel)[0] as HTMLElement)
|
||
// The add card kept its draft…
|
||
expect(screen.getByLabelText(en.provider)).toBeTruthy()
|
||
// …and DeepSeek collapsed to an ordinary row carrying the missing-key dot.
|
||
expect(screen.getAllByLabelText(en.keyInput)).toHaveLength(1)
|
||
expect(screen.getAllByRole('img', { name: en.credentialMissing })
|
||
.some(dot => dot.closest('li')?.textContent?.includes('DeepSeek') === true)).toBe(true)
|
||
// Its card reopens through Edit, which closes the add card as any row does.
|
||
fireEvent.click(screen.getByRole('button', { name: deepSeekCopy(en.editProvider) }))
|
||
expect(screen.getAllByLabelText(en.keyInput)).toHaveLength(1)
|
||
expect(screen.queryByLabelText(en.provider)).toBeNull()
|
||
})
|
||
|
||
it('loads on first render of an idle controller', async () => {
|
||
const { face } = scriptedFace()
|
||
const controller = new ModelsSettingsStore(ctxWith(face), settingsSchema, new SettingsDescribeMirror(ctxWith(face)))
|
||
render(<ModelsSection
|
||
controller={controller}
|
||
useSnapshot={bindSnapshotSelector(controller.store)}
|
||
operations={operationsWith(face)}
|
||
schema={settingsSchema}
|
||
t={t}
|
||
renderSlot={() => null}
|
||
/>)
|
||
await screen.findByText('DeepSeek')
|
||
})
|
||
|
||
it('removes by unsetting the profile path, never by rebuilding the section', async () => {
|
||
// The page only needs to name the profile path; rebuilding the section
|
||
// would widen the write for no benefit.
|
||
const { face, mutate, controller } = await mountSection()
|
||
await removeProviderProfile(
|
||
operationsWith(face),
|
||
controller,
|
||
{ settingsNs: 'llm-plain', settingsPath: ['ghost-profile'] },
|
||
)
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-plain',
|
||
[{ op: 'unset', path: ['ghost-profile'] }],
|
||
undefined,
|
||
])
|
||
})
|
||
|
||
it('keeps the snapshot untouched and reports the message when a removal write is refused', async () => {
|
||
const { face, controller } = await mountSection({
|
||
mutate: vi.fn(() => Promise.resolve(remoteFail('read-only', 'settings/rejected'))),
|
||
})
|
||
const before = controller.store.getSnapshot().rows
|
||
const failure = await removeProviderProfile(
|
||
operationsWith(face),
|
||
controller,
|
||
{ settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
|
||
)
|
||
expect(failure).toBe('read-only')
|
||
expect(controller.store.getSnapshot().rows).toBe(before)
|
||
})
|
||
|
||
it('keeps a failed identified deletion recoverable in its confirmation dialog', async () => {
|
||
const mutate = vi.fn()
|
||
.mockResolvedValueOnce(remoteFail('the host refused', 'settings/rejected'))
|
||
.mockResolvedValueOnce(remoteOk(wireNamespaces()[2]!))
|
||
const { unset } = await mountSection({ mutate })
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||
const confirm = within(dialog).getByRole('button', { name: openaiCopy(en.deleteConfirm) })
|
||
fireEvent.click(confirm)
|
||
await within(dialog).findByText('the host refused')
|
||
expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
||
expect(unset).toHaveBeenCalledOnce()
|
||
expect(mutate).toHaveBeenCalledOnce()
|
||
|
||
fireEvent.click(confirm)
|
||
await waitFor(() => { expect(unset).toHaveBeenCalledTimes(2) })
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(2) })
|
||
await waitFor(() => {
|
||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||
})
|
||
})
|
||
|
||
it('retains credentials that are not identified as page-managed', async () => {
|
||
const { unset, mutate } = await mountSection()
|
||
const target = { provider: 'zombie', displayName: 'zombie' }
|
||
fireEvent.click(screen.getByRole('button', { name: providerCopy(en.removeProvider, target) }))
|
||
const dialog = screen.getByRole('dialog', { name: providerCopy(en.deleteTitle, target) })
|
||
expect(dialog.textContent).toContain(providerCopy(en.deleteDescription, target))
|
||
fireEvent.click(within(dialog).getByRole('button', { name: providerCopy(en.deleteConfirm, target) }))
|
||
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
||
expect(unset).not.toHaveBeenCalled()
|
||
expect(mutate.mock.calls[0]).toEqual([
|
||
'llm-pi-ai',
|
||
[{ op: 'unset', path: ['providers', 'zombie'] }],
|
||
undefined,
|
||
])
|
||
})
|
||
|
||
it('does not remove provider settings when its managed credential removal is refused', async () => {
|
||
const { face, controller, mutate } = await mountSection({
|
||
unset: vi.fn(() => Promise.resolve(remoteFail('credential is read-only'))),
|
||
})
|
||
const failure = await removeProviderProfile(
|
||
operationsWith(face),
|
||
controller,
|
||
{
|
||
settingsNs: 'llm-pi-ai',
|
||
settingsPath: ['providers', 'openai'],
|
||
credentialRef: 'OPENAI_API_KEY',
|
||
},
|
||
)
|
||
expect(failure).toBe('credential is read-only')
|
||
expect(mutate).not.toHaveBeenCalled()
|
||
})
|
||
|
||
})
|
||
|
||
describe('apiKeyFailure', () => {
|
||
it('treats a blank field as no failure — it means keep the stored key', () => {
|
||
expect(apiKeyFailure('')).toBeUndefined()
|
||
})
|
||
|
||
it.each([
|
||
['a printable-ASCII key', 'sk-0123456789'],
|
||
['a padded key, which the caller trims', ' sk-abc '],
|
||
['the printable-ASCII boundary characters', '!~'],
|
||
['a hyphenated key carrying an equals sign', 'sk-ABC=xyz'],
|
||
['an all-upper-case key ending in base64 padding', 'ABCD=='],
|
||
['an all-upper-case key ending in one padding character', 'MNOPQRST='],
|
||
])('accepts %s', (_label, draft) => {
|
||
expect(apiKeyFailure(draft)).toBeUndefined()
|
||
})
|
||
|
||
it.each([
|
||
['spaces', ' '],
|
||
['a tab', '\t'],
|
||
])('fails a field holding only %s instead of silently dropping it', (_label, draft) => {
|
||
expect(apiKeyFailure(draft)).toBe('keyBlank')
|
||
})
|
||
|
||
it.each([
|
||
['an emoji', 'sk-\u{1F600}'],
|
||
['CJK text', 'sk-你好'],
|
||
['full-width punctuation', 'sk-abc,'],
|
||
['an interior space', 'sk-abc def'],
|
||
['a C0 control character', 'sk-abc\x01'],
|
||
['a latin-1 character', 'sk-café'],
|
||
])('fails %s as illegal characters', (_label, draft) => {
|
||
expect(apiKeyFailure(draft)).toBe('keyIllegalCharacters')
|
||
})
|
||
|
||
it.each([
|
||
['a pasted environment line', 'DEEPSEEK_API_KEY=sk-abc'],
|
||
['double quotes', '"sk-abc"'],
|
||
['single quotes', '\'sk-abc\''],
|
||
['backticks', '`sk-abc`'],
|
||
])('fails %s as a format failure', (_label, draft) => {
|
||
expect(apiKeyFailure(draft)).toBe('keyIllegalCharacters')
|
||
})
|
||
|
||
it('needs a matching closing quote before it calls a value wrapped', () => {
|
||
// A lone quote and an unbalanced one are legal printable ASCII, so the
|
||
// heuristic leaves them alone rather than guessing at a paste error.
|
||
expect(apiKeyFailure('"')).toBeUndefined()
|
||
expect(apiKeyFailure('"a')).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
it.each(['file', 'env', 'project-env', 'user-env'] as const)('shows only the blank-input environment detection for source %s', (source) => {
|
||
renderPiAiEditor('openai', piAiNamespaceWith('openai', { apiKeyEnv: 'OPENAI_API_KEY' }), {
|
||
credential: { configured: true, source, writable: true },
|
||
})
|
||
const hint = en.keyFallbackDetected.replace('{hint}', 'OPENAI_API_KEY')
|
||
const input = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(screen.queryByText(/^API key source:/)).toBeNull()
|
||
if (source === 'file') expect(screen.queryByText(hint)).toBeNull()
|
||
else expect(screen.getAllByText(hint)).toHaveLength(1)
|
||
expect(input.value).toBe('')
|
||
expect(input.placeholder)
|
||
.toBe(source === 'file' ? en.keyPlaceholderSaved : en.keyPlaceholderFallback.replace('{hint}', 'OPENAI_API_KEY'))
|
||
fireEvent.change(input, { target: { value: 'new-key' } })
|
||
expect(screen.queryByText(hint)).toBeNull()
|
||
expect(screen.queryByText(/^API key source:/)).toBeNull()
|
||
expect(input.getAttribute('aria-describedby')).toBeNull()
|
||
fireEvent.change(input, { target: { value: '' } })
|
||
if (source === 'file') expect(screen.queryByText(hint)).toBeNull()
|
||
else expect(screen.getAllByText(hint)).toHaveLength(1)
|
||
})
|
||
|
||
it.each([
|
||
['unconfigured', { apiKeyEnv: 'OPENAI_API_KEY' }, { configured: false, source: 'env', writable: true }],
|
||
['unknown source', { apiKeyEnv: 'OPENAI_API_KEY' }, { configured: true, source: 'oauth', writable: true }],
|
||
['native authentication', {}, { configured: true, source: 'file', writable: true }],
|
||
] as const)('does not guess a source for %s', (_label, profile, credential) => {
|
||
renderPiAiEditor('openai', piAiNamespaceWith('openai', profile), { credential })
|
||
expect(screen.queryByText(/^API key source:/)).toBeNull()
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it('refreshes a blank editor from saved credentials to environment detection without an editor probe', async () => {
|
||
const { face, controller } = await mountSection()
|
||
const reads = face.credentials.describe.mock.calls.length
|
||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder).toBe(en.keyPlaceholderSaved)
|
||
expect(screen.queryByText(/^API key source:/)).toBeNull()
|
||
expect(screen.queryByText(/Env var detected/)).toBeNull()
|
||
expect(face.credentials.describe).toHaveBeenCalledTimes(reads)
|
||
face.credentials.describe.mockImplementation(refs => Promise.resolve(remoteOk(
|
||
Object.fromEntries(refs.map(ref => [ref, { configured: true, source: 'env', writable: true }])),
|
||
)))
|
||
await act(() => controller.load())
|
||
expect(screen.getByText(en.keyFallbackDetected.replace('{hint}', 'OPENAI_API_KEY'))).toBeTruthy()
|
||
expect(screen.queryByText(/^API key source:/)).toBeNull()
|
||
expect(face.credentials.describe).toHaveBeenCalledTimes(reads + 1)
|
||
})
|
||
|
||
it('keeps an OAuth-only provider on its native authentication path', () => {
|
||
renderPiAiEditor('openai-codex', wireNamespaces().find(view => view.ns === 'llm-pi-ai')!, {
|
||
nativeAuth: { env: [], oauth: true },
|
||
})
|
||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||
expect(authHelpText()).toBeNull()
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it.each([
|
||
['anthropic', ['ANTHROPIC_API_KEY'], en.keyHelp.replace('{env}', 'ANTHROPIC_API_KEY')],
|
||
['openai', ['OPENAI_API_KEY'], en.keyHelp.replace('{env}', 'OPENAI_API_KEY')],
|
||
] as const)('opens authentication help for %s on click and dismisses it with Escape', (provider, env, text) => {
|
||
renderPiAiEditor(provider, piAiNamespaceWith(provider, {}), {
|
||
nativeAuth: { env: [...env], oauth: true },
|
||
})
|
||
const help = screen.getByLabelText(en.authHelp)
|
||
fireEvent.click(help)
|
||
const tooltip = screen.getByRole('tooltip')
|
||
expect(tooltip.textContent).toBe(text)
|
||
expect(help.getAttribute('aria-describedby')).toBe(tooltip.id)
|
||
fireEvent.keyDown(document, { key: 'Escape' })
|
||
expect(screen.queryByRole('tooltip')).toBeNull()
|
||
})
|
||
|
||
it.each([
|
||
['an OAuth-only provider', 'openai-codex', 'CODEX_TOKEN', { env: [], oauth: true }],
|
||
[
|
||
'a provider with catalog variables and an OAuth sign-in', 'anthropic', 'TEAM_ANTHROPIC_KEY',
|
||
{ env: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true },
|
||
],
|
||
] as const)('names the profile reference of %s instead of its native authentication', (_label, provider, ref, nativeAuth) => {
|
||
renderPiAiEditor(provider, piAiNamespaceWith(provider, { apiKeyEnv: ref }), { nativeAuth })
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder)
|
||
.toBe(en.keyPlaceholderFallback.replace('{hint}', ref))
|
||
expect(authHelpText()).toBe(en.keyHelp.replace('{env}', ref))
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it.each([
|
||
[
|
||
'with an OAuth sign-in', 'anthropic',
|
||
{ env: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true },
|
||
'ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY',
|
||
],
|
||
['without an OAuth sign-in', 'minimax-cn', { env: ['MINIMAX_CN_API_KEY'], oauth: false }, 'MINIMAX_CN_API_KEY'],
|
||
] as const)('lists method-neutral fallbacks for a catalog route %s', (_label, provider, nativeAuth, refs) => {
|
||
renderPiAiEditor(provider, piAiNamespaceWith(provider, {}), { nativeAuth })
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder)
|
||
.toBe(en.keyPlaceholderFallback.replace('{hint}', nativeAuth.env[0]))
|
||
expect(authHelpText()).toBe(en.keyHelp.replace('{env}', refs))
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it('shows the next method-neutral fallback without modifying native authentication metadata', () => {
|
||
const nativeAuth = { env: ['ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true }
|
||
renderPiAiEditor('anthropic', piAiNamespaceWith('anthropic', {}), { nativeAuth })
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder)
|
||
.toBe(en.keyPlaceholderFallback.replace('{hint}', 'ANTHROPIC_API_KEY'))
|
||
expect(authHelpText()).toBe(en.keyHelp.replace('{env}', 'ANTHROPIC_API_KEY'))
|
||
expect(nativeAuth.env).toEqual(['ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'])
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it('detects a hidden native credential without displaying its reference', () => {
|
||
renderPiAiEditor('anthropic', piAiNamespaceWith('anthropic', {}), {
|
||
nativeAuth: { env: ['ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true },
|
||
credentialChecks: {
|
||
ANTHROPIC_OAUTH_TOKEN: { configured: true, source: 'env', writable: true },
|
||
ANTHROPIC_API_KEY: { configured: false, writable: true },
|
||
},
|
||
})
|
||
expect(screen.queryByText(en.keyFallbackMissing.replace('{hint}', 'ANTHROPIC_API_KEY'))).toBeNull()
|
||
expect(document.body.textContent).toContain('Env var detected.')
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it.each([
|
||
['cloudflare-ai-gateway', ['CLOUDFLARE_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_GATEWAY_ID'], ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_GATEWAY_ID']],
|
||
['cloudflare-workers-ai', ['CLOUDFLARE_API_KEY', 'CLOUDFLARE_ACCOUNT_ID'], ['CLOUDFLARE_ACCOUNT_ID']],
|
||
['google-vertex', ['GOOGLE_CLOUD_API_KEY', 'GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'], ['GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION']],
|
||
] as const)('does not report endpoint variables as detected credentials for %s', (provider, env, baseURLEnv) => {
|
||
renderPiAiEditor(provider, piAiNamespaceWith(provider, {}), {
|
||
nativeAuth: { env, oauth: false },
|
||
baseURLEnv,
|
||
credentialChecks: Object.fromEntries(env.map(ref => [ref, {
|
||
configured: baseURLEnv.some(name => name === ref), source: 'env', writable: true,
|
||
}])),
|
||
})
|
||
expect(screen.getByText(en.keyFallbackMissing.replace('{hint}', env[0]))).toBeTruthy()
|
||
expect(authHelpText()).toBe(en.keyHelp.replace('{env}', env[0]))
|
||
expect(screen.queryByText(/Env var detected/)).toBeNull()
|
||
})
|
||
|
||
it('keeps a method-specific profile reference usable without displaying its name', () => {
|
||
renderPiAiEditor('anthropic', piAiNamespaceWith('anthropic', { apiKeyEnv: 'TEAM_OAUTH_TOKEN' }), {
|
||
nativeAuth: { env: ['ANTHROPIC_API_KEY'], oauth: true },
|
||
})
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder).toBe(en.keyPlaceholder)
|
||
expect(authHelpText()).toBeNull()
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it('offers a plain key field to a route the adapter reports no authentication or endpoint for', () => {
|
||
renderPiAiEditor('zombie', wireNamespaces().find(view => view.ns === 'llm-pi-ai')!)
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.keyInput).placeholder).toBe(en.keyPlaceholder)
|
||
expect(authHelpText()).toBeNull()
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
expect(screen.getByLabelText<HTMLInputElement>(en.baseUrl).placeholder).toBe(en.baseUrlDefault)
|
||
})
|
||
|
||
it('shows endpoint configuration variables without presenting them as credential keys', () => {
|
||
const baseURLEnv = ['AZURE_OPENAI_BASE_URL', 'AZURE_OPENAI_RESOURCE_NAME']
|
||
renderPiAiEditor('azure', wireNamespaces().find(view => view.ns === 'llm-pi-ai')!, {
|
||
baseURLEnv, nativeAuth: { env: ['AZURE_OPENAI_API_KEY'], oauth: false },
|
||
})
|
||
expect(authHelpText()).toContain('AZURE_OPENAI_API_KEY')
|
||
expect(authHelpText()).not.toContain('AZURE_OPENAI_BASE_URL')
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const input = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(input.placeholder).toBe(en.baseUrlConfigured)
|
||
expect(document.getElementById(input.getAttribute('aria-describedby')!)?.textContent)
|
||
.toBe(en.baseUrlEnvironment.replace('{env}', baseURLEnv.join(', ')))
|
||
})
|
||
|
||
it.each([true, false, undefined])('shows the confirmed fallback state without assuming a connection works: %s', (configured) => {
|
||
renderPiAiEditor('deepseek', wireNamespaces().find(view => view.ns === 'llm-pi-ai')!, {
|
||
nativeAuth: { env: ['DEEPSEEK_API_KEY'], oauth: false },
|
||
...configured === undefined ? {} : { credentialChecks: { DEEPSEEK_API_KEY: { configured, writable: true } } },
|
||
})
|
||
const input = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||
expect(input.placeholder).toBe(en.keyPlaceholderFallback.replace('{hint}', 'DEEPSEEK_API_KEY'))
|
||
const hint = configured === true ? en.keyFallbackDetected.replace('{hint}', 'DEEPSEEK_API_KEY')
|
||
: configured === false ? en.keyFallbackMissing.replace('{hint}', 'DEEPSEEK_API_KEY') : en.keyFallbackUnchecked
|
||
expect(screen.getByText(hint).getAttribute('role')).toBeNull()
|
||
expect(document.getElementById(input.getAttribute('aria-describedby')!)?.textContent).toBe(hint)
|
||
fireEvent.change(input, { target: { value: 'new-key' } })
|
||
expect(screen.queryByText(hint)).toBeNull()
|
||
expect(input.getAttribute('aria-describedby')).toBeNull()
|
||
})
|
||
|
||
it('checks all native fallback names and displays a later configured source', () => {
|
||
renderPiAiEditor('anthropic', wireNamespaces().find(view => view.ns === 'llm-pi-ai')!, {
|
||
nativeAuth: { env: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'], oauth: true },
|
||
credentialChecks: {
|
||
ANTHROPIC_AUTH_TOKEN: { configured: false, writable: true },
|
||
ANTHROPIC_OAUTH_TOKEN: { configured: true, writable: true },
|
||
ANTHROPIC_API_KEY: { configured: true, source: 'env', writable: true },
|
||
},
|
||
})
|
||
expect(screen.getByText(en.keyFallbackDetectedHidden)).toBeTruthy()
|
||
expect(document.body.textContent).not.toMatch(/oauth/i)
|
||
})
|
||
|
||
it('keeps an inherited profile endpoint ahead of dynamic provider configuration', () => {
|
||
const namespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
||
renderPiAiEditor('azure', { ...namespace, value: { providers: { azure: { baseURL: 'https://team.example/openai/v1' } } } }, {
|
||
baseURLEnv: ['AZURE_OPENAI_BASE_URL'],
|
||
})
|
||
fireEvent.click(screen.getByText(en.customized))
|
||
const input = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||
expect(input.placeholder).toBe('https://team.example/openai/v1')
|
||
expect(input.getAttribute('aria-describedby')).toBeNull()
|
||
})
|
||
|
||
it('renders the localized account row as signed in without an edit action', async () => {
|
||
const scripted = scriptedFace({})
|
||
const { controller } = await mountFace(scripted)
|
||
const row = controller.store.getSnapshot().rows[0]!
|
||
await act(async () => { controller.store.update((state) => {
|
||
state.rows = [{ ...row, accountAvailable: true, entry: { ...row.entry, provider: 'deepseek-account' } }]
|
||
}) })
|
||
const item = screen.getByText(en.deepSeekAccount).closest('li')!
|
||
expect(within(item).getByText(en.accountSignedIn)).toBeTruthy()
|
||
expect(within(item).queryByText(en.edit)).toBeNull()
|
||
})
|