feat(cli)!: dsh boots profiles; plugin subcommand manages them via pnpm

dsh --profile <name> replaces the fixed entry modes: --config and -p are
removed, --patch adds overlays over the composed profile, a positional task
selects one-shot mode (requires the headless-runner row), and dsh web stays as
the alias for --profile web carrying the Web flag family as patches. dsh
plugin --profile <name> forwards verbatim to pnpm in the profile directory,
initializes on first use, and reconciles the dsh.plugins layer list after
add/remove (patch-less packages warn and stay plain dependencies). Config
dumps and the keyless web e2e scaffold compose the same bundle layers over the
same empty root as the boot.
This commit is contained in:
Turtle
2026-08-06 04:40:32 +08:00
parent 9235d0f90f
commit cd6b4ee3c9
44 changed files with 1126 additions and 1845 deletions

View File

@@ -21,49 +21,65 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
it('routes the required raw config, one-shot prompt, and Web command', () => {
expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'config', config: 'custom.yml' })
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
expect(parse(['web', '--config', 'web.yml'])).toEqual({ mode: 'web', dev: false, config: 'web.yml' })
it('routes profile boots, one-shot tasks, and the web alias', () => {
expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
.toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
.toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w' })
.toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w', patches: [] })
expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
.toEqual({ mode: 'web', dev: false, trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
.toEqual({ mode: 'web', dev: false, patches: [], trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
})
it('routes raw and Web config dumps', () => {
expect(parse(['--config', 'c.yml', '--dump-config']))
.toEqual({ mode: 'dump-config', surface: 'config', defaultOnly: false, config: 'c.yml' })
expect(parse(['--dump-default-config']))
.toEqual({ mode: 'dump-config', surface: 'config', defaultOnly: true })
it('routes the plugin pnpm forwarder', () => {
expect(parse(['plugin', '--profile', 'tui', 'add', 'turtle-ui']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', 'turtle-ui'] })
expect(parse(['plugin', '--profile', 'tui', 'remove', 'turtle-ui']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['remove', 'turtle-ui'] })
expect(parse(['plugin', '--profile', 'tui', 'why', 'cordis']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['why', 'cordis'] })
// Unknown pnpm flags forward verbatim.
expect(parse(['plugin', '--profile', 'tui', 'add', '--save-dev', 'x']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--save-dev', 'x'] })
})
it('routes profile and web config dumps', () => {
expect(parse(['--profile', 'web', '--dump-config']))
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: false, patches: [] })
expect(parse(['--profile', 'web', '--dump-default-config']))
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
expect(parse(['--profile', 'tui', '--dump-config', '--patch', 'x.yml']))
.toEqual({ mode: 'dump-config', profile: 'tui', defaultOnly: false, patches: ['x.yml'] })
expect(parse(['web', '--dump-config']))
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: false })
expect(parse(['web', '--dump-config', '--config', 'w.yml']))
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: false, config: 'w.yml' })
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: false, patches: [] })
expect(parse(['web', '--dump-default-config']))
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: true })
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
})
it('rejects missing config, removed commands, and contradictory inputs', () => {
it('rejects missing profile, removed flags, and contradictory inputs', () => {
expect(exitCode([])).toBe(1)
expect(exitCode(['tui'])).toBe(1)
expect(exitCode(['meta'])).toBe(1)
expect(exitCode(['upgrade'])).toBe(1)
expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
expect(exitCode(['-p', 'task'])).toBe(1) // removed
expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1)
expect(exitCode(['--dump-config', '--dump-default-config', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['--dump-default-config', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['--dump-config', '--config', 'c.yml', '-p', 'task'])).toBe(1)
expect(exitCode(['-p', ''])).toBe(1)
expect(exitCode(['--config='])).toBe(1)
expect(exitCode(['-p', 'x', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['--profile', 'x', '--dump-config', '--dump-default-config'])).toBe(1)
expect(exitCode(['--profile', 'x', '--dump-default-config', '--patch', 'p.yml'])).toBe(1)
expect(exitCode(['--profile', 'x', '--dump-config', 'task'])).toBe(1)
expect(exitCode(['--bogus'])).toBe(1)
expect(exitCode(['bogus-positional'])).toBe(1)
expect(exitCode(['web', '-p', 'task'])).toBe(1)
expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
expect(exitCode(['--profile', 'x', 'web'])).toBe(1)
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
expect(exitCode(['web', '--dump-default-config', '--config', 'w.yml'])).toBe(1)
expect(exitCode(['web', '--config='])).toBe(1)
expect(exitCode(['web', '--dump-default-config', '--patch', 'w.yml'])).toBe(1)
expect(exitCode(['web', '--patch='])).toBe(1)
expect(exitCode(['plugin', 'add', 'x'])).toBe(1) // --profile required
expect(exitCode(['plugin', '--profile', 'tui'])).toBe(1) // nothing to forward
expect(exitCode(['plugin', '--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', 'plugin', 'add', 'y'])).toBe(1)
})
it('exits 0 for help and version', () => {

View File

@@ -1,15 +1,13 @@
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { execa } from 'execa'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
/** Published-entry acceptance for raw argument errors and boot-free config dumps. */
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
const rawOverlay = fileURLToPath(new URL('./fixtures/raw-overlay.cordis.yml', import.meta.url))
const rawInvalidProvider = fileURLToPath(new URL('./fixtures/raw-invalid-provider.cordis.yml', import.meta.url))
async function runBuiltBin(
args: readonly string[] = [],
@@ -31,60 +29,91 @@ async function runBuiltBin(
async function waitForFile(file: string): Promise<void> {
const deadline = Date.now() + 20_000
while (!existsSync(file)) {
if (Date.now() >= deadline) throw new Error(`dsh raw lifecycle marker did not appear: ${file}`)
if (Date.now() >= deadline) throw new Error(`dsh profile lifecycle marker did not appear: ${file}`)
await new Promise(resolve => setTimeout(resolve, 20))
}
}
interface RawLifecycleFixture {
interface ProfileLifecycleFixture {
home: string
ready: string
settled: string
disposed: string
overlay: string
}
function createRawLifecycleFixture(): RawLifecycleFixture {
const home = mkdtempSync(join(tmpdir(), 'dsh-raw-lifecycle-'))
/**
* A minimal custom profile: one lifecycle-marker plugin bundle listed in
* dsh.plugins, no dsh-base — proving out-of-box composition machinery without
* booting the entire product tree.
*/
function createProfileLifecycleFixture(): ProfileLifecycleFixture {
const home = mkdtempSync(join(tmpdir(), 'dsh-profile-lifecycle-'))
const ready = join(home, 'ready')
const settled = join(home, 'settled')
const disposed = join(home, 'disposed')
const plugin = join(home, 'lifecycle.mjs')
const overlay = join(home, 'overlay.cordis.yml')
writeFileSync(plugin, [
const bundleDir = join(home, 'lifecycle-bundle')
mkdirSync(bundleDir, { recursive: true })
writeFileSync(join(bundleDir, 'plugin.mjs'), [
"import { writeFileSync } from 'node:fs'",
"export const name = 'raw-lifecycle-fixture'",
"export const inject = ['sessionQuery']",
"export const name = 'profile-lifecycle-fixture'",
'export function apply(ctx) {',
' let active = true',
' // Keep the event loop alive so process lifetime is signal-owned, like a real surface.',
' const heartbeat = setInterval(() => {}, 1000)',
" writeFileSync(process.env.RAW_READY_FILE, 'ready')",
' void ctx.loader.await().then(() => {',
" if (active) writeFileSync(process.env.RAW_SETTLED_FILE, 'settled')",
' })',
' ctx.effect(() => () => {',
' active = false',
' clearInterval(heartbeat)',
" writeFileSync(process.env.RAW_DISPOSED_FILE, 'disposed')",
' })',
'}',
'',
].join('\n'))
writeFileSync(overlay, [
writeFileSync(join(bundleDir, 'cordis.patch.yml'), [
'- insert:',
' - id: raw-lifecycle-fixture',
` name: ${pathToFileURL(plugin).href}`,
' - id: profile-lifecycle-fixture',
` name: ${pathToFileURL(join(bundleDir, 'plugin.mjs')).href}`,
'',
].join('\n'))
return { home, ready, settled, disposed, overlay }
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({
name: 'dsh-lifecycle-bundle',
version: '0.0.0',
type: 'module',
dsh: { patch: './cordis.patch.yml' },
}, undefined, 2))
const profileDir = join(home, 'profiles', 'lifecycle')
mkdirSync(join(profileDir, 'node_modules'), { recursive: true })
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
name: 'dsh-profile-lifecycle',
private: true,
dependencies: {},
dsh: { plugins: ['dsh-lifecycle-bundle'] },
}, undefined, 2))
// Hand-place the "installed" bundle where profile resolution finds it.
writeFileSync(join(profileDir, 'cordis.patch.yml'), '[]\n')
const linkTarget = join(profileDir, 'node_modules', 'dsh-lifecycle-bundle')
mkdirSync(join(profileDir, 'node_modules'), { recursive: true })
try {
rmSync(linkTarget, { recursive: true, force: true })
} catch { /* fresh dir */ }
// Copy-free: a package.json redirecting via a relative main is enough for require.resolve.
mkdirSync(linkTarget, { recursive: true })
for (const file of ['package.json', 'cordis.patch.yml', 'plugin.mjs']) {
writeFileSync(join(linkTarget, file), readFileSync(join(bundleDir, file)))
}
return { home, ready, settled, disposed }
}
function startRawLifecycle(fixture: RawLifecycleFixture) {
return execa(process.execPath, [dshBin, '--config', fixture.overlay], {
function startProfileLifecycle(fixture: ProfileLifecycleFixture) {
return execa(process.execPath, [dshBin, '--profile', 'lifecycle'], {
cwd: fixture.home,
input: '',
reject: false,
env: {
DSH_HOME: fixture.home,
DSH_TELEMETRY_DISABLED: '1',
RAW_READY_FILE: fixture.ready,
RAW_SETTLED_FILE: fixture.settled,
RAW_DISPOSED_FILE: fixture.disposed,
@@ -93,35 +122,37 @@ function startRawLifecycle(fixture: RawLifecycleFixture) {
}
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
it('requires --config for the raw command and rejects removed commands', async () => {
it('requires --profile and rejects removed commands', async () => {
const bare = await runBuiltBin()
expect(bare.code).toBe(1)
expect(bare.stdout).toBe('')
expect(bare.stderr).toContain('--config <path> is required')
expect(bare.stderr).toContain('--profile <name> is required')
const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --config ./app.cordis.yml')
expect(help.stdout).toContain('dsh --profile web')
expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
for (const command of ['tui', 'meta', 'upgrade']) {
const removed = await runBuiltBin([command])
expect(removed.code).toBe(1)
expect(removed.stderr).not.toContain('experimental')
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task']]) {
const result = await runBuiltBin(removed)
expect(result.code).toBe(1)
}
}, 30_000)
it('reports a raw overlay boot failure without hanging', async () => {
const result = await runBuiltBin(['--config', rawInvalidProvider], {
DEEPSEEK_API_KEY: 'keyless-invalid-config',
DSH_TELEMETRY_DISABLED: '1',
})
expect(result.code).toBe(1)
expect(result.stdout).toBe('')
expect(result.stderr).toContain('llm-pi-ai')
it('fails loud on a nonexistent profile with the plugin-command hint', async () => {
const home = mkdtempSync(join(tmpdir(), 'dsh-missing-profile-'))
try {
const result = await runBuiltBin(['--profile', 'nope'], { DSH_HOME: home })
expect(result.code).toBe(1)
expect(result.stderr).toContain('profile "nope" does not exist')
expect(result.stderr).toContain('dsh plugin --profile nope add')
} finally {
rmSync(home, { recursive: true, force: true })
}
}, 30_000)
it('applies an inserted raw plugin and disposes it on a startup-time signal', async () => {
const fixture = createRawLifecycleFixture()
const child = startRawLifecycle(fixture)
it('applies a custom profile bundle and disposes it on a startup-time signal', async () => {
const fixture = createProfileLifecycleFixture()
const child = startProfileLifecycle(fixture)
try {
await waitForFile(fixture.ready)
child.kill('SIGTERM')
@@ -135,11 +166,24 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, 30_000)
it('fully settles a valid raw overlay and disposes it on a signal', async () => {
const fixture = createRawLifecycleFixture()
const child = startRawLifecycle(fixture)
it('fully settles a custom profile, hot-reloads its patch layer, and disposes on a signal', async () => {
const fixture = createProfileLifecycleFixture()
const child = startProfileLifecycle(fixture)
try {
await waitForFile(fixture.settled)
// The live profile layer: even without an hmr row in the composition,
// the launcher mounts a config-only watcher, so an edited
// cordis.patch.yml lands in the running tree (the reload disposes the
// patched row's old fiber — observable as the disposed marker — and
// mounts the new config, which re-writes the ready marker).
rmSync(fixture.ready)
writeFileSync(join(fixture.home, 'profiles', 'lifecycle', 'cordis.patch.yml'), [
'- id: profile-lifecycle-fixture',
' config:',
' generation: 2',
'',
].join('\n'))
await waitForFile(fixture.ready)
child.kill('SIGTERM')
const result = await child
expect(result.exitCode).toBe(0)
@@ -156,50 +200,53 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'dsh-dump-bin-')) })
afterEach(() => { rmSync(home, { recursive: true, force: true }) })
it('prints the shipped base without a user layer', async () => {
const { stdout, code, stderr } = await runBuiltBin(['--dump-default-config'], { DSH_HOME: home })
it('prints the web profile bundle layers without a user layer', async () => {
const { stdout, code, stderr } = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home })
expect(code).toBe(0)
expect(stderr).toBe('')
expect(stdout).toContain("name: '@deepseek-ai/dsh-agent-loop'")
expect(stdout).toContain('agents: []')
expect(stdout).toContain('# == base.cordis.yml')
expect(stdout).toContain('# == @deepseek-ai/dsh-base')
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
}, 30_000)
it('composes the required raw overlay directly over the base', async () => {
writeFileSync(join(home, 'config.yaml'), [
it('composes the profile user layer and a --patch overlay in order', async () => {
// Auto-init the web profile first, then write its user layer.
const init = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home })
expect(init.code).toBe(0)
const profilePatch = join(home, 'profiles', 'web', 'cordis.patch.yml')
writeFileSync(profilePatch, [
'- id: agent-loop',
' config:',
' agents:',
' - id: personal',
' provider: personal-provider',
' model: personal-model',
'- id: absent-row',
' config:',
' x: 1',
'',
].join('\n'))
const overlay = join(home, 'overlay.cordis.yml')
writeFileSync(overlay, [
'- id: agent-loop',
' config:',
' agents:',
' - id: configured',
' provider: configured-provider',
' model: configured-model',
'',
].join('\n'))
const { stdout, code, stderr } = await runBuiltBin(
['--config', rawOverlay, '--dump-config'],
['--profile', 'web', '--patch', overlay, '--dump-config'],
{ DSH_HOME: home },
)
expect(code).toBe(0)
expect(stdout).toContain('provider: configured-provider')
expect(stdout).not.toContain('personal-provider')
expect(stdout).toContain(`patched by ${rawOverlay}`)
// Both layers patched the row; provenance lists them in application order.
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
expect(stderr).toContain('patch: entry "absent-row" not found')
}, 30_000)
it('keeps the Web overlay and personal layer on the Web command', async () => {
writeFileSync(join(home, 'config.yaml'), [
'- id: agent-loop',
' config:',
' agents:',
' - id: personal',
' provider: personal-provider',
' model: personal-model',
'',
].join('\n'))
const { stdout, code } = await runBuiltBin(['web', '--dump-config'], { DSH_HOME: home })
expect(code).toBe(0)
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
expect(stdout).toContain('provider: personal-provider')
}, 30_000)
})
})

View File

@@ -65,8 +65,17 @@ async function runHeadlessPtySmoke(): Promise<string> {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-headless-shutdown-'))
try {
const home = join(cwd, '.dsh')
await mkdir(home, { recursive: true })
await writeFile(join(home, 'config.yaml'), [
// Pre-initialize the headless profile with the never-dispose row in its
// user patch layer (the same file `dsh --profile headless` hot-reloads).
const profileDir = join(home, 'profiles', 'headless')
await mkdir(profileDir, { recursive: true })
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
name: 'dsh-profile-headless',
private: true,
dependencies: {},
dsh: { plugins: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'] },
}, undefined, 2))
await writeFile(join(profileDir, 'cordis.patch.yml'), [
'- insert:',
' - id: never-dispose',
` name: '${neverDisposePlugin}'`,
@@ -74,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
].join('\n'))
const launch = resolveExampleLaunch({
srcBin: dshBinScript,
configArgs: ['-p', 'never complete'],
configArgs: ['--profile', 'headless', 'never complete'],
tsconfigPath,
env: {
DSH_HOME: home,

View File

@@ -4,8 +4,8 @@
* Only the dedicated Node compatibility gate opts this test in after building
* both artifacts; ordinary Vitest inventory deterministically skips it.
* The child runs built artifacts under plain Node with the real shipped
* config (base.cordis.yml + the web.cordis.yml overlay).
* Its URL line follows AppCLIEntry's settled boot; SIGTERM then exercises the
* web profile (dsh-base + dsh-web-app bundle patches, auto-initialized).
* Its URL line follows the settled profile boot; SIGTERM then exercises the
* shipped quiescent disposer.
*/
@@ -21,8 +21,8 @@ import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
const webDist = join(repoRoot, 'apps/web/dist/index.html')
// The web overlay owns the session-query-sqlite lazy-open patch row.
const configPath = join(repoRoot, 'apps/cli/config/web.cordis.yml')
// The web bundle's patch owns the session-query-sqlite lazy-open row.
const configPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
interface ConfigRow {

View File

@@ -16,7 +16,7 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshSourceBin = 'apps/cli/src/bin.ts'
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
it('boots the source entry and requires the raw config overlay', async () => {
it('boots the source entry and requires a profile', async () => {
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
cwd: repoRoot,
input: '',
@@ -28,7 +28,7 @@ describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
throw new Error(`dsh source launch did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toContain('--config <path> is required')
expect(result.stderr).toContain('--profile <name> is required')
expect(result.stdout).toBe('')
}, 30_000)
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { resolveTelemetryPatch } from '../src/app-cli-entry.ts'
import { resolveTelemetryPatch } from '../src/profile-boot.ts'
describe('resolveTelemetryPatch', () => {
it('keeps telemetry enabled when the switch is unset or empty', () => {

View File

@@ -1,7 +1,7 @@
/** Single-sample LAN-trust resolution for the /api browser-trust fence (`resolveLanTrust`). */
import { describe, expect, it, vi } from 'vitest'
import { resolveLanTrust } from '../src/app-cli-entry.ts'
import { resolveLanTrust } from '../src/web.ts'
vi.mock('node:os', () => ({
networkInterfaces: () => ({

View File

@@ -1,32 +0,0 @@
import { sep } from 'node:path'
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { HARNESS_SOURCE_SECTION } from '@deepseek-ai/dsh-app-boot'
import type {} from '@deepseek-ai/dsh-host-webserver'
import { prepareWebRuntimeContext } from '../src/web.ts'
describe('prepareWebRuntimeContext', () => {
it('installs both sections before a later systemPrompt consumer activates', async () => {
const ctx = new Context()
const sourceRoot = `${sep}opt${sep}harness-src`
let observedSections: { name: string; text: string }[] | undefined
try {
prepareWebRuntimeContext(ctx, sourceRoot, 'production')
ctx.provide('httpServer', { port: 3080 } as Context['httpServer'])
const consumer = ctx.inject(['systemPrompt'], async (promptCtx) => {
const assembly = await promptCtx.systemPrompt.assemble()
observedSections = assembly.sections
})
await ctx.plugin(SystemPrompt, { persona: 'You are a coding agent.' })
await consumer
expect(observedSections?.map(section => section.name)).toContain(HARNESS_SOURCE_SECTION)
expect(observedSections?.find(section => section.name === 'app:web-surface')?.text)
.toContain('http://127.0.0.1:3080')
} finally {
await ctx.fiber.dispose()
}
})
})