Files
deepseek-harness/packages/client/ui-settings-models/tests/store.client.spec.ts
T
Yichen Jiang bf0db65bb0 fix(ui-settings-models): address review — derived key fact, required render seat, spec sync
The provider-card seat's keyConfigured now derives from the reference the
page would use — the profile's apiKeyEnv, or the page's derived
<ROUTE>_API_KEY while the profile names none — so the add-provider draft
agrees with its own editor about an existing conventional credential (the
store joins the derived describe in the same batched call, as
ProviderRow.derivedCredential). ModelsSectionProps makes the renderSlot seat
required so a direct render that forgets it fails to compile; the one such
render in provider-form.client.spec regained a real mount and the test
boilerplate collapsed to renderSlot={() => null}. The extension-slots Agent
Note now states the keyed cell's real override rule (same priority throws,
a different priority shadows), and docs/subsystems/slots.md carries the two
new seats in its hierarchy, both languages.
2026-08-26 17:12:58 +08:00

310 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Page-store join: directory × namespaces × credentials, with last-good rows on failure. */
import { describe, expect, it } from 'vitest'
import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client'
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts'
import { settingsSchema } from './settings-schema.client.ts'
import { messageOf, ModelsSettingsStore } from '../src/client/store.ts'
let nextRpc = 0
function ok<T>(value: T): RpcResponse<T> {
return { rpcId: `r-${nextRpc++}` as never, result: { ok: true, value } }
}
function fail<T>(message: string): RpcResponse<T> {
return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code: 'internal', message, details: {} } } }
}
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 },
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false },
{ provider: 'ghost', displayName: 'Ghost', settingsNs: '', settingsPath: [], active: true },
]
const NAMESPACES = [
{
ns: 'llm-deepseek',
schema: {},
value: { apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://base' },
base: { baseURL: 'https://base' },
applies: 'live' as const,
secrets: [],
revision: 0,
},
{
ns: 'llm-pi-ai',
schema: {},
value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } },
user: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } },
applies: 'live' as const,
secrets: [],
revision: 0,
},
]
function api(overrides: {
providers?: () => Promise<RpcResponse<{ providers: typeof DIRECTORY }>>
describeSettings?: () => Promise<RpcResponse<{ writable: boolean; namespaces: typeof NAMESPACES }>>
describeCredentials?: (refs: string[]) => Promise<RpcResponse<{ credentials: Record<string, unknown> }>>
} = {}) {
const seenRefs: string[][] = []
const face = {
llm: {
providers: overrides.providers ?? (() => Promise.resolve(ok({ providers: DIRECTORY }))),
models: () => Promise.resolve(ok({ groups: [], failures: [] })),
},
settings: {
describe: overrides.describeSettings ?? (() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: NAMESPACES }))),
update: () => Promise.resolve(fail('unused')),
replace: () => Promise.resolve(fail('unused')),
},
credentials: {
describe: (payload: { refs: string[] }) => {
seenRefs.push(payload.refs)
return (overrides.describeCredentials ?? (refs => Promise.resolve(ok({
credentials: Object.fromEntries(refs.map(ref => [ref, { configured: ref === 'OPENAI_API_KEY', writable: true }])),
}))))(payload.refs)
},
set: () => Promise.resolve(ok({})),
unset: () => Promise.resolve(ok({})),
},
}
const wire = face as never
return { face: wire, mirror: new SettingsDescribeMirror(wire), seenRefs }
}
describe('ModelsSettingsStore', () => {
it('joins rows with configured, removable, and credential state', async () => {
const { face, mirror, seenRefs } = api()
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
const state = store.store.getSnapshot()
expect(state.status).toBe('ready')
expect(state.writable).toBe(true)
expect(state.credentialError).toBeNull()
// Named references first (rows order), then the derived <ROUTE>_API_KEY
// of every row whose profile names none — one batched describe.
expect(seenRefs).toEqual([['DEEPSEEK_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GHOST_API_KEY']])
const byProvider = new Map(state.rows.map(row => [row.entry.provider, row]))
expect(byProvider.get('deepseek-official')).toMatchObject({
configured: true,
removable: false,
apiKeyEnv: 'DEEPSEEK_API_KEY',
credential: { configured: false, writable: true },
})
expect(byProvider.get('openai')).toMatchObject({
configured: true,
removable: true,
apiKeyEnv: 'OPENAI_API_KEY',
credential: { configured: true },
})
expect(byProvider.get('anthropic')).toMatchObject({ configured: false, removable: false })
expect(byProvider.get('anthropic')?.apiKeyEnv).toBeUndefined()
expect(byProvider.get('ghost')).toMatchObject({ configured: false, removable: false })
expect(state.namespaces.get('llm-pi-ai')?.ns).toBe('llm-pi-ai')
})
it('degrades the credential badge, not the page, when the credential domain fails', async () => {
const { face, mirror } = api({ describeCredentials: () => Promise.resolve(fail('no provider')) })
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
const state = store.store.getSnapshot()
expect(state.status).toBe('ready')
expect(state.credentialError).toBe('no provider')
expect(state.rows.every(row => row.credential === undefined)).toBe(true)
})
it('settles a credential transport rejection without leaving the store loading', async () => {
const { face, mirror } = api({
describeCredentials: () => Promise.reject(new Error('credential transport down')),
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await expect(store.load()).resolves.toBeUndefined()
expect(store.store.getSnapshot()).toMatchObject({
status: 'ready',
credentialError: 'credential transport down',
})
})
it('stringifies a non-Error credential transport rejection', async () => {
const { face, mirror } = api({
describeCredentials: async () => { throw 'credential transport refusal' },
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await expect(store.load()).resolves.toBeUndefined()
expect(store.store.getSnapshot().credentialError).toBe('credential transport refusal')
})
it('surfaces a directory failure and keeps the last good rows', async () => {
const { face, mirror } = api()
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
expect(store.store.getSnapshot().rows).toHaveLength(4)
const broken = api({ providers: () => Promise.resolve(fail('directory down')) })
const failing = new ModelsSettingsStore(broken.face, settingsSchema, broken.mirror)
await failing.load()
expect(failing.store.getSnapshot()).toMatchObject({ status: 'error', error: 'directory down' })
// The first store's snapshot is untouched by the second's failure.
expect(store.store.getSnapshot().status).toBe('ready')
})
it('lets the newest load win over a stale slow response', async () => {
let release: (() => void) | undefined
const gate = new Promise<void>((resolve) => { release = resolve })
let call = 0
const { face, mirror } = api({
providers: async () => {
call += 1
if (call === 1) {
await gate
return fail('stale slow failure')
}
return ok({ providers: DIRECTORY })
},
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
const first = store.load()
const second = store.load()
release?.()
await Promise.all([first, second])
expect(store.store.getSnapshot().status).toBe('ready')
})
})
describe('edge joins', () => {
it('treats a non-object profile as having no credential reference', async () => {
const { face, mirror } = api({
describeSettings: () => Promise.resolve(ok({
writable: true,
hasDocument: false,
namespaces: [{
ns: 'llm-pi-ai',
schema: {},
value: { providers: { weird: 'oops' } },
applies: 'live' as const,
secrets: [],
revision: 0,
}] as never,
})),
providers: () => Promise.resolve(ok({
providers: [
{ provider: 'weird', displayName: 'weird', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'weird'], active: false },
] as never,
})),
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
const state = store.store.getSnapshot()
expect(state.rows[0]).toMatchObject({ configured: true, removable: false })
expect(state.rows[0]?.apiKeyEnv).toBeUndefined()
})
it('describes the derived reference for a row whose profile names none', async () => {
const { face, mirror, seenRefs } = api({
describeSettings: () => Promise.resolve(ok({
writable: true,
hasDocument: false,
namespaces: [{ ns: 'llm-pi-ai', schema: {}, value: { providers: {} }, applies: 'live' as const, secrets: [], revision: 0 }] as never,
})),
providers: () => Promise.resolve(ok({
providers: [
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false },
] as never,
})),
describeCredentials: refs => Promise.resolve(ok({
credentials: Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
})),
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
// The dormant row names no reference, so the join asks about the page's
// own derived <ROUTE>_API_KEY — what the editor would display for it.
expect(seenRefs).toEqual([['ANTHROPIC_API_KEY']])
const state = store.store.getSnapshot()
expect(state.status).toBe('ready')
expect(state.rows[0]?.credential).toBeUndefined()
expect(state.rows[0]?.derivedCredential).toMatchObject({ configured: true })
})
it('surfaces a settings describe failure', async () => {
const { face, mirror } = api({ describeSettings: () => Promise.resolve(fail('settings down')) })
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
expect(store.store.getSnapshot()).toMatchObject({ status: 'error', error: 'settings down' })
})
it('reports a terminally unavailable settings mirror precisely', async () => {
const { face } = api()
const store = new ModelsSettingsStore(
face,
settingsSchema,
new SettingsDescribeMirror(face, 'memory'),
)
await store.load()
expect(store.store.getSnapshot()).toMatchObject({
status: 'error',
error: 'settings are unavailable in this browser',
})
})
it('reuses a held settings view after its refresh fails', async () => {
let settingsCall = 0
const { face, mirror } = api({
describeSettings: () => {
settingsCall += 1
return Promise.resolve(settingsCall === 1
? ok({ writable: true, hasDocument: false, namespaces: NAMESPACES })
: fail('settings refresh down'))
},
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
await mirror.load()
expect(mirror.getSnapshot().error).toBe('settings refresh down')
await store.load()
expect(store.store.getSnapshot()).toMatchObject({ status: 'ready', error: null })
expect(store.store.getSnapshot().rows).toHaveLength(4)
})
it('stringifies a non-Error load failure', async () => {
// The wire can surface non-Error throwables; the store must stringify them.
const { face, mirror } = api({ providers: async () => { throw 'plain refusal' } })
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
await store.load()
expect(store.store.getSnapshot()).toMatchObject({ status: 'error', error: 'plain refusal' })
})
it('drops a stale successful response after a newer load finished', async () => {
let release: (() => void) | undefined
const gate = new Promise<void>((resolve) => { release = resolve })
let call = 0
const { face, mirror } = api({
providers: async () => {
call += 1
if (call === 1) {
await gate
return ok({ providers: [] as never })
}
return ok({ providers: DIRECTORY })
},
})
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
const first = store.load()
const second = store.load()
await second
release?.()
await first
// The stale empty directory never overwrote the newer join.
expect(store.store.getSnapshot().rows).toHaveLength(4)
})
})
describe('messageOf', () => {
it('reads an Error message, and stringifies anything else a rejection may carry', () => {
// The wire layer rejects with an Error, but a host or a runtime can reject
// with any value, and the page still has to render something.
expect(messageOf(new Error('connection lost'))).toBe('connection lost')
expect(messageOf('the host refused')).toBe('the host refused')
expect(messageOf(undefined)).toBe('undefined')
})
})