Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
This commit is contained in:
@@ -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', '--workspace-root', '/w']))
|
||||
@@ -65,6 +69,13 @@ describe('parseDshArgs', () => {
|
||||
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', '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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -182,14 +182,56 @@ 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']]) {
|
||||
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
|
||||
const result = await runBuiltBin(removed)
|
||||
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).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+$/u)
|
||||
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')
|
||||
|
||||
176
apps/cli/tests/dsh-badge.snapshot.ts
Normal file
176
apps/cli/tests/dsh-badge.snapshot.ts
Normal 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://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
|
||||
\`\`\`markdown
|
||||

|
||||
\`\`\`
|
||||
|
||||
## 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://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
|
||||
\`\`\`markdown
|
||||

|
||||
\`\`\`
|
||||
|
||||
## 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)
|
||||
})
|
||||
9
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
Normal file
9
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
- id: skill-badge
|
||||
disabled: false
|
||||
|
||||
- id: skill-local
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
disabled: true
|
||||
6
apps/cli/tests/fixtures/dsh-badge/default.cordis.yml
vendored
Normal file
6
apps/cli/tests/fixtures/dsh-badge/default.cordis.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
- id: skill-local
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
disabled: true
|
||||
56
apps/cli/tests/fixtures/dsh-badge/snapshot.ts
vendored
Normal file
56
apps/cli/tests/fixtures/dsh-badge/snapshot.ts
vendored
Normal 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()
|
||||
}
|
||||
@@ -66,7 +66,7 @@ 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({
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user