| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- /** 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-client-connection/client'
- 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: [{ path: ['apiKey'], set: false }],
- 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({})),
- },
- }
- return { face: face as never, seenRefs }
- }
- describe('ModelsSettingsStore', () => {
- it('joins rows with configured, removable, and credential state', async () => {
- const { face, seenRefs } = api()
- const store = new ModelsSettingsStore(face)
- await store.load()
- const state = store.store.getSnapshot()
- expect(state.status).toBe('ready')
- expect(state.writable).toBe(true)
- expect(state.credentialError).toBeNull()
- expect(seenRefs).toEqual([['DEEPSEEK_API_KEY', 'OPENAI_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 },
- literalApiKeyConfigured: false,
- })
- 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 } = api({ describeCredentials: () => Promise.resolve(fail('no provider')) })
- const store = new ModelsSettingsStore(face)
- 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 } = api({
- describeCredentials: () => Promise.reject(new Error('credential transport down')),
- })
- const store = new ModelsSettingsStore(face)
- 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 } = api({
- // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error rejection is the scenario
- describeCredentials: () => Promise.reject('credential transport refusal'),
- })
- const store = new ModelsSettingsStore(face)
- await expect(store.load()).resolves.toBeUndefined()
- expect(store.store.getSnapshot().credentialError).toBe('credential transport refusal')
- })
- it('joins a configured literal key from the redacted secret sidecar', async () => {
- const { face } = api({
- describeSettings: () => Promise.resolve(ok({
- writable: true,
- hasDocument: false,
- namespaces: [{
- ...NAMESPACES[0],
- secrets: [
- { path: ['apiKey', 'nested'], set: true },
- { path: ['different'], set: true },
- { path: ['apiKey'], set: true },
- ],
- }] as never,
- })),
- providers: () => Promise.resolve(ok({ providers: [DIRECTORY[0]] as never })),
- })
- const store = new ModelsSettingsStore(face)
- await store.load()
- expect(store.store.getSnapshot().rows[0]).toMatchObject({
- literalApiKeyConfigured: true,
- apiKeyEnv: 'DEEPSEEK_API_KEY',
- })
- })
- it('surfaces a directory failure and keeps the last good rows', async () => {
- const { face } = api()
- const store = new ModelsSettingsStore(face)
- 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)
- 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 } = api({
- providers: async () => {
- call += 1
- if (call === 1) {
- await gate
- return fail('stale slow failure')
- }
- return ok({ providers: DIRECTORY })
- },
- })
- const store = new ModelsSettingsStore(face)
- 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 } = 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)
- await store.load()
- const state = store.store.getSnapshot()
- expect(state.rows[0]).toMatchObject({ configured: true, removable: false })
- expect(state.rows[0]?.apiKeyEnv).toBeUndefined()
- })
- it('skips the credential describe entirely when no row names a reference', async () => {
- const { face, 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,
- })),
- })
- const store = new ModelsSettingsStore(face)
- await store.load()
- expect(seenRefs).toEqual([])
- expect(store.store.getSnapshot().status).toBe('ready')
- })
- it('surfaces a settings describe failure', async () => {
- const { face } = api({ describeSettings: () => Promise.resolve(fail('settings down')) })
- const store = new ModelsSettingsStore(face)
- await store.load()
- expect(store.store.getSnapshot()).toMatchObject({ status: 'error', error: 'settings down' })
- })
- it('stringifies a non-Error load failure', async () => {
- // The wire can surface non-Error throwables; the store must stringify them.
- // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error rejection is the scenario
- const { face } = api({ providers: () => Promise.reject('plain refusal') })
- const store = new ModelsSettingsStore(face)
- 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 } = api({
- providers: async () => {
- call += 1
- if (call === 1) {
- await gate
- return ok({ providers: [] as never })
- }
- return ok({ providers: DIRECTORY })
- },
- })
- const store = new ModelsSettingsStore(face)
- 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')
- })
- })
|