| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- /**
- * Real-composition guard for the dynamic-configuration chain: LlmService,
- * settings-local, credentials-local, and llm-deepseek boot from a test-only
- * cordis.yml through the actual Loader + Include path, external edits of
- * settings.yaml and .env hot-publish through their providers, and the very
- * next request carries the fresh base URL and credential. The same adapter
- * composition without settings or credentials entries keeps entry-config
- * behavior — the documented optional-inject fallback.
- */
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { pathToFileURL } from 'node:url'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { Context } from 'cordis'
- import Loader from '@cordisjs/plugin-loader'
- import Include from '@cordisjs/plugin-include'
- 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')
- let root: string | undefined
- let context: Context | undefined
- afterEach(async () => {
- await context?.fiber.dispose()
- context = undefined
- if (root !== undefined) await rm(root, { recursive: true, force: true })
- root = undefined
- await closeMockServers()
- vi.unstubAllEnvs()
- })
- async function loadComposition(
- options: { withDynamic: boolean; baseURL: string; reuseRoot?: string },
- ): Promise<{ ctx: Context; settingsPath: string; envPath: string }> {
- // A reused root is the restart case: the same harness home, its documents
- // exactly as the previous process left them.
- const fresh = options.reuseRoot === undefined
- root = options.reuseRoot ?? await mkdtemp(join(tmpdir(), 'dsh-llm-composition-'))
- const settingsPath = join(root, 'settings.yaml')
- const envPath = join(root, '.env')
- if (options.withDynamic && fresh) {
- await writeFile(settingsPath, '# personal settings\n')
- await writeFile(envPath, 'DEEPSEEK_API_KEY=boot-key\n')
- }
- const configPath = join(root, 'cordis.yml')
- await writeFile(configPath, [
- '- id: llm',
- " name: 'test-llm-service'",
- ...options.withDynamic
- ? [
- '- id: settings',
- " name: '@deepseek-ai/dsh-settings-local'",
- ' config:',
- ` path: ${JSON.stringify(settingsPath)}`,
- ' debounceMs: 10',
- '- id: credentials',
- " name: '@deepseek-ai/dsh-credentials-local'",
- ' config:',
- ` path: ${JSON.stringify(envPath)}`,
- ' debounceMs: 10',
- ]
- : [],
- '- id: llm-deepseek',
- " name: '@deepseek-ai/dsh-llm-deepseek'",
- ' config:',
- ` baseURL: ${JSON.stringify(options.baseURL)}`,
- ...options.withDynamic ? [] : [' apiKey: entry-key'],
- '',
- ].join('\n'))
- const ctx = new Context()
- context = ctx
- ctx.baseUrl = pathToFileURL(root).href + '/'
- await ctx.plugin(Loader)
- ctx.loader.builtins.include = Include
- const modules = new Map<string, unknown>([
- ['test-llm-service', LlmService],
- ['@deepseek-ai/dsh-settings-local', SettingsLocal],
- ['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
- ['@deepseek-ai/dsh-llm-deepseek', LlmDeepSeek],
- ])
- ctx.loader.internal = {
- version: 'v2',
- async import(specifier: string) {
- if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
- return modules.get(specifier)
- },
- } as unknown as NonNullable<typeof ctx.loader.internal>
- await ctx.loader.create({
- name: 'cordis:include',
- config: { path: pathToFileURL(configPath).href },
- })
- await ctx.loader.await()
- return { ctx, settingsPath, envPath }
- }
- describe('llm-deepseek real dynamic composition', () => {
- it('boots from cordis.yml and routes the next request after external settings and .env edits', async () => {
- vi.stubEnv('DEEPSEEK_API_KEY', '')
- const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
- const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
- const { ctx, settingsPath, envPath } = await loadComposition({ withDynamic: true, baseURL: serverA.url })
- expect(ctx.get('settings')!.describe().map(entry => entry.ns)).toEqual([NS])
- await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
- expect(serverA.headers[0]?.authorization).toBe('Bearer boot-key')
- // External edits, exactly as a user or the web UI would leave them on disk.
- await writeFile(settingsPath, `llm-deepseek:\n baseURL: ${serverB.url}\n`)
- await vi.waitFor(() => {
- expect((ctx.get('settings')!.get(NS) as { baseURL?: string }).baseURL).toBe(serverB.url)
- }, { timeout: 5000 })
- await writeFile(envPath, 'DEEPSEEK_API_KEY=rotated-key\n')
- await vi.waitFor(async () => {
- expect(await ctx.get('credentials')!.resolve(KEY_REF)).toEqual({ value: 'rotated-key', source: 'file' })
- }, { timeout: 5000 })
- await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
- expect(serverA.requests).toHaveLength(1)
- expect(serverB.headers[0]?.authorization).toBe('Bearer rotated-key')
- })
- it('keeps a stored key writable and rotatable across a real restart', async () => {
- // No ambient DEEPSEEK_API_KEY: the shipped surfaces no longer hoist
- // $DSH_HOME/.env into process.env, so a stored key must stay file-sourced.
- vi.stubEnv('DEEPSEEK_API_KEY', '')
- const first = await mockServer([{ kind: 'sse', events: textEvents }])
- const second = await mockServer([{ kind: 'sse', events: textEvents }])
- const boot = await loadComposition({ withDynamic: true, baseURL: first.url })
- const home = root!
- await boot.ctx.get('credentials')!.set(KEY_REF, 'stored-by-ui')
- expect(await boot.ctx.get('credentials')!.describe(KEY_REF))
- .toEqual({ configured: true, source: 'file', writable: true })
- await assemble(boot.ctx, { model: 'deepseek-v4-flash', messages: [] })
- expect(first.headers[0]?.authorization).toBe('Bearer stored-by-ui')
- await boot.ctx.fiber.dispose()
- context = undefined
- // Restart over the same harness home.
- const restarted = await loadComposition({ withDynamic: true, baseURL: second.url, reuseRoot: home })
- const credentials = restarted.ctx.get('credentials')!
- // The stored key is still the provider's own writable file entry — not a
- // read-only launch override, which is what hoisting it would have made it.
- expect(await credentials.resolve(KEY_REF)).toEqual({ value: 'stored-by-ui', source: 'file' })
- expect(await credentials.describe(KEY_REF)).toEqual({ configured: true, source: 'file', writable: true })
- // Rotation still works after the restart, and the next request uses it.
- await credentials.set(KEY_REF, 'rotated-after-restart')
- await assemble(restarted.ctx, { model: 'deepseek-v4-flash', messages: [] })
- expect(second.headers[0]?.authorization).toBe('Bearer rotated-after-restart')
- })
- it('boots the same adapter without settings or credentials entries on entry config alone', async () => {
- vi.stubEnv('DEEPSEEK_API_KEY', '')
- const server = await mockServer([{ kind: 'sse', events: textEvents }])
- const { ctx } = await loadComposition({ withDynamic: false, baseURL: server.url })
- expect(ctx.get('settings')).toBeUndefined()
- expect(ctx.get('credentials')).toBeUndefined()
- await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
- expect(server.headers[0]?.authorization).toBe('Bearer entry-key')
- })
- })
|