The Models settings section joins llm.providers (the configurable directory with live state), settings.describe (schemas, layered redacted values, secret slots), and credentials.describe (value-free badges) into provider rows with one editor card at a time. The editor renders the provider's profile subtree through dsh-client-schema-form; the credential-ref role mounts a control that shows configured/source state and stores keys write-only through credentials.set. Apply without removals merges a minimal patch (stored secrets outside it survive); apply after a reset — and row deletion — replace the user section so removals land. The client runtime bridges the three new host frames to typed ctx events (settings/credentials/models changed), the page refetches on any of them once loaded, and ui-model's per-session picker directories reload on models/changed so a settings-born route appears in open pickers without a reopen.
72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
/**
|
|
* Wire-to-typed-event bridge (web input-triggers cut 1): host/commands-changed
|
|
* → ctx 'commands/changed'; each established connection generation →
|
|
* ctx 'connection/reset' (the forced cache-invalidation broadcast).
|
|
*/
|
|
import { Context } from 'cordis'
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { ConnectionHandle, ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
|
|
import * as RuntimeClient from '../src/client/index.ts'
|
|
import { FakeApiClient } from './fake-api.ts'
|
|
|
|
interface Bench {
|
|
ctx: Context
|
|
sinks: ConnectionSinks | undefined
|
|
}
|
|
|
|
async function mount(): Promise<Bench> {
|
|
const ctx = new Context()
|
|
const api = new FakeApiClient()
|
|
const bench: Bench = { ctx, sinks: undefined }
|
|
const handle: ConnectionHandle = {
|
|
api,
|
|
start: (sinks) => {
|
|
bench.sinks = sinks
|
|
return { stop: () => {} }
|
|
},
|
|
}
|
|
ctx.reflect.provide('connection', handle)
|
|
await ctx.plugin(RuntimeClient).await()
|
|
return bench
|
|
}
|
|
|
|
describe('wire event bridge', () => {
|
|
it('broadcasts commands/changed on a host/commands-changed frame, not on other host frames', async () => {
|
|
const bench = await mount()
|
|
let changed = 0
|
|
bench.ctx.on('commands/changed', () => { changed++ })
|
|
bench.sinks?.onHostEnvelope?.({ rpcId: 'r1' as never, payload: { type: 'host/commands-changed' } })
|
|
expect(changed).toBe(1)
|
|
bench.sinks?.onHostEnvelope?.({
|
|
rpcId: 'r2' as never,
|
|
payload: { type: 'host/session-status', sessionId: 's1' as never, running: true },
|
|
})
|
|
expect(changed).toBe(1)
|
|
})
|
|
|
|
it('broadcasts the settings/credentials/models invalidations with their frame payloads', async () => {
|
|
const bench = await mount()
|
|
const seen: unknown[][] = []
|
|
bench.ctx.on('settings/changed', ns => seen.push(['settings', ns]))
|
|
bench.ctx.on('credentials/changed', ref => seen.push(['credentials', ref]))
|
|
bench.ctx.on('models/changed', () => seen.push(['models']))
|
|
bench.sinks?.onHostEnvelope?.({ rpcId: 'r3' as never, payload: { type: 'host/settings-changed', ns: 'llm-pi-ai' } })
|
|
bench.sinks?.onHostEnvelope?.({ rpcId: 'r4' as never, payload: { type: 'host/credentials-changed', ref: 'OPENAI_API_KEY' } })
|
|
bench.sinks?.onHostEnvelope?.({ rpcId: 'r5' as never, payload: { type: 'host/models-changed' } })
|
|
expect(seen).toEqual([
|
|
['settings', 'llm-pi-ai'],
|
|
['credentials', 'OPENAI_API_KEY'],
|
|
['models'],
|
|
])
|
|
})
|
|
|
|
it('broadcasts connection/reset on every established generation (reconnect invalidation)', async () => {
|
|
const bench = await mount()
|
|
let resets = 0
|
|
bench.ctx.on('connection/reset', () => { resets++ })
|
|
bench.sinks?.onConnected?.()
|
|
bench.sinks?.onConnected?.() // second generation after a reconnect
|
|
expect(resets).toBe(2)
|
|
})
|
|
})
|