Merge remote-tracking branch 'origin/master' into worktree/drop-create-by-name

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md
#	apps/cli/reference/README.i18n.yaml
#	docs/config-catalog.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/api-proxy-approval.spec.ts
#	packages/host/apiproxy/tests/api-proxy-blank.spec.ts
#	packages/host/apiproxy/tests/api-proxy-cold.spec.ts
#	packages/host/apiproxy/tests/api-proxy-commands.spec.ts
#	packages/host/apiproxy/tests/api-proxy-config.spec.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/apiproxy/tests/api-proxy-projections.spec.ts
#	packages/host/apiproxy/tests/api-proxy-question.spec.ts
#	packages/host/apiproxy/tests/api-proxy-rename.spec.ts
#	packages/host/apiproxy/tests/api-proxy-search.spec.ts
#	packages/host/apiproxy/tests/api-proxy-subagents.spec.ts
#	packages/host/apiproxy/tests/api-proxy-view.spec.ts
#	packages/host/apiproxy/tests/api-proxy-workspace.spec.ts
#	packages/todo/tool-todo/tests/projection.spec.ts
#	scripts/hero-composer-dom-continuity.mjs
This commit is contained in:
creatixchu
2026-08-10 15:49:34 +08:00
4118 changed files with 128795 additions and 32407 deletions

View File

@@ -21,12 +21,16 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
it('routes profile boots, one-shot tasks, and the web alias', () => {
it('routes profile boots, one-shot runs, 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(['run', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['run', '--profile', 'custom', '--patch', 'a.yml', '--patch', 'b.yml', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'custom', patches: ['a.yml', 'b.yml'], task: 'run the tests' })
expect(parse(['run', '--', '--profile', 'is', 'task', 'text']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: '--profile is task text' })
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']))
@@ -60,11 +64,18 @@ describe('parseDshArgs', () => {
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
})
it('rejects missing profile, removed flags, and contradictory inputs', () => {
it('rejects missing profile, flags outside the current grammar, and contradictory inputs', () => {
expect(exitCode([])).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(['--config', 'c.yml'])).toBe(1) // outside the current grammar
expect(exitCode(['-p', 'task'])).toBe(1) // outside the current grammar
expect(exitCode(['--profile', 'headless', 'task'])).toBe(1) // tasks belong to `run`
expect(exitCode(['run'])).toBe(1)
expect(exitCode(['run', ''])).toBe(1)
expect(exitCode(['run', '--profile', '', 'task'])).toBe(1)
expect(exitCode(['run', '--patch=', 'task'])).toBe(1)
expect(exitCode(['--profile', 'headless', 'run', 'task'])).toBe(1)
expect(exitCode(['--patch', 'parent.yml', 'run', 'task'])).toBe(1)
expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1)
@@ -90,6 +101,7 @@ describe('parseDshArgs', () => {
it('exits 0 for help and version', () => {
expect(exitCode(['--help'])).toBe(0)
expect(exitCode(['run', '--help'])).toBe(0)
expect(exitCode(['--version'])).toBe(0)
})
})

View File

@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { execa } from 'execa'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
@@ -13,14 +14,21 @@ const invalidProvider = fileURLToPath(new URL('./fixtures/invalid-provider.cordi
async function runBuiltBin(
args: readonly string[] = [],
env: Record<string, string> = {},
env: Readonly<Record<string, string | undefined>> = {},
cwd?: string,
): Promise<{ stdout: string; code: number; stderr: string }> {
const childEnv = Object.fromEntries(
Object.entries({ ...process.env, ...env })
.filter((entry): entry is [string, string] => entry[1] !== undefined),
)
const result = await execa(process.execPath, [dshBin, ...args], {
input: '',
timeout: 25_000,
killSignal: 'SIGKILL',
reject: false,
env,
env: childEnv,
extendEnv: false,
...cwd === undefined ? {} : { cwd },
})
if (result.timedOut) {
throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
@@ -41,6 +49,7 @@ interface ProfileLifecycleFixture {
ready: string
settled: string
disposed: string
interrupt: string
}
/**
@@ -53,16 +62,23 @@ function createProfileLifecycleFixture(): ProfileLifecycleFixture {
const ready = join(home, 'ready')
const settled = join(home, 'settled')
const disposed = join(home, 'disposed')
const interrupt = join(home, 'interrupt')
const bundleDir = join(home, 'lifecycle-bundle')
mkdirSync(bundleDir, { recursive: true })
writeFileSync(join(bundleDir, 'plugin.mjs'), [
"import { writeFileSync } from 'node:fs'",
"import { existsSync, writeFileSync } from 'node:fs'",
"import { join } from 'node:path'",
"export const name = 'profile-lifecycle-fixture'",
'export function apply(ctx, config = {}) {',
' let active = true',
' // Keep the event loop alive so process lifetime is signal-owned, like a real surface.',
' const heartbeat = setInterval(() => {}, 1000)',
' // Windows has no deliverable SIGTERM; the marker emits the same process event there.',
' let interrupted = false',
' const heartbeat = setInterval(() => {',
' if (interrupted || !existsSync(process.env.RAW_INTERRUPT_FILE)) return',
' interrupted = true',
" process.emit('SIGTERM')",
' }, 20)',
' // Echo the mounted generation so the hot-reload e2e can assert both an',
' // applied override and its removal reverting to this bundle default.',
" writeFileSync(join(process.env.DSH_HOME, 'config-echo'), String(config.generation ?? 'bundle-default'))",
@@ -110,7 +126,7 @@ function createProfileLifecycleFixture(): ProfileLifecycleFixture {
for (const file of ['package.json', 'cordis.patch.yml', 'plugin.mjs']) {
writeFileSync(join(linkTarget, file), readFileSync(join(bundleDir, file)))
}
return { home, ready, settled, disposed }
return { home, ready, settled, disposed, interrupt }
}
function startProfileLifecycle(fixture: ProfileLifecycleFixture) {
@@ -123,12 +139,63 @@ function startProfileLifecycle(fixture: ProfileLifecycleFixture) {
RAW_READY_FILE: fixture.ready,
RAW_SETTLED_FILE: fixture.settled,
RAW_DISPOSED_FILE: fixture.disposed,
RAW_INTERRUPT_FILE: fixture.interrupt,
},
})
}
function requestProfileShutdown(
child: ReturnType<typeof startProfileLifecycle>,
fixture: ProfileLifecycleFixture,
): void {
if (process.platform === 'win32') {
writeFileSync(fixture.interrupt, 'interrupt')
return
}
child.kill('SIGTERM')
}
function createEnvironmentProbeProfile(home: string, project: string): void {
const pluginFile = join(project, 'environment-probe.mjs')
writeFileSync(pluginFile, [
"export const name = 'environment-probe'",
"export const inject = ['llm']",
'export function apply(ctx) {',
' void ctx.loader.await().then(async () => {',
" let text = ''",
' for await (const chunk of ctx.llm.stream({',
" provider: 'deepseek-official',",
" model: 'deepseek-v4-flash',",
' messages: [],',
' maxTokens: 32,',
' })) {',
" if (chunk.type === 'text-delta') text += chunk.text",
' }',
' process.stdout.write(`${text}\\n`)',
" if (process.platform === 'win32') process.emit('SIGTERM')",
" else process.kill(process.pid, 'SIGTERM')",
' })',
'}',
'',
].join('\n'))
const profileDir = join(home, 'profiles', 'environment-probe')
mkdirSync(profileDir, { recursive: true })
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
name: 'dsh-profile-environment-probe',
private: true,
dependencies: {},
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base'] } },
}, undefined, 2))
writeFileSync(join(profileDir, 'cordis.patch.yml'), [
'- insert:',
' - id: environment-probe',
` name: ${pathToFileURL(pluginFile).href}`,
'',
].join('\n'))
}
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
it('requires --profile and rejects removed commands', async () => {
it('requires --profile and rejects inputs outside the current grammar', async () => {
const bare = await runBuiltBin()
expect(bare.code).toBe(1)
expect(bare.stdout).toBe('')
@@ -136,14 +203,67 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --profile web')
expect(help.stdout).toContain('dsh run "run the tests"')
expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task']]) {
const result = await runBuiltBin(removed)
for (const outsideGrammar of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
const result = await runBuiltBin(outsideGrammar)
expect(result.code).toBe(1)
}
}, 30_000)
it('prints run help without initializing the selected profile', async () => {
const parent = mkdtempSync(join(tmpdir(), 'dsh-run-help-'))
const home = join(parent, 'not-created')
try {
const result = await runBuiltBin(['run', '--help'], { DSH_HOME: home })
expect(result.code).toBe(0)
expect(result.stderr).toBe('')
expect(result.stdout).toContain('Usage: dsh run [options] <task...>')
expect(existsSync(home)).toBe(false)
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
it('runs the default headless profile through the published run command', async () => {
const apiKey = 'built-dsh-run-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'published dsh run reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-built-run-'))
try {
const result = await runBuiltBin(['run', 'answer', 'from', 'the', 'published', 'entry'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('published dsh run reached the mock')
expect(result.stderr).toBe('')
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
expect(JSON.stringify(server.requests.map(request => request.body))).toContain('answer from the published entry')
} finally {
await server.close()
rmSync(home, { recursive: true, force: true })
}
}, 30_000)
it('does not load a project environment for --version', async () => {
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n')
try {
const result = await runBuiltBin(['--version'], {}, project)
expect(result).toEqual({ code: 0, stdout: '0.0.1', stderr: '' })
} finally {
rmSync(project, { recursive: true, force: true })
}
})
it('fails loud on a nonexistent profile with the plugin-command hint', async () => {
const home = mkdtempSync(join(tmpdir(), 'dsh-missing-profile-'))
try {
@@ -156,10 +276,51 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, 30_000)
it('uses the Harness-home environment and managed credential through the published entry', async () => {
const apiKey = 'built-home-layer-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'home environment reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-home-environment-'))
const project = mkdtempSync(join(tmpdir(), 'dsh-home-project-'))
writeFileSync(join(home, '.env'), `DEEPSEEK_BASE_URL=${server.baseURL}\n`)
writeFileSync(join(home, '.credentials.yaml'), `DEEPSEEK_API_KEY: ${apiKey}\n`, { mode: 0o600 })
createEnvironmentProbeProfile(home, project)
try {
const result = await runBuiltBin(
['--profile', 'environment-probe'],
{
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: undefined,
DEEPSEEK_BASE_URL: undefined,
},
project,
)
expect(
result.code,
`${result.stderr}\nstdout:\n${result.stdout}\nmock requests: ${String(server.requests.length)}`,
).toBe(0)
expect(result.stdout).toBe('home environment reached the mock')
expect(result.stdout).not.toContain(apiKey)
expect(result.stderr).not.toContain(apiKey)
expect(server.requests).toHaveLength(1)
expect(server.requests[0]?.path).toBe('/chat/completions')
expect(server.requests[0]?.headers.authorization).toBe(`Bearer ${apiKey}`)
expect(JSON.stringify(server.requests[0]?.body)).not.toContain(apiKey)
} finally {
await server.close()
rmSync(home, { recursive: true, force: true })
rmSync(project, { recursive: true, force: true })
}
}, 30_000)
it('reports a patch-overlay boot failure without hanging', async () => {
// The HMR main watcher's initial scan once refreshed the include
// mid-initial-apply, deadlocking the failing apply's rollback against the
// refresh drain: dsh exited 13 with no diagnostic instead of settling
// An HMR main-watcher initial scan that refreshes the include
// mid-initial-apply deadlocks the failing apply's rollback against the
// refresh drain: dsh exits 13 with no diagnostic instead of settling
// ([Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md)).
const home = mkdtempSync(join(tmpdir(), 'dsh-invalid-patch-'))
try {
@@ -181,9 +342,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const child = startProfileLifecycle(fixture)
try {
await waitForFile(fixture.ready)
child.kill('SIGTERM')
requestProfileShutdown(child, fixture)
const result = await child
expect(result.exitCode).toBe(0)
expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}\nsignal: ${String(result.signal)}`).toBe(0)
expect(result.signal).toBeUndefined()
expect(existsSync(fixture.disposed)).toBe(true)
} finally {
@@ -233,9 +394,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
].join('\n'))
await waitForFile(fixture.ready)
expect(readFileSync(configFile, 'utf8')).toBe('home')
child.kill('SIGTERM')
requestProfileShutdown(child, fixture)
const result = await child
expect(result.exitCode).toBe(0)
expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}\nsignal: ${String(result.signal)}`).toBe(0)
expect(result.signal).toBeUndefined()
expect(existsSync(fixture.disposed)).toBe(true)
} finally {
@@ -330,6 +491,20 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
}, 30_000)
it('prints a headless profile with no Host, HTTP, or browser rows', async () => {
const { stdout, code, stderr } = await runBuiltBin(
['--profile', 'headless', '--dump-default-config'],
{ DSH_HOME: home },
)
expect(code).toBe(0)
expect(stderr).toBe('')
expect(stdout).toContain("name: '@deepseek-ai/dsh-agent-default-model'")
expect(stdout).toContain("name: '@deepseek-ai/dsh-headless'")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-host-")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-client-")
}, 30_000)
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 })
@@ -364,7 +539,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(code).toBe(0)
expect(stdout).toContain('provider: configured-provider')
expect(stdout).not.toContain('personal-provider')
// Both layers patched the row; provenance lists them in application order.
// Both layers patched the row; the comment lists them in application order.
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
expect(stderr).toContain('patch: entry "absent-row" not found')
}, 30_000)

View File

@@ -0,0 +1,176 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('./fixtures/dsh-badge/snapshot.ts', import.meta.url))
const configPath = fileURLToPath(new URL('./fixtures/dsh-badge/cordis.yml', import.meta.url))
const defaultConfigPath = fileURLToPath(new URL('./fixtures/dsh-badge/default.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const badgeAssetsPath = fileURLToPath(new URL('../../../packages/skill/skill-badge/assets/', import.meta.url))
describe('dsh badge assembled snapshot', () => {
it('advertises and loads the opt-in bundled skill through the shipped app', async () => {
const disabled = await runLoaderSmoke({
label: 'disabled dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-disabled-',
binScript,
libBinScript: binScript,
configPath: defaultConfigPath,
tsconfigPath,
})
const enabled = await runLoaderSmoke({
label: 'dsh badge skill snapshot',
tempDirPrefix: 'headless-snapshot-dsh-badge-',
binScript,
libBinScript: binScript,
configPath,
tsconfigPath,
})
const disabledSnapshot = JSON.parse(disabled.stdout) as unknown
const enabledSnapshot = JSON.parse(
enabled.stdout.replaceAll(badgeAssetsPath, '{{badgeAssetsPath}}'),
) as unknown
expect(disabled.stderr).toBe('')
expect(enabled.stderr).toBe('')
expect(disabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": null,
"result": {
"content": [
{
"text": "Error: skill "dsh-badge" is unknown or no longer available",
"type": "text",
},
],
"error": {
"message": "skill "dsh-badge" is unknown or no longer available",
},
"isError": true,
},
"summary": null,
}
`)
expect(enabledSnapshot).toMatchInlineSnapshot(`
{
"catalog": [
{
"text": "<system-reminder>
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
<available_skills>
- \`dsh-badge\`: Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.
</available_skills>
If the user names a skill, or the task clearly matches a skill's description, call the \`skill\` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.
A user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the \`skill\` tool again for that skill.
</system-reminder>",
"type": "text",
},
],
"result": {
"content": [
{
"text": "<skill_content name="dsh-badge">
<skill_resources>
Base directory for this skill: {{badgeAssetsPath}}
Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.
</skill_resources>
<skill_instructions>
# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
</skill_instructions>
</skill_content>",
"type": "text",
},
],
"isError": false,
"value": {
"content": "# dsh Badge
Add the official “powered by dsh” badge without recreating or restyling it.
## Assets
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk)
\`\`\`
If attribution should not be linked, use:
\`\`\`markdown
![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)
\`\`\`
## Usage rules
- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image.
- For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge.
- Preserve the badge's 121×20 dimensions and aspect ratio.
- Place the badge at the end of the attributed document or section unless the user specifies another position.
- Do not substitute another color, logo, label, or project URL.
",
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
},
},
"summary": {
"description": "Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.",
"invocation": {
"modelInvocable": true,
"userInvocable": true,
},
"name": "dsh-badge",
"provider": "dsh-badge",
"resourceBase": {
"kind": "directory",
"path": "{{badgeAssetsPath}}",
},
"source": "bundled",
},
}
`)
}, LOADER_SMOKE_TEST_TIMEOUT_MS * 2)
})

View File

@@ -0,0 +1,9 @@
- id: skill-badge
disabled: false
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,6 @@
- id: skill-local
config:
watch: false
- id: telemetry-otel
disabled: true

View File

@@ -0,0 +1,56 @@
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
const overlayPath = process.argv[2]
if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path')
const rootConfigPath = fileURLToPath(new URL('../../../../../packages/bundle/base/tests/fixtures/root.cordis.yml', import.meta.url))
const basePatchPath = fileURLToPath(new URL('../../../../../packages/bundle/base/cordis.patch.yml', import.meta.url))
const ctx = await boot('dsh-badge-snapshot', rootConfigPath, [
...loadOverlayPatches('dsh-badge-snapshot', basePatchPath),
...loadOverlayPatches('dsh-badge-snapshot', overlayPath),
])
try {
const agentId = SessionId('dsh-badge-snapshot')
const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } })
const agent: Agent = {
ctx: new Context(),
id: agentId,
options: {},
session,
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
status: 'idle',
send: () => {},
followup: () => {},
steer: () => {},
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
cancel: () => {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
{ messages: [], turn: 1, step: 1, signal: new AbortController().signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const catalog = decision.kind === 'enter'
? decision.messages.find(message => message.role === 'user'
&& message.source.kind === 'skill-catalog')?.content
: undefined
const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge')
const result = await ctx.tools.execute({
callId: CallId('dsh-badge-snapshot'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
})
process.stdout.write(`${JSON.stringify({ catalog: catalog ?? null, summary: summary ?? null, result })}\n`)
} finally {
await ctx.fiber.dispose()
}

View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"github_repository": {
"command": "node",
"args": [
"lib/mcp-server.mjs"
]
}
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "dsh-github-repository-plugin-e2e-fixture",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"lib",
"dsh-plugin.mjs",
"dsh-plugin-assets"
],
"scripts": {
"prepack": "tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare"
},
"dsh": {
"skills": [
"../skills"
],
"mcpServers": "./.mcp.json",
"entry": "./lib/plugin.mjs"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.29.0"
},
"devDependencies": {
"@deepseek-ai/dsh-repository-plugin": "0.0.1",
"cordis": "4.0.0-rc.7",
"tsdown": "0.22.2",
"typescript": "6.0.3"
}
}

View File

@@ -0,0 +1,19 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// The repository root's linter cannot resolve this independently installed
// Git-package dependency; the package's prepack tsc validates the SDK types.
/* oxlint-disable typescript/no-unsafe-assignment, typescript/no-unsafe-call, typescript/no-unsafe-member-access */
const server = new McpServer({
name: 'github-repository-plugin-e2e',
version: '0.0.0',
})
server.registerTool('proof', {
description: 'Proves that an MCP server compiled from the exact GitHub repository package is active.',
inputSchema: {},
}, async () => ({
content: [{ type: 'text', text: 'MCP_FROM_GITHUB_REPOSITORY' }],
}))
await server.connect(new StdioServerTransport())

View File

@@ -0,0 +1,59 @@
import type { Context } from 'cordis'
const PROOF_TOOL_NAME = 'mcp__github_repository__proof'
interface TextBlock {
readonly type: 'text'
readonly text: string
}
interface ToolExecution {
readonly name: string
}
interface ToolResult {
readonly isError: boolean
readonly content: readonly TextBlock[]
}
type PostDecision =
| { readonly kind: 'accept'; readonly content?: readonly TextBlock[]; readonly value?: unknown; readonly additionalContexts?: readonly unknown[] }
| { readonly kind: 'block'; readonly feedback: readonly TextBlock[] }
type PostListener = (
execution: ToolExecution,
result: ToolResult,
next: () => Promise<PostDecision>,
) => Promise<PostDecision>
type DshContext = Context & {
on(event: 'tools/post-execute', listener: PostListener): () => void
}
/** Cordis plugin name used by the repository acceptance fixture. */
export const name = 'github-repository-typescript-proof'
/** DSH tool registry required by the post-execute contribution. */
export const inject = ['tools']
/**
* Append a marker after the repository MCP proof tool succeeds.
* @param ctx - trusted DSH Cordis context supplied to the repository package.
*/
export function apply(ctx: Context): void {
const dsh = ctx as DshContext
dsh.on('tools/post-execute', async (execution, result, next): Promise<PostDecision> => {
const decision = await next()
if (execution.name !== PROOF_TOOL_NAME || result.isError || decision.kind !== 'accept' || Object.hasOwn(decision, 'value')) {
return decision
}
return {
kind: 'accept',
content: [
...(decision.content ?? result.content),
{ type: 'text', text: 'TS_PLUGIN_FROM_GITHUB_REPOSITORY' },
],
...decision.additionalContexts === undefined ? {} : { additionalContexts: decision.additionalContexts },
}
})
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": [
"src/**/*.ts"
]
}

View File

@@ -0,0 +1,6 @@
---
name: github-source-proof
description: Proves that dsh installed a private repository Plugin from an exact GitHub source.
---
This skill exists only in the GitHub repository source fixture.

View File

@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs'
*/
export function apply(ctx) {
const keepAlive = setInterval(() => {}, 60_000)
process.stderr.write('dsh-test: never-dispose ready\n')
ctx.effect(() => async () => {
clearInterval(keepAlive)
const armFile = process.env.DSH_TEST_SHUTDOWN_ARM_FILE

View File

@@ -0,0 +1,256 @@
import { createHash } from 'node:crypto'
import { cpSync, existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:http'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
const repositoryPluginPackage = join(repoRoot, 'packages/self-modification/repository-plugin')
const releasePackageNames = new Set(globSync([
'vendor/*/package.json',
'packages/*/*/package.json',
'apps/*/package.json',
], { cwd: repoRoot }).map((filename) => {
const manifest = JSON.parse(readFileSync(join(repoRoot, filename), 'utf8')) as Record<string, unknown>
if (typeof manifest.name !== 'string') throw new Error(`workspace package name is missing: ${filename}`)
return manifest.name
}))
const source = process.env.DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE
const required = process.env.DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E === '1'
const enabled = required || source !== undefined
interface PublishedPackageRegistry {
url: string
requests: string[]
close(): Promise<void>
}
function publishedManifest(): Record<string, unknown> {
const manifest = JSON.parse(readFileSync(join(repositoryPluginPackage, 'package.json'), 'utf8')) as Record<string, unknown>
const version = manifest.version
if (typeof version !== 'string') throw new Error('repository Plugin package version is missing')
Reflect.deleteProperty(manifest, 'private')
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
const dependencies = manifest[field]
if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies)) continue
const entries = dependencies as Record<string, unknown>
for (const name of Object.keys(entries)) {
if (releasePackageNames.has(name)) {
entries[name] = version
}
}
}
return manifest
}
async function startPublishedPackageRegistry(root: string): Promise<PublishedPackageRegistry> {
const staging = join(root, 'published-repository-plugin')
const artifacts = join(root, 'npm-registry-artifacts')
mkdirSync(staging)
mkdirSync(artifacts)
cpSync(join(repositoryPluginPackage, 'lib'), join(staging, 'lib'), { recursive: true })
for (const filename of ['README.md', 'README.zh.md', 'README.i18n.yaml']) {
cpSync(join(repositoryPluginPackage, filename), join(staging, filename))
}
cpSync(join(repoRoot, 'LICENSE'), join(staging, 'LICENSE'))
const manifest = publishedManifest()
writeFileSync(join(staging, 'package.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const packed = await execa('pnpm', ['pack', '--pack-destination', artifacts], {
cwd: staging,
reject: false,
})
if (packed.exitCode !== 0) {
throw new Error(`failed to pack the simulated published prepare package:\n${packed.stderr}\n${packed.stdout}`)
}
const tarballs = readdirSync(artifacts).filter(filename => filename.endsWith('.tgz'))
if (tarballs.length !== 1) throw new Error(`expected one simulated published tarball, found ${tarballs.length}`)
const tarball = readFileSync(join(artifacts, tarballs[0]!))
const name = manifest.name as string
const version = manifest.version as string
const requests: string[] = []
let registryUrl = ''
const server = createServer((request, response) => {
const path = decodeURIComponent(new URL(request.url ?? '/', registryUrl).pathname)
requests.push(`${request.method ?? 'GET'} ${path}`)
if (path === `/${name}`) {
const metadata = {
name,
'dist-tags': { latest: version },
versions: {
[version]: {
...manifest,
dist: {
tarball: `${registryUrl}${name}/-/${name.split('/').at(-1)}-${version}.tgz`,
shasum: createHash('sha1').update(tarball).digest('hex'),
integrity: `sha512-${createHash('sha512').update(tarball).digest('base64')}`,
},
},
},
}
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify(metadata))
return
}
if (path === `/${name}/-/${name.split('/').at(-1)}-${version}.tgz`) {
response.writeHead(200, {
'content-type': 'application/octet-stream',
'content-length': String(tarball.length),
})
response.end(tarball)
return
}
response.writeHead(404, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: 'not found' }))
})
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('simulated npm registry did not expose a TCP address')
registryUrl = `http://127.0.0.1:${address.port}/`
return {
url: registryUrl,
requests,
close: () => new Promise<void>((resolve, reject) => {
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
}),
}
}
describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () => {
it('installs the published prepare dependency, then builds and runs skill, MCP, and TypeScript Plugin contributions from a private exact GitHub source', async () => {
expect(existsSync(dshBin), 'the repository Plugin acceptance must run the built dsh entry').toBe(true)
expect(source, 'DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE is required by this CI lane').toMatch(
/^github:[^/\s#&]+\/[^/\s#&]+#[0-9a-f]{40}&path:\/.*\/\.dsh-plugin$/u,
)
const apiKey = 'github-repository-plugin-e2e-key'
const server = await startMockLlmServer({
sequence: ['tool_call_success', 'success'],
apiKey,
toolName: 'mcp__github_repository__proof',
toolArguments: '{}',
successText: 'trusted GitHub repository package reached dsh run',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-github-repository-plugin-'))
const registry = await startPublishedPackageRegistry(home)
const npmrc = join(home, 'npmrc')
writeFileSync(npmrc, `@deepseek-ai:registry=${registry.url}\n`)
const hostBin = join(home, 'host-bin')
mkdirSync(hostBin)
writeFileSync(join(hostBin, 'dsh-plugin-prepare'), [
'#!/bin/sh',
'echo "host PATH supplied dsh-plugin-prepare instead of the declared npm dependency" >&2',
'exit 91',
'',
].join('\n'), { mode: 0o700 })
const patch = join(home, 'github-repository-plugin.cordis.patch.yml')
writeFileSync(patch, [
'- id: repository-plugins',
' config:',
' repositories:',
` - ${JSON.stringify(source)}`,
'- id: session-title-llm',
' disabled: true',
'',
].join('\n'))
try {
const result = await execa(process.execPath, [
dshBin,
'run',
'--patch',
patch,
'prove the private GitHub repository Plugin is active',
], {
cwd: repoRoot,
input: '',
timeout: 180_000,
killSignal: 'SIGKILL',
reject: false,
env: {
...process.env,
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
NPM_CONFIG_USERCONFIG: npmrc,
// A warm runner cache could satisfy the exact tarball without
// contacting this test's registry, which would stop proving the
// unpublished package was installed through the simulated release.
PNPM_CONFIG_CACHE_DIR: join(home, 'pnpm-cache'),
PNPM_CONFIG_STORE_DIR: join(home, 'pnpm-store'),
PATH: process.env.PATH === undefined ? hostBin : `${hostBin}${delimiter}${process.env.PATH}`,
},
})
if (result.timedOut) {
throw new Error(`dsh GitHub repository Plugin run did not exit within 180s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}`).toBe(0)
expect(result.stdout).toBe('trusted GitHub repository package reached dsh run')
expect(server.requests).toHaveLength(2)
const runtimeDiagnostic = `${result.stderr}\nstdout:\n${result.stdout}`
expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin')
expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin/-/dsh-repository-plugin-0.0.1.tgz')
const firstRequest = JSON.stringify(server.requests[0]!.body)
const secondRequest = JSON.stringify(server.requests[1]!.body)
expect(firstRequest, runtimeDiagnostic).toContain(
'Proves that dsh installed a private repository Plugin from an exact GitHub source.',
)
expect(firstRequest, runtimeDiagnostic).toContain('mcp__github_repository__proof')
expect(firstRequest, runtimeDiagnostic).toContain('Proves that an MCP server compiled from the exact GitHub repository package is active.')
expect(secondRequest, runtimeDiagnostic).toContain('MCP_FROM_GITHUB_REPOSITORY')
expect(secondRequest, runtimeDiagnostic).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
const cacheRoot = join(home, 'cache', 'repository-plugins')
const generations = readdirSync(cacheRoot, { withFileTypes: true }).filter(entry => entry.isDirectory())
expect(generations).toHaveLength(1)
const installed = join(cacheRoot, generations[0]!.name, 'node_modules', 'repository')
const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as Record<string, unknown>
expect(manifest).toMatchObject({
name: 'dsh-github-repository-plugin-e2e-fixture',
private: true,
scripts: {
prepack: 'tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare',
},
dsh: {
skills: ['../skills'],
mcpServers: './.mcp.json',
entry: './lib/plugin.mjs',
},
dependencies: {
'@modelcontextprotocol/sdk': '1.29.0',
},
devDependencies: {
'@deepseek-ai/dsh-repository-plugin': '0.0.1',
cordis: '4.0.0-rc.7',
tsdown: '0.22.2',
typescript: '6.0.3',
},
})
expect(readFileSync(join(installed, 'dsh-plugin-assets/skills/0/github-source-proof/SKILL.md'), 'utf8'))
.toContain('This skill exists only in the GitHub repository source fixture.')
expect(readFileSync(join(installed, 'dsh-plugin-assets/.mcp.json'), 'utf8')).toContain('lib/mcp-server.mjs')
expect(readFileSync(join(installed, 'lib/plugin.mjs'), 'utf8')).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
expect(readFileSync(join(installed, 'lib/mcp-server.mjs'), 'utf8')).toContain('MCP_FROM_GITHUB_REPOSITORY')
expect(existsSync(join(installed, 'src'))).toBe(false)
const installedRequire = createRequire(join(installed, 'lib/mcp-server.mjs'))
expect(existsSync(installedRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js'))).toBe(true)
const wrapper = readFileSync(join(installed, 'dsh-plugin.mjs'), 'utf8')
expect(wrapper).toContain('dsh-repository-plugin')
expect(wrapper).toContain('await import(manifest.entry)')
expect(wrapper).toContain('"entry":"./lib/plugin.mjs"')
} finally {
await server.close()
await registry.close()
rmSync(home, { recursive: true, force: true })
}
}, 190_000)
})

View File

@@ -22,7 +22,7 @@ if pid == 0:
os.chdir(cwd)
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
markers = [b"dsh: observing at ", b"dsh-test: never-dispose started"]
markers = [b"dsh-test: never-dispose ready", b"dsh-test: never-dispose started"]
output = bytearray()
marker_index = 0
deadline = time.monotonic() + float(timeout_seconds)
@@ -66,14 +66,14 @@ async function runHeadlessPtySmoke(): Promise<string> {
try {
const home = join(cwd, '.dsh')
// Pre-initialize the headless profile with the never-dispose row in its
// user patch layer (the same file `dsh --profile headless` hot-reloads).
// user patch layer (the same file a long-lived profile boot 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: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'] } },
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'] } },
}, undefined, 2))
await writeFile(join(profileDir, 'cordis.patch.yml'), [
'- insert:',
@@ -83,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
].join('\n'))
const launch = resolveExampleLaunch({
srcBin: dshBinScript,
configArgs: ['--profile', 'headless', 'never complete'],
configArgs: ['run', 'never complete'],
tsconfigPath,
env: {
DSH_HOME: home,
@@ -124,7 +124,8 @@ async function runHeadlessPtySmoke(): Promise<string> {
describe.skipIf(process.platform === 'win32')('headless process shutdown (real Loader tree in a PTY)', () => {
it('lets a second Ctrl+C force exit while the first signal is draining', async () => {
const output = await runHeadlessPtySmoke()
expect(output).toContain('dsh: observing at ')
expect(output).not.toContain('dsh: observing at ')
expect(output).toContain('dsh-test: never-dispose ready')
expect(output).toContain('dsh-test: never-dispose started')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -20,35 +20,50 @@ afterEach(() => {
})
describe('process shutdown', () => {
it('exits once after graceful disposal resolves or rejects', async () => {
it('completes naturally after disposal resolves and forces exit when it rejects', async () => {
const resolvedExit = vi.fn()
const resolved = createProcessShutdown(() => Promise.resolve(), resolvedExit)
const resolvedComplete = vi.fn()
const resolved = createProcessShutdown(() => Promise.resolve(), resolvedExit, resolvedComplete)
await resolved.shutdown(0)
expect(resolvedExit).toHaveBeenCalledOnce()
expect(resolvedExit).toHaveBeenCalledWith(0)
expect(resolvedComplete).toHaveBeenCalledOnce()
expect(resolvedComplete).toHaveBeenCalledWith(0)
expect(resolvedExit).not.toHaveBeenCalled()
const rejectedExit = vi.fn()
const rejected = createProcessShutdown(() => Promise.reject(new Error('dispose failed')), rejectedExit)
const rejectedComplete = vi.fn()
const rejected = createProcessShutdown(
() => Promise.reject(new Error('dispose failed')),
rejectedExit,
rejectedComplete,
)
await rejected.shutdown(1)
expect(rejectedExit).toHaveBeenCalledOnce()
expect(rejectedExit).toHaveBeenCalledWith(1)
expect(rejectedComplete).not.toHaveBeenCalled()
})
it('uses process.exit as the default process boundary', async () => {
it('uses process.exitCode for default normal completion', async () => {
const exit = vi.spyOn(process, 'exit').mockImplementation(_code => undefined as never)
const originalExitCode = process.exitCode
process.exitCode = undefined
const shutdown = createProcessShutdown(() => Promise.resolve())
await shutdown.shutdown(7)
try {
await shutdown.shutdown(7)
expect(exit).toHaveBeenCalledOnce()
expect(exit).toHaveBeenCalledWith(7)
expect(process.exitCode).toBe(7)
expect(exit).not.toHaveBeenCalled()
} finally {
process.exitCode = originalExitCode
}
})
it('forces exit when graceful disposal reaches its bound', async () => {
vi.useFakeTimers()
const disposal = deferred()
const exit = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit)
const complete = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit, complete)
const pending = shutdown.shutdown(0)
await vi.advanceTimersByTimeAsync(PROCESS_SHUTDOWN_TIMEOUT_MS - 1)
@@ -60,13 +75,14 @@ describe('process shutdown', () => {
disposal.resolve()
await pending
expect(exit).toHaveBeenCalledOnce()
expect(complete).not.toHaveBeenCalled()
})
it('honors a caller-supplied grace period', async () => {
vi.useFakeTimers()
const disposal = deferred()
const exit = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit, 25)
const shutdown = createProcessShutdown(() => disposal.promise, exit, vi.fn(), 25)
const pending = shutdown.shutdown(0)
await vi.advanceTimersByTimeAsync(24)
@@ -81,7 +97,8 @@ describe('process shutdown', () => {
it('lets Ctrl+C force a normal shutdown already stuck in disposal', async () => {
const disposal = deferred()
const exit = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit)
const complete = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit, complete)
const pending = shutdown.shutdown(0)
shutdown.interrupt(130)
@@ -91,13 +108,29 @@ describe('process shutdown', () => {
disposal.resolve()
await pending
expect(exit).toHaveBeenCalledOnce()
expect(complete).not.toHaveBeenCalled()
})
it('forces exit after disposal started by a signal', async () => {
const disposal = deferred()
const exit = vi.fn()
const complete = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit, complete)
shutdown.interrupt(143)
disposal.resolve()
await shutdown.shutdown(0)
expect(exit).toHaveBeenCalledOnce()
expect(exit).toHaveBeenCalledWith(143)
expect(complete).not.toHaveBeenCalled()
})
it('drains on the first signal and forces on the second signal', async () => {
const disposal = deferred()
const dispose = vi.fn(() => disposal.promise)
const exit = vi.fn()
const shutdown = createProcessShutdown(dispose, exit)
const shutdown = createProcessShutdown(dispose, exit, vi.fn())
shutdown.interrupt(143)
await Promise.resolve()
@@ -116,7 +149,8 @@ describe('process shutdown', () => {
it('coalesces normal shutdown calls without treating them as escalation', async () => {
const disposal = deferred()
const exit = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit)
const complete = vi.fn()
const shutdown = createProcessShutdown(() => disposal.promise, exit, complete)
const first = shutdown.shutdown(0)
const second = shutdown.shutdown(1)
@@ -125,7 +159,21 @@ describe('process shutdown', () => {
disposal.resolve()
await first
expect(complete).toHaveBeenCalledOnce()
expect(complete).toHaveBeenCalledWith(0)
expect(exit).not.toHaveBeenCalled()
})
it('lets a signal force exit while natural completion drains remaining handles', async () => {
const exit = vi.fn()
const complete = vi.fn()
const shutdown = createProcessShutdown(() => Promise.resolve(), exit, complete)
await shutdown.shutdown(0)
shutdown.interrupt(130)
expect(complete).toHaveBeenCalledOnce()
expect(exit).toHaveBeenCalledOnce()
expect(exit).toHaveBeenCalledWith(0)
expect(exit).toHaveBeenCalledWith(130)
})
})

View File

@@ -0,0 +1,547 @@
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { Context } from 'cordis'
import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { PatchOptions } from '@cordisjs/plugin-include'
import { beforeAll, describe, expect, it } from 'vitest'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import { CallId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
const CONFIG_DIR = fileURLToPath(new URL('../config/', import.meta.url))
const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
/** The shipped Web surface: the dsh-base and dsh-web-app bundle patches over an empty preset root. */
const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
/**
* Boot the shipped Web composition, minus the rows that would bind a port,
* touch the network, or write outside the test. Everything that decides an
* agent's capabilities is the real thing, including both shipped presets.
*/
async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promise<Context> {
const storageRoot = join(dirname(settingsFile), 'storages')
const patches: PatchOptions[] = [
...loadOverlayPatches('dsh-test', BASE_PATCH),
...loadOverlayPatches('dsh-test', WEB_PATCH),
// The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
// reads the developer's own document — and since the default preset is a
// setting, a stored `agent-presets.default` would decide this file's
// outcome. Point it at a temp file for the same reason the roster below
// names only the shipped root.
{ id: 'settings', config: { path: settingsFile, watch: false } },
// storage-json's root is anchored to the real $DSH_HOME. Unpinned, this
// file writes the developer's own `~/.dsh/storages/` — and then reads it
// back on the next run, so a stored document from any other build decides
// this test's boot. Same reason the settings row above is pinned.
{ id: 'storage-json', config: { root: storageRoot } },
// Host rows with side effects outside this process: a bound port, a served
// asset tree, a telemetry exporter. `api-gateway` and `directory-picker`
// stay ENABLED on purpose — the api-proxy is the host row that injects
// `subagents`, `workspace`, and the rest of the agent plane, so disabling
// it would hide exactly the breakage this file exists to catch: a service
// moved into the presets that a host row still waits for. The boot audit
// is that assertion.
{ id: 'webserver', disabled: true },
// The web bundle's runtime row injects `httpServer`, so it cannot
// activate without the bound port disabled above. It owns dist serving
// and the URL prompt line — surface glue, not anything that decides an
// agent's capabilities, which is all this file asserts.
{ id: 'web-runtime', disabled: true },
{ id: 'telemetry-otel', disabled: true },
// A deployment-level skill on the host registry's GLOBAL layer — the same
// registration shape a repository plugin's skill root uses. The layered
// skills test below proves it reaches preset-composed agents.
{ id: 'skill-badge', disabled: false },
{ id: 'modules', disabled: true },
{ id: 'connection', disabled: true },
// The shipped `-auto` chooser resolves its interaction from a running
// host and so waits for the webserver disabled above; the browse variant
// supplies `directoryPicker` without one.
{ id: 'directory-picker', disabled: true },
{ insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
// The roster AppCLIEntry would patch in; only the shipped root, so a
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
// `default` here is the COMPOSITION default — the base layer the settings
// document overrides.
{
id: 'agent-presets',
config: { default: 'standard', roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }] },
},
...extra,
]
// The surface is patch layers over an empty preset root, so the root sits
// outside this workspace and bare plugin names cannot resolve by Node's
// upward walk. The flat fallback the preset boot maintains is what makes
// them resolvable — the same mechanism, not a test-only shim.
const home = dirname(settingsFile)
healProfilesModuleFallback(INSTALL_ANCHOR, home)
const profileDir = join(home, 'profiles', 'spec')
await mkdir(profileDir, { recursive: true })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
return await boot('dsh-test', rootConfig, patches)
}
const toolNames = (ctx: Context, agent?: Agent): string[] =>
ctx.tools.schemas(agent).map(schema => schema.name).sort()
let ctx: Context
beforeAll(async () => {
const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-web-presets-')), 'settings.yaml')
await writeFile(settingsFile, '{}\n')
ctx = await bootWeb(settingsFile)
}, 120_000)
describe('the shipped Web composition', () => {
it('leaves the global tool layer empty', () => {
// Every model-facing tool belongs to a preset, `ask_user_question`
// included: a tool in the global layer reaches EVERY agent regardless of
// which preset composed it, so a two-tool benchmark surface would really
// present three. A regression here means an agent-plane row came back to
// the host composition.
expect(toolNames(ctx)).toEqual([])
})
it('supplies both shipped presets, and only those, from the system root', async () => {
const listed = await ctx.agentPresets.list()
expect(listed.map(preset => preset.id).sort()).toEqual(['code', 'cordis', 'minimal', 'standard'])
expect(listed.every(preset => preset.trust === 'system')).toBe(true)
expect(ctx.agentPresets.defaultId).toBe('standard')
})
it('composes the full agent from `standard`', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-standard'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The EXACT catalog, not a spot-check: an omission is this design's
// quietest failure mode, because a row that registers into the wrong
// layer mounts cleanly and simply contributes nothing. `glob`/`grep` are
// excluded for the reason the TUI composition e2e excludes them — they
// depend on ripgrep being present on the machine.
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'send_message', 'skill',
'str_replace_editor', 'subagent', 'subagent_fork', 'task_kill',
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
'workflow', 'write',
])
} finally {
await handle.dispose()
}
})
it('composes exactly two tools from `minimal`', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// Exactly what the preset lists — nothing arrives from the host.
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
} finally {
await handle.dispose()
}
})
it('keeps two differently composed sessions independent', async () => {
const full = await ctx.agents.create({
sessionId: SessionId('preset-both-full'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
const minimal = await ctx.agents.create({
sessionId: SessionId('preset-both-minimal'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'str_replace_editor'])
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
await minimal.dispose()
// Tearing the minimal session down leaves the full one whole.
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
expect(toolNames(ctx)).toEqual([])
} finally {
await full.dispose()
}
})
it('composes the cordis agent with its own toolset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'cordis').then(() => undefined),
})
try {
const tools = toolNames(ctx, handle.agent)
// The self-referential toolset is what distinguishes this preset.
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
// The preset's own authoring skill registers into ITS layer of the host
// registry: the cordis agent's view carries it, the global view does not.
const scoped = (await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('editing-cordis-compositions')
expect((await ctx.skills.list()).map(skill => skill.name)).not.toContain('editing-cordis-compositions')
} finally {
await handle.dispose()
}
})
it('presents `code` as Code Mode without disturbing a native session beside it', async () => {
const coded = await ctx.agents.create({
sessionId: SessionId('preset-code'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'code').then(() => undefined),
})
const native = await ctx.agents.create({
sessionId: SessionId('preset-code-native'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// One tool reaches the MODEL: the transport. The registry's catalog for
// this agent is unchanged — a code mode collapses the presentation, not
// the capabilities — so the assembly is what carries the claim.
const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
expect(toolNames(ctx, coded.agent)).toContain('str_replace_editor')
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain('str_replace_editor')
expect(sdk).toContain('web_search')
// The presentation is this agent's alone: the deployment default is
// native, and the session composed from `standard` still sees it.
const nativeAssembly = await ctx.systemPrompt.assemble({ scope: native.agent })
expect(nativeAssembly.tools.map(tool => tool.name)).toContain('bash')
expect(nativeAssembly.tools.map(tool => tool.name)).not.toContain('run_code')
expect(nativeAssembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
} finally {
await native.dispose()
await coded.dispose()
}
})
it('keeps the self-referential toolset out of every other preset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-no-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// Editing the live runtime is opt-in per session, not ambient.
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_mount')
} finally {
await handle.dispose()
}
})
it('ships the composition-authoring skill inside the preset directory', async () => {
// The preset's skill root is derived from its own `baseUrl`, so the skill
// travels with the directory wherever the preset is installed.
const skill = join(
CONFIG_DIR, 'agent-presets', 'cordis', 'skills', 'editing-cordis-compositions', 'SKILL.md',
)
expect((await readFile(skill, 'utf8')).startsWith('---\nname: editing-cordis-compositions')).toBe(true)
})
it('merges the global skill layer into a preset agent\'s catalog, keeping local discovery preset-side', async () => {
const proj = await mkdtemp(join(tmpdir(), 'dsh-preset-skill-proj-'))
await mkdir(join(proj, '.dsh', 'skills', 'project-proof'), { recursive: true })
await writeFile(join(proj, '.dsh', 'skills', 'project-proof', 'SKILL.md'), [
'---',
'name: project-proof',
'description: Proves the preset layer discovers project skills beside global ones.',
'---',
'',
'Project proof body.',
'',
].join('\n'))
const handle = await ctx.agents.create({
// Unique per run: the composition persists into the ambient DSH home,
// and a fixed id would collide with a log an earlier run left there.
sessionId: SessionId(`preset-skills-standard-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The host (global) view carries the deployment-level provider alone:
// local discovery moved behind the presets with `skill-local`.
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
// The standard agent's view merges the global layer with its preset's
// own local discovery over the session cwd.
const scoped = (await ctx.skills.list({ cwd: proj, scope: handle.agent })).map(skill => skill.name)
expect(scoped).toContain('dsh-badge')
expect(scoped).toContain('project-proof')
// The preset's own loader tool resolves the global-layer skill.
const loaded = await ctx.tools.execute({
callId: CallId('preset-skills-load'),
name: 'skill',
arguments: { name: 'dsh-badge' },
signal: new AbortController().signal,
agent: handle.agent,
})
expect(loaded.isError).toBe(false)
expect(JSON.stringify(loaded.content)).toContain('powered by dsh')
} finally {
await handle.dispose()
}
})
it('shows a minimal agent the global layer but no loader tool', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId(`preset-skills-minimal-${randomUUID()}`),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// Layer visibility is the registry's; whether an agent can USE skills
// stays the preset's choice — minimal mounts no `tool-skill`, so its
// tool table has no loader even though the global layer is readable.
expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
} finally {
await handle.dispose()
}
})
it('never rewrites the preset file it composed from', async () => {
// The Loader persists a tree whose plugin self-disposed, and tearing an
// agent down disposes its whole subtree. Inherited, that rewrote the
// shipped composition — truncating it to `[]` the first time a session
// ended — so `PresetTree` refuses to write at all.
const path = join(CONFIG_DIR, 'agent-presets', 'standard', 'agent.cordis.yml')
const before = await readFile(path, 'utf8')
const handle = await ctx.agents.create({
sessionId: SessionId('preset-readonly'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
await handle.dispose()
// Slack, not a race the number has to win. The write is driven by the
// Loader's fiber-unload listener, which fires as the subtree's fibers
// settle rather than when `dispose()` resolves, and the Loader exposes no
// flush to await. A regression writes synchronously inside that listener,
// so any wait past settlement fails; a longer one only slows the test.
await new Promise(resolve => setTimeout(resolve, 50))
expect(await readFile(path, 'utf8')).toBe(before)
})
it('gives each session its own persona', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-persona'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
expect(assembly.sections.find(section => section.name === 'deployment:persona')?.text)
.toContain('You are a coding agent powered by')
} finally {
await handle.dispose()
}
})
})
describe('a switch survives the session', () => {
it('records the choice so the log states what the agent runs', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-switch-logged'),
meta: { agentPreset: 'standard' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// The api-proxy's select does exactly this pair while the session is blank.
await ctx.agentPresets.recompose(handle.agent.ctx, 'minimal')
handle.agent.session.append('agent-preset/selected', { agentPreset: 'minimal' })
// The header keeps the creation fact; the log carries what it runs.
expect(handle.agent.session.header.agentPreset).toBe('standard')
expect(resolveSessionPreset(handle.agent.session)).toBe('minimal')
} finally {
await handle.dispose()
}
})
it('rebuilds a switched session from the log, not the creation header', () => {
// The exact shape a resume reads back from disk: the header says standard,
// the log records the switch the user made while the session was blank.
const rebuilt = resolveSessionPreset({
header: { version: 0, id: SessionId('x'), createdAt: 0, agentPreset: 'standard' },
events: [
{ type: 'agent-preset/selected', seq: 1, time: 0, data: { agentPreset: 'minimal' } },
{ type: 'turn/start', seq: 2, time: 0, data: { turn: 0, trigger: { kind: 'message', source: { kind: 'user' } } } },
] as never,
})
// Reading the header alone would compose the creation-time preset over a
// history another one produced — the replay the blank-only lock prevents.
expect(rebuilt).toBe('minimal')
})
})
describe('a forked session', () => {
it('inherits the composition its seeded history was produced under', async () => {
const parent = await ctx.agents.create({
sessionId: SessionId('preset-fork-parent'),
meta: { agentPreset: 'minimal' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
const inherited = resolveSessionPreset(parent.agent.session)
const child = await ctx.agents.create({
sessionId: SessionId('preset-fork-child'),
meta: {
parentSession: SessionId('preset-fork-parent'),
seedLength: 0,
...inherited === undefined ? {} : { agentPreset: inherited },
},
setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
})
try {
// Composing nothing would leave the child empty: this layer moved every
// model-facing row out of the host plane, so there is nothing to inherit
// for free any more.
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
expect(toolNames(ctx, child.agent).length).toBeGreaterThan(0)
} finally {
await child.dispose()
await parent.dispose()
}
})
})
describe('authoring a preset on the shipped composition', () => {
let authorCtx: Context
let userRoot: string
beforeAll(async () => {
userRoot = join(await mkdtemp(join(tmpdir(), 'dsh-preset-authoring-')), 'profiles')
const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-preset-authoring-settings-')), 'settings.yaml')
await writeFile(settingsFile, '{}\n')
authorCtx = await bootWeb(settingsFile, [{
id: 'agent-presets',
config: {
default: 'standard',
roots: [
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
// The root does not exist yet: a deployment whose user has authored
// nothing is the normal first-run state.
{ path: userRoot, trust: 'user' },
],
},
}])
})
it('refuses to copy over or delete a shipped preset', async () => {
await expect(authorCtx.agentPresets.copy('minimal', 'standard')).rejects.toThrow(/already exists/)
await expect(authorCtx.agentPresets.remove('standard')).rejects.toThrow(/ships with the deployment/)
})
it.each(['../escape', 'a/b', '/abs', 'Upper'])('refuses the uncontainable id %j', async (id) => {
// The id becomes a directory name under the user root, so containment is
// checked on the id rather than on the joined path afterwards.
await expect(authorCtx.agentPresets.copy('minimal', id)).rejects.toThrow()
})
it('copies a shipped preset a session then really composes from', async () => {
await authorCtx.agentPresets.copy('minimal', 'my-agent', '我的模式')
// Round-trips through the roster as a `user` row carrying the given name
// and the source's description, over the source's own composition text.
const preset = await authorCtx.agentPresets.resolve('my-agent')
const source = await authorCtx.agentPresets.resolve('minimal')
expect(preset.trust).toBe('user')
expect(preset.name).toBe('我的模式')
expect(preset.description).toBe(source.description)
expect(await authorCtx.agentPresets.read('my-agent')).toBe(await authorCtx.agentPresets.read('minimal'))
// Owner-only, in an owner-only directory: a composition is executable
// configuration on a machine that may have other users.
expect((await stat(preset.path)).mode & 0o777).toBe(0o600)
const handle = await authorCtx.agents.create({
sessionId: SessionId('preset-authored'),
setup: agentCtx => authorCtx.agentPresets.mount(agentCtx, 'my-agent').then(() => undefined),
})
try {
// The same tools the shipped `minimal` composes, from a directory copied
// through the service into a root outside the installed harness.
expect(toolNames(authorCtx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
} finally {
await handle.dispose()
}
})
it('deletes what it copied', async () => {
await authorCtx.agentPresets.copy('minimal', 'doomed')
await authorCtx.agentPresets.remove('doomed')
expect((await authorCtx.agentPresets.list()).map(preset => preset.id)).not.toContain('doomed')
})
})
/**
* Which preset an unnamed session gets is a user setting layered over the
* composition's own default. The package suite proves the layering against a
* hand-built context; this proves it through the shipped `cordis.yml` — that
* the roster and the settings provider are actually wired to each other, and
* that the id the setting names is the one a session composes from.
*/
describe('the default preset as a user setting', () => {
it('composes an unnamed session from the stored default, not the composed one', async () => {
expect(ctx.agentPresets.defaultId).toBe('standard')
await ctx.settings.update(settingsNamespace(SETTINGS_NAMESPACE), { default: 'minimal' })
try {
expect(ctx.agentPresets.defaultId).toBe('minimal')
const handle = await ctx.agents.create({
sessionId: SessionId('preset-user-default'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
// `mount()` with no id resolves the effective default. Two tools, not
// `standard`'s catalog: the setting decided the composition.
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
} finally {
await handle.dispose()
}
} finally {
// The context is shared with the rest of the file. `replace({})` drops
// the user section wholesale so the field re-inherits the composition
// base; `update` merges, and would leave the override standing.
await ctx.settings.replace(settingsNamespace(SETTINGS_NAMESPACE), {})
}
expect(ctx.agentPresets.defaultId).toBe('standard')
})
})
describe('a session keeps the preset it was created with', () => {
it('refuses to adopt a live session under a different preset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-locked'),
meta: { agentPreset: 'minimal' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
// The api-proxy guard reads exactly this: the header records what the
// session runs, so naming anything else is a caller error rather than a
// switch. Its history was produced under `minimal`'s two tools.
expect(handle.agent.session.header.agentPreset).toBe('minimal')
} finally {
await handle.dispose()
}
})
})

View File

@@ -0,0 +1,139 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
import {
BASE_BUNDLE,
resolveWindowsShellLayer,
WINDOWS_SHELL_PATCH_FILENAME,
} from '../src/windows-shell.ts'
const WINDOWS_PATCH = `- id: bash-sandbox
disabled: true
- insert:
- id: pwsh-sandbox
name: '@deepseek-ai/dsh-pwsh-sandbox'
`
/** One fake bundle layer rooted in a temp directory. */
function fakeLayer(packageName: string, dir: string): ProfileLayer {
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
}
/** A base bundle layer whose package carries the Windows shell patch. */
function baseLayerWithPatch(dir: string): ProfileLayer {
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
return fakeLayer(BASE_BUNDLE, dir)
}
describe('resolveWindowsShellLayer', () => {
let base: string
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
const tempBase = (): string => {
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
return base
}
it('never applies on POSIX hosts', () => {
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
})
it('defaults Windows hosts to the pwsh platform layer', () => {
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
expect(layer).toBeDefined()
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
expect(layer?.patches).toEqual([
{ id: 'bash-sandbox', disabled: true },
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
])
})
it('skips custom profiles without a base bundle', () => {
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
})
it('fails loud when the base bundle ships no Windows shell patch', () => {
const base = tempBase()
mkdirSync(base, { recursive: true })
// The overlay loader owns the fail-loud contract: the caller named this
// file, so its absence is a misconfiguration, not "no overlay".
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
})
})
describe('the shipped Windows composition (real bundle layers)', () => {
let home: string
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
// resolve from the REAL dsh-base/dsh-web-app packages through it, so this
// suite composes the shipped patch files, not test fixtures.
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
it('composes the win32 confined roster through the real patch layers', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
const warnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
expect(win32).toBeDefined()
const rows = composeEntries(
[...profile.layers.map(layer => layer.patches), win32!.patches],
message => warnings.push(message),
)
const byId = new Map(rows.map(row => [row.id, row]))
// Only the POSIX bash stack leaves the roster: the permission surface
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
// exactly as on POSIX — the confined pwsh executor is what changes.
for (const id of ['bash-sandbox', 'tool-bash']) {
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
}
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
}
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
expect(byId.has(id), `inserted row ${id}`).toBe(true)
}
// The launcher's cold-start module fallback BFS-links the apps/cli
// dependency closure into the profile's node_modules (the pwsh-local
// precedent), so every inserted bare plugin must resolve from there.
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
}
// The patch touches only base-owned rows plus inserts, so the full web
// profile composes without any no-match warning.
expect(warnings).toEqual([])
})
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
// POSIX: no platform layer, the bash stack stays enabled.
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
const posixById = new Map(posixRows.map(row => [row.id, row]))
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
expect(posixById.has('pwsh-local')).toBe(false)
expect(posixById.has('pwsh-sandbox')).toBe(false)
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
// inserts, so the composition produces no no-match warning.
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
const baseWarnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
expect(win32).toBeDefined()
composeEntries(
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
message => baseWarnings.push(message),
)
expect(baseWarnings).toEqual([])
})
})