From be3e4ac455bd1760d873d530c09d0796a89b6abf Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:06:01 +0800 Subject: [PATCH] fix(test): address cli demo cleanup review feedback --- ...-20-remove-stdio-and-echo-agents.i18n.yaml | 4 +- ...2026-07-20-remove-stdio-and-echo-agents.md | 4 +- ...6-07-20-remove-stdio-and-echo-agents.zh.md | 4 +- .../2026-08-08-remove-cli-demo.i18n.yaml | 4 +- .../2026-08-08-remove-cli-demo.md | 2 +- .../2026-08-08-remove-cli-demo.zh.md | 2 +- docs/event-producer-consumer.md | 2 +- docs/user/guide/quickstart.i18n.yaml | 4 +- docs/user/guide/quickstart.md | 4 +- docs/user/guide/quickstart.zh.md | 4 +- .../fixtures/subagent/subagent-acp/driver.ts | 2 +- .../tests/fixtures/headless-driver.ts | 4 +- .../tests/fixtures/telemetry-otel-driver.ts | 2 +- .../tests/fixtures/time-context-driver.ts | 2 +- .../subagent/subagent-dsh-sdk/driver.ts | 2 +- examples/package.json | 1 + packages/examples/README.i18n.yaml | 4 +- packages/examples/README.md | 2 +- packages/examples/README.zh.md | 2 +- .../support/loader-smoke/README.i18n.yaml | 4 +- packages/support/loader-smoke/README.md | 6 +- packages/support/loader-smoke/README.zh.md | 6 +- packages/support/loader-smoke/package.json | 8 +- .../support/loader-smoke/src/agent-turn.ts | 7 +- packages/support/loader-smoke/src/index.ts | 6 + .../loader-smoke/tests/agent-turn.spec.ts | 160 ++++++++++++++++++ packages/support/loader-smoke/tsconfig.json | 9 + pnpm-lock.yaml | 12 ++ .../verify-package-readme-model-experience.ts | 2 +- 29 files changed, 238 insertions(+), 37 deletions(-) rename examples/headless-agent/tests/fixtures/one-shot.ts => packages/support/loader-smoke/src/agent-turn.ts (93%) create mode 100644 packages/support/loader-smoke/tests/agent-turn.spec.ts diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml index 51e416d9f8..a21ac5cfe9 100644 --- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md -2026-07-20-remove-stdio-and-echo-agents.md: 4ffeacae4afc212b0d7b739def2c0e96780d5e54 -2026-07-20-remove-stdio-and-echo-agents.zh.md: 462dc275f151dbeb41bf431fc2ee583e74f22a6b +2026-07-20-remove-stdio-and-echo-agents.md: 256e626f4ff41016d0227eef7cc3e4e51e15058b +2026-07-20-remove-stdio-and-echo-agents.zh.md: bb7a2abd8be73ac3ea089121592b3f26162bf619 diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md index 4ffeacae4a..256e626f4f 100644 --- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md +++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md @@ -24,13 +24,13 @@ The remaining application roles are explicit: The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents. -Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the `dsh` built-bin suite pins one-shot output, persistence, failure, and signal semantics, and package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent. +Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the `dsh` built-bin suite pins the published one-shot entry and output, the product Headless snapshot pins persistence, and the Headless PTY shutdown e2e pins signal escalation. Package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent. ## Verification TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references. -The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh run`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot path under plain Node, including output, persistence, invalid arguments, missing configuration, and signals. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior. +The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh run`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot entry under plain Node, including output and invalid arguments. `examples/headless-agent/tests/headless.snapshot.ts` pins product persistence, while `apps/cli/tests/headless-shutdown.e2e.ts` owns bounded signal escalation. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior. ## Alternatives considered diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md index 462dc275f1..bb7a2abd8b 100644 --- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md @@ -24,13 +24,13 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个 SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。 -无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;`dsh` built-bin 测试套件固定单次运行的输出、持久化、失败和信号语义;各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。 +无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;`dsh` built-bin 测试套件固定已发布的一次性入口和输出;产品 Headless 快照固定持久化;Headless PTY 关闭 e2e 固定信号升级。各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。 ## 验证 TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。 -构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh run`;`apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性路径,包括输出、持久化、无效参数、缺失配置和信号。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)契约。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。 +构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh run`;`apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性入口,包括输出和无效参数。`examples/headless-agent/tests/headless.snapshot.ts` 固定产品持久化,`apps/cli/tests/headless-shutdown.e2e.ts` 则负责有界信号升级。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)契约。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml index ae6e47b2c1..cd43c3a27b 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md -2026-08-08-remove-cli-demo.md: 5875f47f2d2463fd6f82f66df5ca08fef8e06aed -2026-08-08-remove-cli-demo.zh.md: 1bb9e2c8860f57e1170527cee0cbe694af9990f6 +2026-08-08-remove-cli-demo.md: c1153f5e9fcc89585e926f8f088829c849d1f6c1 +2026-08-08-remove-cli-demo.zh.md: 1f9719b176c05009900a268a5bb2d2d7fcc79df3 diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md index 5875f47f2d..c1153f5e9f 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md +++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md @@ -14,7 +14,7 @@ The replay suites still need canonical session events to pin assembled backend b Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. The root `demo:headless` script is retained only as a direct alias of `dsh run`; the product command owns final-text stdout, the observation URL on stderr, persistence, exit status, and shutdown. -`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. An unexported example-owned TypeScript fixture drives a task and emits canonical events as JSONL for replay snapshots. It is launched only by tests, has no package export or bin, and is not a supported product output format. +`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. The support-tier `@deepseek-ai/dsh-loader-smoke` package owns the shared direct-agent turn helper; unexported example-local drivers select their Loader configuration and render canonical events as JSONL. They are launched only by tests, have no bin, and do not define a supported product output format. ## Alternatives considered diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md index 1bb9e2c886..1f9719b176 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md @@ -14,7 +14,7 @@ Status: implemented 彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。根目录的 `demo:headless` 脚本仅作为 `dsh run` 的直接别名保留;stdout 上的最终文本、stderr 上的观察 URL、持久化、退出状态和关闭行为均由产品命令负责。 -`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。一个由示例自有且未导出的 TypeScript fixture(测试前置数据)会驱动任务,并以 JSONL 发出供回放快照使用的规范事件。该 fixture 只由测试启动,没有包导出或 bin,也不是受支持的产品输出格式。 +`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。支持层的 `@deepseek-ai/dsh-loader-smoke` 包负责共享的直接 agent 轮次 helper;未导出的示例本地 driver 选择各自的 Loader 配置,并将规范事件渲染为 JSONL。这些 driver 只由测试启动,不提供 bin,也不定义受支持的产品输出格式。 ## 考虑过的替代方案 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index b4e5fa1795..13e96e9dd8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -32,7 +32,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) | | `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`loader-smoke`](../packages/support/loader-smoke), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | diff --git a/docs/user/guide/quickstart.i18n.yaml b/docs/user/guide/quickstart.i18n.yaml index b5ab33db5f..dfdba88bec 100644 --- a/docs/user/guide/quickstart.i18n.yaml +++ b/docs/user/guide/quickstart.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md -quickstart.md: 2f86ce4cb7896ca75b7457186d4633066a899b83 -quickstart.zh.md: f7ddf88c74b5a32daa1d2b81feeaed192e7a3f9c +quickstart.md: 9d1a04f0a094919bb008a7e94564a6612e10732d +quickstart.zh.md: 7182f63586f4f08524c643a7c6c1ad599b2de098 diff --git a/docs/user/guide/quickstart.md b/docs/user/guide/quickstart.md index 2f86ce4cb7..9d1a04f0a0 100644 --- a/docs/user/guide/quickstart.md +++ b/docs/user/guide/quickstart.md @@ -22,6 +22,7 @@ pnpm -v git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git cd deepseek-harness pnpm install +pnpm run build ``` Create the gitignored repository-root `.env`: @@ -42,10 +43,9 @@ pnpm run dsh run "summarize the architecture of this workspace" ## Step 3: use the Web UI -Build and start the browser interface: +Start the browser interface: ```sh -pnpm run build pnpm run dsh web ``` diff --git a/docs/user/guide/quickstart.zh.md b/docs/user/guide/quickstart.zh.md index f7ddf88c74..7182f63586 100644 --- a/docs/user/guide/quickstart.zh.md +++ b/docs/user/guide/quickstart.zh.md @@ -22,6 +22,7 @@ pnpm -v git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git cd deepseek-harness pnpm install +pnpm run build ``` 在仓库根目录创建已被 Git 忽略的 `.env`: @@ -42,10 +43,9 @@ pnpm run dsh run "summarize the architecture of this workspace" ## 第三步:使用 Web UI -构建并启动浏览器界面: +启动浏览器界面: ```sh -pnpm run build pnpm run dsh web ``` diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts index 4436d52b24..18f7691d55 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts @@ -2,7 +2,7 @@ /** Test driver: one delegation turn through a headless Loader composition. */ import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' -import { runFixtureTurn } from '../../../../../headless-agent/tests/fixtures/one-shot.ts' +import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke' const configPath = process.argv[2] if (configPath === undefined) throw new Error('acp-subagent cwd driver requires a config path') diff --git a/examples/headless-agent/tests/fixtures/headless-driver.ts b/examples/headless-agent/tests/fixtures/headless-driver.ts index d9a30afa83..88d7d73b67 100644 --- a/examples/headless-agent/tests/fixtures/headless-driver.ts +++ b/examples/headless-agent/tests/fixtures/headless-driver.ts @@ -3,8 +3,8 @@ import type { Context } from 'cordis' import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' +import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke' import type { SessionEvent } from '@deepseek-ai/dsh-session' -import { runFixtureTurn } from './one-shot.ts' const NAME = 'headless-test-driver' const [configPath, ...taskParts] = process.argv.slice(2) @@ -16,7 +16,7 @@ const uninstallFailLoud = installFailLoud(NAME) let ctx: Context | undefined try { loadEnv(NAME) - ctx = await boot(NAME, resolveConfigPath(configPath, process.env.DSH_SNAPSHOT)) + ctx = await boot(NAME, resolveConfigPath(configPath, undefined)) const result = await runFixtureTurn(ctx, { task: taskParts.join(' '), onEvent: (sessionId: string, event: SessionEvent) => { diff --git a/examples/headless-agent/tests/fixtures/telemetry-otel-driver.ts b/examples/headless-agent/tests/fixtures/telemetry-otel-driver.ts index f7e1001628..afb00b0eca 100644 --- a/examples/headless-agent/tests/fixtures/telemetry-otel-driver.ts +++ b/examples/headless-agent/tests/fixtures/telemetry-otel-driver.ts @@ -11,7 +11,7 @@ import { createServer } from 'node:http' import { once } from 'node:events' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import { recordFeedback } from '@deepseek-ai/dsh-command-feedback' -import { runFixtureTurn } from './one-shot.ts' +import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke' const configPath = process.argv[2] if (configPath === undefined) throw new Error('telemetry-otel driver requires a config path') diff --git a/examples/headless-agent/tests/fixtures/time-context-driver.ts b/examples/headless-agent/tests/fixtures/time-context-driver.ts index 843bbdc2ea..00479b4221 100644 --- a/examples/headless-agent/tests/fixtures/time-context-driver.ts +++ b/examples/headless-agent/tests/fixtures/time-context-driver.ts @@ -2,7 +2,7 @@ /** Test driver that sends two turns through one Headless Loader composition. */ import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' -import { runFixtureTurn } from './one-shot.ts' +import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke' const configPath = process.argv[2] if (configPath === undefined) throw new Error('time-context driver requires a config path') diff --git a/examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts b/examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts index 4b52a0f929..7ed378b8b5 100644 --- a/examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts +++ b/examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts @@ -2,7 +2,7 @@ /** Test driver: one delegation turn through a headless Loader composition. */ import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' -import { runFixtureTurn } from '../../../../../headless-agent/tests/fixtures/one-shot.ts' +import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke' const configPath = process.argv[2] if (configPath === undefined) throw new Error('sdk-subagent cwd driver requires a config path') diff --git a/examples/package.json b/examples/package.json index 40628f0255..1f6e99d240 100644 --- a/examples/package.json +++ b/examples/package.json @@ -39,6 +39,7 @@ "@deepseek-ai/dsh-llm-deepseek": "workspace:*", "@deepseek-ai/dsh-llm-pi-ai": "workspace:*", "@deepseek-ai/dsh-llm-replay": "workspace:*", + "@deepseek-ai/dsh-loader-smoke": "workspace:*", "@deepseek-ai/dsh-lsp": "workspace:*", "@deepseek-ai/dsh-lsp-local": "workspace:*", "@deepseek-ai/dsh-permission": "workspace:*", diff --git a/packages/examples/README.i18n.yaml b/packages/examples/README.i18n.yaml index e6a48a3a7d..506e867ef5 100644 --- a/packages/examples/README.i18n.yaml +++ b/packages/examples/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/examples/README.md -README.md: 36d1c0d0ddb7a3840af10e6a69ea407d3471c661 -README.zh.md: 9886abe8eedc27a6c62728b93fd63748b2c7dea6 +README.md: 2d672dcc307bb280cf3803f29128eba4988a8da0 +README.zh.md: e827e7cff4ff9d6521e5889e48270e06641ef38c diff --git a/packages/examples/README.md b/packages/examples/README.md index 36d1c0d0dd..2d672dcc30 100644 --- a/packages/examples/README.md +++ b/packages/examples/README.md @@ -10,7 +10,7 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling | [`acp-demo/`](acp-demo/README.md) | `@deepseek-ai/dsh-acp-demo` | ACP automation application bundle | | [`jsonrpc-demo/`](jsonrpc-demo/README.md) | `@deepseek-ai/dsh-jsonrpc-demo` | External-config JSON-RPC runtime | -`agent-spine-demo` is the shared bundle; `acp-demo` adds its automation front door, while `jsonrpc-demo` boots a deployment-owned plugin tree. Product one-shot execution belongs to `dsh run` rather than a package in this directory. +`agent-spine-demo` is the shared bundle; `acp-demo` adds its automation front door, while `jsonrpc-demo` boots a deployment-owned plugin tree. Product one-shot execution belongs to `dsh run`; no package in this directory provides it. These packages are not product API. Product seams and front doors remain in their owning groups; demo bundles select concrete compositions. diff --git a/packages/examples/README.zh.md b/packages/examples/README.zh.md index 9886abe8ee..e827e7cff4 100644 --- a/packages/examples/README.zh.md +++ b/packages/examples/README.zh.md @@ -10,7 +10,7 @@ | [`acp-demo/`](acp-demo/README.md) | `@deepseek-ai/dsh-acp-demo` | ACP 自动化应用组合包 | | [`jsonrpc-demo/`](jsonrpc-demo/README.md) | `@deepseek-ai/dsh-jsonrpc-demo` | 外部配置 JSON-RPC 运行时 | -`agent-spine-demo` 是共享组合包;`acp-demo` 添加自动化入口,`jsonrpc-demo` 则启动由部署方拥有的插件树。产品单次执行归 `dsh run` 所有,而不再由本目录中的 package 提供。 +`agent-spine-demo` 是共享组合包;`acp-demo` 添加自动化入口,`jsonrpc-demo` 则启动由部署方拥有的插件树。产品单次执行归 `dsh run` 所有;本目录没有任何包提供该功能。 这些包不是产品 API。产品 seam 与前端入口仍位于各自的归属组;演示组合包只选择具体组合。 diff --git a/packages/support/loader-smoke/README.i18n.yaml b/packages/support/loader-smoke/README.i18n.yaml index 2c99378710..4e49d185bc 100644 --- a/packages/support/loader-smoke/README.i18n.yaml +++ b/packages/support/loader-smoke/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/support/loader-smoke/README.md -README.md: 73610ce50ebac4c6fc7bb9135f7b41b347c60685 -README.zh.md: 9254cd1592d4bf3f119d350f04165228db87b1cf +README.md: e5a33beb95f4e5940364cf309c8ea5fea60686f1 +README.zh.md: 1f3c6235175b3cfdcefec111eac39b2361b0e05b diff --git a/packages/support/loader-smoke/README.md b/packages/support/loader-smoke/README.md index 73610ce50e..e5a33beb95 100644 --- a/packages/support/loader-smoke/README.md +++ b/packages/support/loader-smoke/README.md @@ -6,15 +6,17 @@ Shared subprocess harness for tests that boot an app and `cordis.yml` through th `runLoaderSmoke` accepts bin and config paths, optional complete bin arguments, environment overrides, stdin, pre-run setup, and pre-cleanup inspection. It owns the isolated cwd, DSH homes, diagnostics, deadline, termination, EOF, and cleanup; it returns both streams after a zero exit and rejects with both streams on failure. +`runFixtureTurn` drives one task through exactly one configured root agent, forwards canonical events after that task reaches the durable inbox, flushes the session, and returns the final assistant text plus accumulated usage. Example-local drivers retain configuration, rendering, and assertion ownership. + This is support-tier test infrastructure, not product API. ## Model Experience -None, as this test-only harness boots example processes and inspects their streams without changing an assembled model request. +None, as the test harness submits only the consuming test's ordinary user task and delegates prompt and tool composition to the loaded tree. #### KV Cache effect -None; this package neither assembles nor sends a provider request. +None beyond the loaded tree; the helper neither changes the request prefix nor retains state across runs. ## Known Limitations and Deferred Work diff --git a/packages/support/loader-smoke/README.zh.md b/packages/support/loader-smoke/README.zh.md index 9254cd1592..1f3c623517 100644 --- a/packages/support/loader-smoke/README.zh.md +++ b/packages/support/loader-smoke/README.zh.md @@ -6,15 +6,17 @@ `runLoaderSmoke` 接受可执行文件路径和配置路径、可选的完整可执行文件参数、环境变量覆盖、标准输入、运行前准备和清理前检查。它负责隔离工作目录、DSH 主目录、诊断、截止时间、终止、EOF 和清理;进程以零状态退出后返回两个流,失败时则返回拒绝并附带两个流。 +`runFixtureTurn` 通过恰好一个已配置的根 agent(智能体)驱动一项任务,在该任务进入持久收件箱后转发规范事件,刷写会话,并返回最终 assistant 文本和累计用量。示例本地 driver 继续负责配置、渲染和断言。 + 这是支持层测试基础设施,而非产品 API。 ## 模型体验 -无。该测试专用 harness 启动示例进程并检查它们的流,不会改变组装后的模型请求。 +无,因为测试 harness 仅提交调用方测试的普通用户任务,并将提示词和工具组装交由已加载的插件树负责。 #### KV Cache 影响 -无;该包既不组装也不发送提供方请求。 +除已加载树本身的影响外,无其他影响;该 helper 既不更改请求前缀,也不跨运行保留状态。 ## 已知限制与暂缓事项 diff --git a/packages/support/loader-smoke/package.json b/packages/support/loader-smoke/package.json index 1eeabb950d..5cb8ef96a2 100644 --- a/packages/support/loader-smoke/package.json +++ b/packages/support/loader-smoke/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-loader-smoke", - "description": "Shared subprocess harness for keyless real-Loader example smoke tests", + "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", "version": "0.0.1", "private": true, "type": "module", @@ -29,11 +29,17 @@ "tsx": "^4.22.4" }, "peerDependencies": { + "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { + "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.6" } } diff --git a/examples/headless-agent/tests/fixtures/one-shot.ts b/packages/support/loader-smoke/src/agent-turn.ts similarity index 93% rename from examples/headless-agent/tests/fixtures/one-shot.ts rename to packages/support/loader-smoke/src/agent-turn.ts index 79b84da36f..ea3a65d725 100644 --- a/examples/headless-agent/tests/fixtures/one-shot.ts +++ b/packages/support/loader-smoke/src/agent-turn.ts @@ -1,4 +1,7 @@ -/** Test-only direct-agent turn driver shared by assembled Loader fixtures. */ +/** + * Test-only direct-agent turn driver shared by assembled Loader fixtures. + * @module @deepseek-ai/dsh-loader-smoke/agent-turn + */ import type { Context } from 'cordis' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -39,7 +42,7 @@ function onlyRootAgent(ctx: Context): Agent { const agents = ctx.get('agents')?.roots() ?? [] const [agent] = agents if (agent === undefined || agents.length !== 1) { - throw new Error(`headless fixture requires exactly one top-level agent, found ${agents.length}`) + throw new Error(`fixture turn requires exactly one top-level agent, found ${agents.length}`) } return agent } diff --git a/packages/support/loader-smoke/src/index.ts b/packages/support/loader-smoke/src/index.ts index 4ee5cfec7d..33ec56ec0e 100644 --- a/packages/support/loader-smoke/src/index.ts +++ b/packages/support/loader-smoke/src/index.ts @@ -16,6 +16,12 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { execa } from 'execa' +export { + runFixtureTurn, + type FixtureTurnOptions, + type FixtureTurnResult, +} from './agent-turn.ts' + const DEFAULT_PROCESS_TIMEOUT_MS = 30_000 /** Vitest deadline that leaves room for the subprocess-owned 30-second diagnostic timeout. */ diff --git a/packages/support/loader-smoke/tests/agent-turn.spec.ts b/packages/support/loader-smoke/tests/agent-turn.spec.ts new file mode 100644 index 0000000000..481b4ef566 --- /dev/null +++ b/packages/support/loader-smoke/tests/agent-turn.spec.ts @@ -0,0 +1,160 @@ +import type { Context } from 'cordis' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import { describe, expect, it, vi } from 'vitest' +import { runFixtureTurn } from '../src/agent-turn.ts' + +type Listener = (session: unknown, event: SessionEvent) => void + +const event = (value: object): SessionEvent => value as unknown as SessionEvent + +function turnHarness(): { + readonly ctx: Context + readonly session: { readonly id: string } + readonly foreignSession: object + readonly emit: (session: unknown, value: object) => void + readonly setFollowup: (callback: (message: { readonly id: unknown }) => void) => void + readonly whenIdle: ReturnType + readonly disposeListener: ReturnType + readonly flush: ReturnType +} { + const session = { id: 'fixture-session' } + const foreignSession = {} + let listener: Listener | undefined + let followup = (_message: { readonly id: unknown }): void => {} + const whenIdle = vi.fn(async () => {}) + const disposeListener = vi.fn() + const flush = vi.fn(async () => {}) + const agent = { + session, + whenIdle, + followup: vi.fn((message: { readonly id: unknown }) => { followup(message) }), + } + const ctx = { + get: (name: string) => name === 'agents' ? { roots: () => [agent] } : undefined, + on: (_name: string, callback: Listener) => { + listener = callback + return disposeListener + }, + sessions: { flush }, + } as unknown as Context + return { + ctx, + session, + foreignSession, + emit: (target, value) => { listener?.(target, event(value)) }, + setFollowup: (callback) => { followup = callback }, + whenIdle, + disposeListener, + flush, + } +} + +describe('runFixtureTurn', () => { + it.each([ + ['no agent registry', undefined, 0], + ['multiple roots', { roots: () => [{}, {}] }, 2], + ])('rejects %s', async (_label, registry, count) => { + const ctx = { get: () => registry } as unknown as Context + await expect(runFixtureTurn(ctx, { task: 'ignored' })) + .rejects.toThrow(`fixture turn requires exactly one top-level agent, found ${count}`) + }) + + it('observes only the owned interval and returns its final text and deduplicated usage', async () => { + const harness = turnHarness() + const observed: SessionEvent[] = [] + harness.setFollowup((message) => { + harness.emit(harness.foreignSession, { + type: 'assistant/message', seq: 0, time: 0, data: { message: { content: [] } }, + }) + harness.emit(harness.session, { + type: 'step/start', seq: 0, time: 0, data: { turn: 1, step: 1 }, + }) + harness.emit(harness.session, { + type: 'agent/inbox/spliced', seq: 1, time: 1, data: { inserted: [{ id: 'other' }] }, + }) + harness.emit(harness.session, { + type: 'agent/inbox/spliced', seq: 2, time: 2, data: { inserted: [message] }, + }) + harness.emit(harness.session, { + type: 'assistant/chunk', seq: 3, time: 3, + data: { turn: 1, step: 1, chunk: { type: 'text-delta', text: 'partial' } }, + }) + harness.emit(harness.session, { + type: 'assistant/chunk', seq: 4, time: 4, + data: { + turn: 1, + step: 1, + chunk: { type: 'usage', usage: { inputTokens: 2, outputTokens: 3, reasoningTokens: 1 } }, + }, + }) + harness.emit(harness.session, { + type: 'assistant/message', seq: 5, time: 5, + data: { + turn: 1, + step: 1, + message: { content: [{ type: 'text', text: 'final answer' }] }, + usage: { inputTokens: 4, outputTokens: 5, cacheReadTokens: 6 }, + }, + }) + harness.emit(harness.session, { + type: 'assistant/chunk', seq: 6, time: 6, + data: { + turn: 1, + step: 2, + chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2, cacheWriteTokens: 7, reasoningTokens: 2 } }, + }, + }) + harness.emit(harness.session, { + type: 'assistant/message', seq: 7, time: 7, + data: { turn: 1, step: 2, message: { content: [{ type: 'tool-call' }] } }, + }) + harness.emit(harness.foreignSession, { + type: 'assistant/message', seq: 8, time: 8, data: { message: { content: [] } }, + }) + }) + + await expect(runFixtureTurn(harness.ctx, { + task: 'prove the fixture', + onEvent: (_sessionId, current) => { observed.push(current) }, + })).resolves.toEqual({ + type: 'result', + sessionId: 'fixture-session', + output: 'final answer', + usage: { + inputTokens: 5, + outputTokens: 7, + cacheReadTokens: 6, + cacheWriteTokens: 7, + reasoningTokens: 2, + }, + }) + expect(observed.map(current => current.seq)).toEqual([2, 3, 4, 5, 6, 7]) + expect(harness.whenIdle).toHaveBeenCalledTimes(2) + expect(harness.flush).toHaveBeenCalledWith(harness.session) + expect(harness.disposeListener).toHaveBeenCalledOnce() + }) + + it('omits usage when the interval records none', async () => { + const harness = turnHarness() + harness.setFollowup((message) => { + harness.emit(harness.session, { + type: 'agent/inbox/spliced', seq: 0, time: 0, data: { inserted: [message] }, + }) + }) + + await expect(runFixtureTurn(harness.ctx, { task: 'no model step' })).resolves.toEqual({ + type: 'result', + sessionId: 'fixture-session', + output: '', + }) + }) + + it('always removes its listener when the turn fails', async () => { + const harness = turnHarness() + harness.whenIdle.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('turn failed')) + + await expect(runFixtureTurn(harness.ctx, { task: 'fail' })).rejects.toThrow('turn failed') + expect(harness.disposeListener).toHaveBeenCalledOnce() + expect(harness.flush).not.toHaveBeenCalled() + }) +}) diff --git a/packages/support/loader-smoke/tsconfig.json b/packages/support/loader-smoke/tsconfig.json index d970a00263..52ec32ca67 100644 --- a/packages/support/loader-smoke/tsconfig.json +++ b/packages/support/loader-smoke/tsconfig.json @@ -8,6 +8,15 @@ "src" ], "references": [ + { + "path": "../../core/agent" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, { "path": "../../support/invariants" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9957263083..8af4853536 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -376,6 +376,9 @@ importers: '@deepseek-ai/dsh-llm-replay': specifier: workspace:* version: link:../packages/support/llm-replay + '@deepseek-ai/dsh-loader-smoke': + specifier: workspace:* + version: link:../packages/support/loader-smoke '@deepseek-ai/dsh-lsp': specifier: workspace:* version: link:../packages/lsp/lsp @@ -5936,9 +5939,18 @@ importers: specifier: ^4.22.4 version: 4.22.4 devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session cordis: specifier: ^4.0.0-rc.6 version: link:../../../vendor/cordis diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 429beb5480..67af38455e 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -124,7 +124,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' }, 'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' }, 'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' }, - 'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' }, + 'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' }, 'packages/support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' }, 'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' }, 'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },