mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Five findings from the #939 review, each reproduced before being fixed. **Configuration reads are as privileged as writes.** `settings.describe` returns every exposed namespace's configuration and `credentials.describe` reports whether an arbitrary environment-variable name is configured and from where — reconnaissance no anonymous caller should have. Both join PRIVILEGED_METHODS, so the whole configuration plane is loopback-only until real authentication exists; `trustedHosts` was never authentication. The model catalog stays reachable: it carries no endpoints or key state, and a LAN client's model picker legitimately needs it. Asserted over a real HTTP server, because the Host header a browser actually sends is what decides this. **The proxy serves only namespaces a registered model provider addresses.** The settings seam is general — any plugin may register one — but the Web configuration plane is the model-provider surface. Without the gate, every future `settings.register()` would silently become remotely readable and writable configuration. An unregistered namespace and an unexposed one answer identically, so no caller can enumerate the registry one probe at a time. **Path-addressed writes replace the redacted-document rebuild.** The editor reads the REDACTED descriptor, so rebuilding a section from it and replacing wholesale deleted every literal secret the wire never returned — reproduced as `{baseURL, reasoning}` in, stored `apiKey` gone out. `settings.mutate` applies set/unset ops to the section as it stands at the front of the seam's write queue, and the client names only fields it can see, so an unseen secret is untouched by construction rather than by care. P2s in the same pass: `llm/adapters-updated` now contains async listener rejections (an uncontained one escaped as unhandledRejection, contradicting the documented "observer failures are contained"); llm-deepseek's retry-policy swap uses the atomic `registration.replace` instead of dispose-then-register, which published `[]` then `["deepseek-official"]` so an observer saw the provider disappear and come back; and a transport rejection no longer strands the page in `loading` or a card in `busy`, with removal failures surfaced on the page banner instead of swallowed.
196 lines
8.1 KiB
TypeScript
196 lines
8.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import LlmService from '@deepseek-ai/dsh-llm'
|
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
|
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
import { SettingsLocal } from '@deepseek-ai/dsh-settings-local'
|
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
|
import { assemble } from './assemble.ts'
|
|
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
|
|
|
const NS = settingsNamespace('llm-deepseek')
|
|
const KEY_REF = credentialRef('DEEPSEEK_API_KEY')
|
|
|
|
const cleanups: Array<() => Promise<void>> = []
|
|
|
|
afterEach(async () => {
|
|
while (cleanups.length > 0) await cleanups.pop()!()
|
|
await closeMockServers()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
async function home(): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-llm-dynamic-'))
|
|
cleanups.push(() => rm(dir, { recursive: true, force: true }))
|
|
return dir
|
|
}
|
|
|
|
interface Harness {
|
|
ctx: Context
|
|
settingsFiber: { dispose(): Promise<void> }
|
|
}
|
|
|
|
/**
|
|
* Real dynamic composition: llm + settings-local + credentials-local +
|
|
* llm-deepseek over one temp harness home. `watch: false` keeps every change
|
|
* flowing through the in-process write path, which is deterministic; external
|
|
* file watching is the providers' own covered concern.
|
|
*/
|
|
async function boot(dir: string, config: object): Promise<Harness> {
|
|
const ctx = new Context()
|
|
cleanups.push(async () => {
|
|
await ctx.fiber.dispose()
|
|
})
|
|
await ctx.plugin(LlmService)
|
|
const settingsFiber = ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
|
|
await settingsFiber
|
|
await ctx.plugin(CredentialsLocal, { path: join(dir, '.env'), watch: false })
|
|
await ctx.plugin(LlmDeepSeek, config)
|
|
return { ctx, settingsFiber }
|
|
}
|
|
|
|
function prompt(ctx: Context) {
|
|
return assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
|
}
|
|
|
|
describe('request-level dynamic configuration', () => {
|
|
it('routes the next request with the freshly resolved base URL and credential', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const dir = await home()
|
|
await writeFile(join(dir, '.env'), 'DEEPSEEK_API_KEY=first-key\n')
|
|
const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx } = await boot(dir, { baseURL: serverA.url })
|
|
|
|
await prompt(ctx)
|
|
expect(serverA.headers[0]?.authorization).toBe('Bearer first-key')
|
|
|
|
await ctx.settings.update(NS, { baseURL: serverB.url })
|
|
await ctx.credentials.set(KEY_REF, 'second-key')
|
|
|
|
await prompt(ctx)
|
|
// No restart, no re-registration: the next request resolved both facts.
|
|
expect(serverA.requests).toHaveLength(1)
|
|
expect(serverB.headers[0]?.authorization).toBe('Bearer second-key')
|
|
})
|
|
|
|
it('prefers a literal settings apiKey over the credential layers', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const dir = await home()
|
|
await writeFile(join(dir, '.env'), 'DEEPSEEK_API_KEY=file-key\n')
|
|
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx } = await boot(dir, { baseURL: server.url })
|
|
|
|
await ctx.settings.update(NS, { apiKey: 'literal-key' })
|
|
await prompt(ctx)
|
|
expect(server.headers[0]?.authorization).toBe('Bearer literal-key')
|
|
})
|
|
|
|
it('starts keyless and serves the next request once the key arrives', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const dir = await home()
|
|
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx } = await boot(dir, { baseURL: server.url })
|
|
|
|
await expect(prompt(ctx)).rejects.toMatchObject({ code: 'MISSING_CREDENTIAL' })
|
|
await ctx.credentials.set(KEY_REF, 'sk-arrived')
|
|
await prompt(ctx)
|
|
expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived')
|
|
})
|
|
|
|
it('advertises a live settings catalog without re-registration', async () => {
|
|
const dir = await home()
|
|
const { ctx } = await boot(dir, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
|
|
|
|
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
|
|
await ctx.settings.update(NS, { models: [{ id: 'settings-model', name: 'From Settings' }] })
|
|
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
|
{ provider: 'deepseek-official', id: 'settings-model', name: 'From Settings' },
|
|
])
|
|
})
|
|
|
|
it('re-registers the route in place when the captured retry policy changes, without an empty-registry window', async () => {
|
|
const dir = await home()
|
|
const { ctx } = await boot(dir, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
|
|
|
|
// Observing the topology event, not just the end state: disposing and
|
|
// re-registering also lands on the right final registry, but publishes an
|
|
// empty route set in between, so an observer sees the provider disappear.
|
|
const observed: string[][] = []
|
|
ctx.on('llm/adapters-updated', () => {
|
|
observed.push(ctx.llm.listProviders().map(provider => provider.id))
|
|
})
|
|
|
|
await ctx.settings.update(NS, {
|
|
retryPolicy: { mode: 'always', backoff: { initialDelayMs: 25, maxDelayMs: 100, jitterRatio: 0.2 } },
|
|
})
|
|
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toEqual({
|
|
mode: 'always',
|
|
initialDelayMs: 25,
|
|
maxDelayMs: 100,
|
|
jitterRatio: 0.2,
|
|
})
|
|
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
|
|
expect(observed).toEqual([['deepseek-official']])
|
|
})
|
|
|
|
it('keeps the last good options when a settings snapshot fails beyond-schema validation', async () => {
|
|
const dir = await home()
|
|
const { ctx } = await boot(dir, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
|
|
|
|
// Schema-valid but resolver-invalid: duplicate catalog ids pass the array
|
|
// schema and fail the explicit resolve step.
|
|
await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] })
|
|
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
|
|
await ctx.settings.update(NS, { models: [{ id: 'recovered' }] })
|
|
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
|
{ provider: 'deepseek-official', id: 'recovered', name: 'recovered' },
|
|
])
|
|
})
|
|
|
|
it('sends the whole last-good snapshot when a rejected one changed both the key and the URL', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const dir = await home()
|
|
const good = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const rejected = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx } = await boot(dir, { apiKey: 'good-key', baseURL: good.url })
|
|
|
|
// One snapshot moves the endpoint AND the literal key, and fails the
|
|
// resolve step beyond the schema (duplicate catalog ids).
|
|
await ctx.settings.update(NS, {
|
|
apiKey: 'rejected-key',
|
|
baseURL: rejected.url,
|
|
models: [{ id: 'dup' }, { id: 'dup' }],
|
|
})
|
|
|
|
await prompt(ctx)
|
|
// The rejected generation contributes nothing: not its endpoint, and — the
|
|
// regression this pins — not its key either.
|
|
expect(rejected.requests).toHaveLength(0)
|
|
expect(good.requests).toHaveLength(1)
|
|
expect(good.headers[0]?.authorization).toBe('Bearer good-key')
|
|
})
|
|
|
|
it('falls back to the composition entry when settings detach', async () => {
|
|
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
|
const dir = await home()
|
|
await writeFile(join(dir, '.env'), 'DEEPSEEK_API_KEY=steady-key\n')
|
|
const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
|
|
const { ctx, settingsFiber } = await boot(dir, { baseURL: serverA.url })
|
|
|
|
await ctx.settings.update(NS, { baseURL: serverB.url })
|
|
await prompt(ctx)
|
|
expect(serverB.requests).toHaveLength(1)
|
|
|
|
await settingsFiber.dispose()
|
|
await prompt(ctx)
|
|
expect(serverA.requests).toHaveLength(1)
|
|
expect(serverA.headers[0]?.authorization).toBe('Bearer steady-key')
|
|
})
|
|
})
|