From 1cfe0f9942df37dde7341ea0b6a858148d8c0323 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:07:20 +0800 Subject: [PATCH] refactor(test): reserve snapshots for session recordings --- ...-bilingual-docs-and-pairing-gate.i18n.yaml | 4 +- ...6-07-02-bilingual-docs-and-pairing-gate.md | 2 +- ...7-02-bilingual-docs-and-pairing-gate.zh.md | 2 +- ...8-24-session-log-snapshot-corpus.i18n.yaml | 4 +- .../2026-08-24-session-log-snapshot-corpus.md | 1 + ...26-08-24-session-log-snapshot-corpus.zh.md | 1 + .agents/skills/dsh-pre-push-checks/SKILL.md | 2 +- AGENTS.md | 3 +- ...ge.snapshot.ts => dsh-badge.golden.e2e.ts} | 0 ...shot.ts => web-browser-open.golden.e2e.ts} | 0 ...t.snapshot.ts => built-boot.golden.e2e.ts} | 0 ...s => command-image-envelope.golden.e2e.ts} | 0 ...pshot.ts => home-path-tilde.golden.e2e.ts} | 0 ...napshot.ts => image-display.golden.e2e.ts} | 0 ...hot.ts => max-tokens-notice.golden.e2e.ts} | 0 ....snapshot.ts => search-card.golden.e2e.ts} | 0 ...row.snapshot.ts => todo-row.golden.e2e.ts} | 0 docs/testing.i18n.yaml | 4 +- docs/testing.md | 3 +- docs/testing.zh.md | 3 +- examples/acp-agent/cordis.yml | 92 +------------------ .../{goal.snapshot.ts => goal.golden.e2e.ts} | 2 +- ...napshot.ts => image-offload.golden.e2e.ts} | 79 +--------------- ...ess.snapshot.ts => headless.golden.e2e.ts} | 0 ...t.ts => semantic-checkpoint.golden.e2e.ts} | 0 ....ts => session-format-guard.golden.e2e.ts} | 0 ...t.ts => subagent-diagnostic.golden.e2e.ts} | 0 ....ts => subagent-inheritance.golden.e2e.ts} | 0 ...=> workspace-context-resume.golden.e2e.ts} | 0 knip.json | 7 +- package.json | 2 + .../session-snapshot/README.i18n.yaml | 4 +- .../test-support/session-snapshot/README.md | 4 +- .../session-snapshot/README.zh.md | 4 +- .../session-snapshot/src/harness.ts | 2 +- .../session-snapshot/src/index.ts | 2 +- scripts/run-gates.spec.ts | 5 +- scripts/run-gates.ts | 16 +++- scripts/session-fixture-layout.snapshot.ts | 17 ---- scripts/session-fixture-layout.spec.ts | 18 +++- ...t.ts => session-snapshot-corpus.corpus.ts} | 29 ++++++ ...t.ts => translation-prompt.golden.spec.ts} | 0 snapshots/AGENTS.md | 2 +- snapshots/acp/acp.snapshot.ts | 63 +++++++++++++ .../escalation-approved}/cordis.snapshot.yml | 2 +- snapshots/acp/escalation-approved/cordis.yml | 91 ++++++++++++++++++ .../subagent-settlement-marker.ts | 0 vitest.e2e.config.ts | 1 + vitest.golden.config.ts | 20 ++++ vitest.snapshot.config.ts | 4 +- 50 files changed, 273 insertions(+), 222 deletions(-) rename apps/cli/tests/{dsh-badge.snapshot.ts => dsh-badge.golden.e2e.ts} (100%) rename apps/cli/tests/{web-browser-open.snapshot.ts => web-browser-open.golden.e2e.ts} (100%) rename apps/web/tests/{built-boot.snapshot.ts => built-boot.golden.e2e.ts} (100%) rename apps/web/tests/{command-image-envelope.snapshot.ts => command-image-envelope.golden.e2e.ts} (100%) rename apps/web/tests/{home-path-tilde.snapshot.ts => home-path-tilde.golden.e2e.ts} (100%) rename apps/web/tests/{image-display.snapshot.ts => image-display.golden.e2e.ts} (100%) rename apps/web/tests/{max-tokens-notice.snapshot.ts => max-tokens-notice.golden.e2e.ts} (100%) rename apps/web/tests/{search-card.snapshot.ts => search-card.golden.e2e.ts} (100%) rename apps/web/tests/{todo-row.snapshot.ts => todo-row.golden.e2e.ts} (100%) mode change 100644 => 120000 examples/acp-agent/cordis.yml rename examples/acp-agent/tests/{goal.snapshot.ts => goal.golden.e2e.ts} (98%) rename examples/acp-agent/tests/{acp.snapshot.ts => image-offload.golden.e2e.ts} (76%) rename examples/headless-agent/tests/{headless.snapshot.ts => headless.golden.e2e.ts} (100%) rename examples/headless-agent/tests/{semantic-checkpoint.snapshot.ts => semantic-checkpoint.golden.e2e.ts} (100%) rename examples/headless-agent/tests/{session-format-guard.snapshot.ts => session-format-guard.golden.e2e.ts} (100%) rename examples/headless-agent/tests/{subagent-diagnostic.snapshot.ts => subagent-diagnostic.golden.e2e.ts} (100%) rename examples/headless-agent/tests/{subagent-inheritance.snapshot.ts => subagent-inheritance.golden.e2e.ts} (100%) rename examples/headless-agent/tests/{workspace-context-resume.snapshot.ts => workspace-context-resume.golden.e2e.ts} (100%) delete mode 100644 scripts/session-fixture-layout.snapshot.ts rename scripts/{session-snapshot-corpus.snapshot.ts => session-snapshot-corpus.corpus.ts} (85%) rename scripts/{translation-prompt.snapshot.ts => translation-prompt.golden.spec.ts} (100%) create mode 100644 snapshots/acp/acp.snapshot.ts rename {examples/acp-agent => snapshots/acp/escalation-approved}/cordis.snapshot.yml (94%) create mode 100644 snapshots/acp/escalation-approved/cordis.yml rename {examples/acp-agent/tests/fixtures => snapshots/acp/escalation-approved}/subagent-settlement-marker.ts (100%) create mode 100644 vitest.golden.config.ts diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml index c5839b97b2..bcfb80de6f 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.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/process/2026-07-02-bilingual-docs-and-pairing-gate.md -2026-07-02-bilingual-docs-and-pairing-gate.md: 8fbda5e8987d7c575e3ef96f1724b04dd7070621 -2026-07-02-bilingual-docs-and-pairing-gate.zh.md: b022abf61be733368263ff3de4176e43832b09a2 +2026-07-02-bilingual-docs-and-pairing-gate.md: f08b5b55b67007cb8c735525afffa5983b9b5863 +2026-07-02-bilingual-docs-and-pairing-gate.zh.md: f857f79f952b67adf50b1a18fb6daf85a5e3a404 diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md index 8fbda5e898..f08b5b55b6 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md @@ -19,7 +19,7 @@ This repo's documentation corpus is read by people and agents inside and outside ## Verification -The verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure, while its discovery tests pin the named root policy documents and automatic README coverage; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable snapshot](../../../../scripts/translation-prompt.snapshot.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible. +The verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure, while its discovery tests pin the named root policy documents and automatic README coverage; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable golden](../../../../scripts/translation-prompt.golden.spec.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md index b022abf61b..f857f79f95 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md @@ -19,7 +19,7 @@ Status: implemented ## 验证 -验证约定分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、语言切换行和结构,其发现测试则固定具名的根目录政策文档与自动 README 覆盖;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行快照](../../../../scripts/translation-prompt.snapshot.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和所消费的响应。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。 +验证约定分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、语言切换行和结构,其发现测试则固定具名的根目录政策文档与自动 README 覆盖;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行 golden](../../../../scripts/translation-prompt.golden.spec.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和所消费的响应。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml index e4e0e5983d..be5a8e8182 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.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/testing/2026-08-24-session-log-snapshot-corpus.md -2026-08-24-session-log-snapshot-corpus.md: 061bb72f0382448d29aec0cd6c38795ab1e82f87 -2026-08-24-session-log-snapshot-corpus.zh.md: 2b321ebb90fa30a51ab131ba5a8a64134d51492a +2026-08-24-session-log-snapshot-corpus.md: 8706a0c431435319bd89ba5013853ebac60e4c50 +2026-08-24-session-log-snapshot-corpus.zh.md: fc64595267dd89b69a3097d552b858e3793eff46 diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md index 061bb72f03..8706a0c431 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md @@ -39,6 +39,7 @@ Workspace inputs remain scenario-local. A mutating scenario compares a complete - Every top-level scenario owns or references session JSONL; non-session goldens remain owner-local. - Committed session fixtures are redaction fixed points, contain no system-prompt or tool-schema bulk, and retain exactly one pin per header class. - Mutating scenarios verify their final workspace externally. +- Owner-local process goldens use `*.golden.e2e.ts` and a separate built-output gate. - Source and built launch modes, browser replay, SDK projections, packaged Python runtime cases, documentation gates, and repository hygiene pass. ## Consequences diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md index 2b321ebb90..fc64595267 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md @@ -39,6 +39,7 @@ Workspace 输入继续归各场景本地所有。变更文件的场景比较完 - 每个顶层场景都拥有或引用会话 JSONL;非会话 golden 继续归所有者本地所有。 - 提交的会话 fixture 是脱敏固定点,不含 system prompt 或工具 schema 正文,并为每个 header 类保留且仅保留一个 pin。 - 变更内容的场景从外部验证最终 workspace。 +- 所属位置的进程 golden 使用 `*.golden.e2e.ts`,并由单独的构建产物门禁运行。 - 源码和构建启动模式、浏览器回放、SDK 投影、打包 Python 运行时场景、文档门禁和仓库卫生检查通过。 ## Consequences diff --git a/.agents/skills/dsh-pre-push-checks/SKILL.md b/.agents/skills/dsh-pre-push-checks/SKILL.md index 70d6d3aae5..f468745e7f 100644 --- a/.agents/skills/dsh-pre-push-checks/SKILL.md +++ b/.agents/skills/dsh-pre-push-checks/SKILL.md @@ -31,7 +31,7 @@ There is no universal local baseline beyond the hooks. Every behavior change nee - **Package or script behavior:** run the owning Vitest file or focused test name. Add adjacent package tests when a shared contract changes; leave repository-wide coverage to CI unless the change is genuinely cross-cutting or the user requests it. - **Documentation, Agent Notes, catalogs, or doc-linked comments:** run `pnpm run doc-sync`; run full lint when the documentation workflow requires it. - **Model-, editor-, CLI-, or terminal-visible output:** run the focused keyless snapshot or real runnable-example scenario that owns the output. -- **Expected-output placement:** a test whose recorded `session.jsonl` is replay input and expected persisted output belongs under top-level `snapshots/`, with `snapshot.yml` naming its shipped `dsh` profile and composition/header pin. ARIA, geometry, generator, CLI, and unit expectations without that session round trip stay beside their owning test under `tests/goldens/`; do not place them in `snapshots/` or give them a `*.snapshot.ts` owner. +- **Expected-output placement:** a test whose recorded `session.jsonl` is replay input and expected persisted output belongs under top-level `snapshots/`, with `snapshot.yml` naming its shipped `dsh` profile and composition/header pin. ARIA, geometry, generator, CLI, and unit expectations without that session round trip stay beside their owning test under `tests/goldens/`; do not place them in `snapshots/` or give them a `*.snapshot.ts` owner. Use the owning `test:golden`, `test:web`, or `test` lane. - **Package manifests, public exports, build configuration, worker/bin entries, or built runtime paths:** run `pnpm run build`, the relevant hygiene checks, and the owning built-artifact smoke. - **Real provider or agent behavior:** run the relevant `pnpm run test:e2e` target when credentials are available; never print secrets. diff --git a/AGENTS.md b/AGENTS.md index ddfa5a0dfb..12ae3be104 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,9 +67,10 @@ Package groups: [packages/README.md](packages/README.md). ```sh pnpm install # pnpm workspaces, node ^22.19 || >=24 pnpm run clean # remove build outputs and safe residue from deleted packages -pnpm run test # vitest unit tests +pnpm run test # unit tests pnpm run test:coverage # CI coverage gate: per-file 100% on packages/*/*/src pnpm run test:e2e # real-API tests; self-skip without DEEPSEEK_API_KEY +pnpm run test:golden # owner-local process goldens pnpm run test:snapshot # keyless recorded-session replay through shipped profiles; filter: -t pnpm run test:snapshot:record # re-record expected outputs (needs key) pnpm run typecheck diff --git a/apps/cli/tests/dsh-badge.snapshot.ts b/apps/cli/tests/dsh-badge.golden.e2e.ts similarity index 100% rename from apps/cli/tests/dsh-badge.snapshot.ts rename to apps/cli/tests/dsh-badge.golden.e2e.ts diff --git a/apps/cli/tests/web-browser-open.snapshot.ts b/apps/cli/tests/web-browser-open.golden.e2e.ts similarity index 100% rename from apps/cli/tests/web-browser-open.snapshot.ts rename to apps/cli/tests/web-browser-open.golden.e2e.ts diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.golden.e2e.ts similarity index 100% rename from apps/web/tests/built-boot.snapshot.ts rename to apps/web/tests/built-boot.golden.e2e.ts diff --git a/apps/web/tests/command-image-envelope.snapshot.ts b/apps/web/tests/command-image-envelope.golden.e2e.ts similarity index 100% rename from apps/web/tests/command-image-envelope.snapshot.ts rename to apps/web/tests/command-image-envelope.golden.e2e.ts diff --git a/apps/web/tests/home-path-tilde.snapshot.ts b/apps/web/tests/home-path-tilde.golden.e2e.ts similarity index 100% rename from apps/web/tests/home-path-tilde.snapshot.ts rename to apps/web/tests/home-path-tilde.golden.e2e.ts diff --git a/apps/web/tests/image-display.snapshot.ts b/apps/web/tests/image-display.golden.e2e.ts similarity index 100% rename from apps/web/tests/image-display.snapshot.ts rename to apps/web/tests/image-display.golden.e2e.ts diff --git a/apps/web/tests/max-tokens-notice.snapshot.ts b/apps/web/tests/max-tokens-notice.golden.e2e.ts similarity index 100% rename from apps/web/tests/max-tokens-notice.snapshot.ts rename to apps/web/tests/max-tokens-notice.golden.e2e.ts diff --git a/apps/web/tests/search-card.snapshot.ts b/apps/web/tests/search-card.golden.e2e.ts similarity index 100% rename from apps/web/tests/search-card.snapshot.ts rename to apps/web/tests/search-card.golden.e2e.ts diff --git a/apps/web/tests/todo-row.snapshot.ts b/apps/web/tests/todo-row.golden.e2e.ts similarity index 100% rename from apps/web/tests/todo-row.snapshot.ts rename to apps/web/tests/todo-row.golden.e2e.ts diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index dc43b0737f..91fcaf9bb3 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.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/testing.md -testing.md: d6d6cdb4b2cdab45b599a3a131f1790fcfcca118 -testing.zh.md: b9f4830be38e41f095eb18aa13087a904ef02023 +testing.md: 8792d928798cabae0a4d2f12ff779248cf44d8ee +testing.zh.md: ea7d8a32f26d98656b977a8efeda698ff7382986 diff --git a/docs/testing.md b/docs/testing.md index d6d6cdb4b2..8792d92879 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,7 +9,8 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent tests for contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`). - **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/shell/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar. - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)). -- **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's recorded `session.jsonl` supplies user input and model replay, then serves as the expected persisted result. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web scenarios retain browser/ARIA evidence beside the same canonical session. `snapshot.yml` declares the shipped profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed identity tokens preserve relationships across parent and child logs; prompts and tool schemas live only in the small set of sidecars named by header pins. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every session, sidecar, protocol, UI, and workspace diff. +- **Owner-local golden** (`pnpm run test:golden`): keyless assembled CLI/process expectations without a recorded-session round trip. Drivers use `*.golden.e2e.ts` beside `tests/goldens/`; CI runs built exports. Package/script goldens use `test`, while browser goldens use `test:web`. +- **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's recorded `session.jsonl` supplies user input and model replay, then serves as the expected persisted result. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web retains browser/ARIA evidence beside the same session. `snapshot.yml` declares the profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed tokens preserve parent/child identity relationships; only header pins own prompt/schema sidecars. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `test:snapshot:record` when a model transcript changes and `test:snapshot:refresh` when replay input remains valid; review every resulting diff. - **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares session-driven output under `snapshots/web/` and UI-only output under `apps/web/tests/goldens/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS. Session fixtures keep headers and payloads but omit body sequence/time envelopes. Replay synthesizes them; runtime persistence is unchanged. Fixtures use canonical packed rows; [the migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites old layouts. diff --git a/docs/testing.zh.md b/docs/testing.zh.md index b9f4830be3..ea7d8a32f2 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -9,7 +9,8 @@ - **单元测试**(`pnpm run test`):vitest 运行包和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR(热模块替换)安全测试(对向该注册表贡献内容的 fiber 执行 dispose(资源释放),并断言清理完成)。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及针对约定回归的永久测试(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。 - **覆盖率门禁**(`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/shell/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其执行器套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。 - **真实 API e2e**(`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY`、`PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md))。 -- **快照**(`pnpm run test:snapshot`):顶层场景的录制 `session.jsonl` 同时提供用户输入和模型回放,并作为持久化结果的预期值。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 场景在同一规范会话旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明随附 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及工作区事实。带类型的身份 token 在父子日志之间保留引用关系;系统提示词和工具 schema 只存放在少量请求头 pin 指定的 sidecar 中。变更工作区的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `pnpm run test:snapshot:record`,回放输入仍有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处会话、sidecar、协议、UI 与工作区差异。 +- **所属位置的 golden**(`pnpm run test:golden`):无录制会话往返的无密钥组装 CLI/进程预期。驱动使用 `*.golden.e2e.ts`,并与 `tests/goldens/` 同属一处;CI 针对构建产物运行。包/脚本 golden 使用 `test`,浏览器 golden 使用 `test:web`。 +- **快照**(`pnpm run test:snapshot`):顶层场景的录制 `session.jsonl` 同时提供用户输入和模型回放,并作为持久化结果的预期值。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 在同一会话旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及工作区事实。带类型的 token 保留父子身份关系;只有请求头 pin 拥有提示词/schema sidecar。变更工作区的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `test:snapshot:record`,回放输入仍有效时使用 `test:snapshot:refresh`;请审查所有结果差异。 - **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 比较 `snapshots/web/` 下由会话驱动的输出,以及 `apps/web/tests/goldens/` 下仅含 UI 的输出。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md)以交付插件 CSS。 会话 fixture 保留 header 与 payload,但省略正文序号/时间 envelope。回放会合成这些字段;运行时持久化不变。fixture 使用规范打包行;[迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写旧布局。 diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml deleted file mode 100644 index 8f1df1955d..0000000000 --- a/examples/acp-agent/cordis.yml +++ /dev/null @@ -1,91 +0,0 @@ -# ACP demo and snapshot-record patch over the shipped `acp` profile. The dsh -# launcher owns environment loading, plugin resolution, and process shutdown; -# stdout remains reserved for ACP JSON-RPC. - -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - config: - thinking: enabled - reasoningEffort: max - models: - - id: deepseek-v4-flash - - id: deepseek-v4-pro - - id: deepseek-v4-flash-vision-exp - inputModalities: [text, image] - -- id: sandbox-policy - name: '@deepseek-ai/dsh-sandbox-policy' - config: - mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')" - workspaceRoot: !!js process.cwd() - -- id: approval - name: '@deepseek-ai/dsh-user-approval' - config: - policy: !!js "(process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')) === 'danger-full-access' ? 'never' : 'ask'" - -- id: session-persistence-jsonl - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? dshHomePath('sessions') - compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'" - -- id: acp - name: '@deepseek-ai/dsh-acp' - config: - provider: deepseek-official - model: deepseek-v4-pro - -- id: system-prompt - name: '@deepseek-ai/dsh-system-prompt' - config: - persona: | - You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. - - Verify your work by running the code or tests. Keep answers brief and factual. - -- id: agent-instructions - name: '@deepseek-ai/dsh-agent-instructions' - config: - maxBytes: 65536 - -- id: tool-subagent - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: spawn - toolName: subagent - enableModelSelection: true - backgroundMode: continuable - maxDepth: 1 - -# Fork omits model selection so provider/model stay equal to the parent and the -# inherited history remains eligible for KV Cache reuse. It stays one-shot because -# a continuable child's `report` tool and prompt section precede that history and -# invalidate the same prefix. `run_in_background` is off as an explicit foreground-only -# choice even though the shipped ACP profile mounts the generic Job runtime. -# See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md -# and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. -- id: tool-subagent-fork - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: fork - toolName: subagent_fork - backgroundMode: one-shot - enableRunInBackground: false - maxDepth: 1 - -- id: fs-sandbox - name: '@deepseek-ai/dsh-fs-sandbox' - config: - cwd: !!js process.cwd() - -- insert: - - id: hooks-claude-code - name: '@deepseek-ai/dsh-hooks-claude-code' - config: - configPath: ./hooks.json - - - id: hooks-codex - name: '@deepseek-ai/dsh-hooks-codex' - config: - configPath: ./codex-hooks.json diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml new file mode 120000 index 0000000000..e498f0aa98 --- /dev/null +++ b/examples/acp-agent/cordis.yml @@ -0,0 +1 @@ +../../snapshots/acp/escalation-approved/cordis.yml \ No newline at end of file diff --git a/examples/acp-agent/tests/goal.snapshot.ts b/examples/acp-agent/tests/goal.golden.e2e.ts similarity index 98% rename from examples/acp-agent/tests/goal.snapshot.ts rename to examples/acp-agent/tests/goal.golden.e2e.ts index ee7001277d..2e1494a01b 100644 --- a/examples/acp-agent/tests/goal.snapshot.ts +++ b/examples/acp-agent/tests/goal.golden.e2e.ts @@ -25,7 +25,7 @@ const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const agent: AgentUnderTest = { binScript: fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)), - configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)), + configPath: fileURLToPath(new URL('../../../snapshots/acp/escalation-approved/cordis.yml', import.meta.url)), profile: 'acp', tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)), } diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/image-offload.golden.e2e.ts similarity index 76% rename from examples/acp-agent/tests/acp.snapshot.ts rename to examples/acp-agent/tests/image-offload.golden.e2e.ts index 447637853a..5f1c0df189 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/image-offload.golden.e2e.ts @@ -1,37 +1,17 @@ import { fileURLToPath } from 'node:url' -import { existsSync } from 'node:fs' -import { spawnSync } from 'node:child_process' import { createServer } from 'node:http' import type { IncomingMessage, ServerResponse } from 'node:http' import { join } from 'node:path' import { expect, it } from 'vitest' import { - defineAcpSnapshotSuite, runScenario, type InputScript, - type Scenario, - type SnapshotSuiteOptions, } from '@deepseek-ai/dsh-session-snapshot' -import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local' import { OFFLOADED_IMAGE_TEXT } from '@deepseek-ai/dsh-llm' -/** - * The acp-agent example's snapshot suite: the scenario table for - * `dsh-session-snapshot`'s ACP suite factory, which owns every compare/guard mechanic - * (expected-output + re-persisted-log diffs, record/refresh write-back, the pinned-header - * uniformity guard, the fixture guards). Fixtures live under `snapshots//`; - * `pnpm run test:snapshot:record` re-records model transcripts against the real - * API; `pnpm run test:snapshot:refresh` rewrites current replay expected outputs keyless. - * See the package README (packages/test-support/session-snapshot) and the snapshot Agent Note, - * .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md. - */ - -// The dsh CLI, this example's profile patch, and -// the repo-root tsconfig (four levels up from examples/acp-agent/tests) — all -// ABSOLUTE: the subprocess cwd is a temp dir outside the repo. const AGENT = { binScript: fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url)), - configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)), + configPath: fileURLToPath(new URL('../../../snapshots/acp/escalation-approved/cordis.yml', import.meta.url)), profile: 'acp', tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)), } @@ -39,63 +19,6 @@ const IMAGE_OFFLOAD_CONFIG = fileURLToPath(new URL('./fixtures/image-offload.cor const SNAPSHOTS_DIR = fileURLToPath(new URL('../../../snapshots/acp/', import.meta.url)) const READ_IMAGE_WORKSPACE = fileURLToPath(new URL('../../../snapshots/session/read-image/workspace/', import.meta.url)) -function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] { - switch (value) { - case undefined: - case '': - case 'replay': - return 'replay' - case 'record': - return 'record' - case 'refresh': - return 'refresh' - default: - throw new Error(`unknown DSH_SNAPSHOT mode: ${value}`) - } -} - -const SCENARIOS: Scenario[] = [ - { name: 'handshake', hasModelTurn: false, recorded: false, headerClass: 'sandbox' }, - { name: 'reject-extra-dirs', hasModelTurn: false, recorded: false, headerClass: 'sandbox' }, - { name: 'cancel', hasModelTurn: true, recorded: false, overridden: true, headerClass: 'sandbox' }, - { name: 'cancel-tool-calls', hasModelTurn: true, recorded: false, overridden: true, headerClass: 'sandbox', posixOnly: true }, - { - name: 'escalation-approved', - hasModelTurn: true, - recorded: true, - pinsHeader: true, - headerClass: 'sandbox', - env: { DSH_PERMISSION_MODE: 'workspace-write' }, - }, - { - name: 'escalation-rejected', - hasModelTurn: true, - recorded: true, - headerClass: 'sandbox', - env: { DSH_PERMISSION_MODE: 'workspace-write' }, - }, - { - name: 'fs-escalation-approved', - hasModelTurn: true, - recorded: true, - headerClass: 'sandbox', - env: { DSH_PERMISSION_MODE: 'workspace-write' }, - }, -] - -// Hosts without a usable PowerShell skip the pwsh-tool-turn run (its fixtures -// stay guarded); the probe follows the executor's own resolution so a Windows -// host with only an install-location pwsh still runs the scenario. -const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0 - -defineAcpSnapshotSuite({ - agent: AGENT, - snapshotsDir: SNAPSHOTS_DIR, - scenarios: SCENARIOS.filter(scenario => existsSync(join(SNAPSHOTS_DIR, scenario.name))), - mode: snapshotModeFromEnv(process.env.DSH_SNAPSHOT), - hasPwsh, -}) - it('pins native DeepSeek Files offload and inline fallback in assembled requests', async () => { const requests: Record[] = [] const fileRequests: Array<{ method: string; path: string; bytes: number }> = [] diff --git a/examples/headless-agent/tests/headless.snapshot.ts b/examples/headless-agent/tests/headless.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/headless.snapshot.ts rename to examples/headless-agent/tests/headless.golden.e2e.ts diff --git a/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts b/examples/headless-agent/tests/semantic-checkpoint.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/semantic-checkpoint.snapshot.ts rename to examples/headless-agent/tests/semantic-checkpoint.golden.e2e.ts diff --git a/examples/headless-agent/tests/session-format-guard.snapshot.ts b/examples/headless-agent/tests/session-format-guard.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/session-format-guard.snapshot.ts rename to examples/headless-agent/tests/session-format-guard.golden.e2e.ts diff --git a/examples/headless-agent/tests/subagent-diagnostic.snapshot.ts b/examples/headless-agent/tests/subagent-diagnostic.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/subagent-diagnostic.snapshot.ts rename to examples/headless-agent/tests/subagent-diagnostic.golden.e2e.ts diff --git a/examples/headless-agent/tests/subagent-inheritance.snapshot.ts b/examples/headless-agent/tests/subagent-inheritance.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/subagent-inheritance.snapshot.ts rename to examples/headless-agent/tests/subagent-inheritance.golden.e2e.ts diff --git a/examples/headless-agent/tests/workspace-context-resume.snapshot.ts b/examples/headless-agent/tests/workspace-context-resume.golden.e2e.ts similarity index 100% rename from examples/headless-agent/tests/workspace-context-resume.snapshot.ts rename to examples/headless-agent/tests/workspace-context-resume.golden.e2e.ts diff --git a/knip.json b/knip.json index e9c1b9868b..bd80f09664 100644 --- a/knip.json +++ b/knip.json @@ -51,7 +51,6 @@ "headless-agent/tests/fixtures/session-telemetry-otel-driver.ts", "headless-agent/tests/fixtures/telemetry-redact-rule.ts", "headless-agent/tests/fixtures/e2b/e2b/bin.ts", - "acp-agent/tests/fixtures/subagent-settlement-marker.ts", "acp-agent/tests/fixtures/control-surface/control-surface-llm.ts", "acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts", "acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts", @@ -62,8 +61,7 @@ "python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts", "python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/child-mock-llm.ts", "python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts", - "*/tests/**/*.e2e.ts", - "*/tests/**/*.snapshot.ts" + "*/tests/**/*.e2e.ts" ], "project": [ "**/*.ts" @@ -650,8 +648,7 @@ "apps/cli": { "entry": [ "tests/**/*.spec.ts", - "tests/**/*.e2e.ts", - "tests/**/*.snapshot.ts" + "tests/**/*.e2e.ts" ], "project": [ "src/**/*.ts", diff --git a/package.json b/package.json index cb5f33f4d9..eeb41b35a2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "test:coverage": "vitest run --coverage", "test:coverage:partitioned": "tsx scripts/run-coverage-partitions.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts", + "test:golden": "vitest run --config vitest.golden.config.ts", + "test:golden:refresh": "DSH_SNAPSHOT=refresh vitest run --config vitest.golden.config.ts", "test:issue-management": "node .github/issue-management/policy.test.mjs", "test:snapshot": "vitest run --config vitest.snapshot.config.ts", "test:snapshot:record": "DSH_SNAPSHOT=record vitest run --config vitest.snapshot.config.ts --update", diff --git a/packages/test-support/session-snapshot/README.i18n.yaml b/packages/test-support/session-snapshot/README.i18n.yaml index 64e039d4ac..ebcedb6914 100644 --- a/packages/test-support/session-snapshot/README.i18n.yaml +++ b/packages/test-support/session-snapshot/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/test-support/session-snapshot/README.md -README.md: ad75128bc2c0af978b5defcbfd0efcf34554091e -README.zh.md: f5167a26f186b3f71e6a39a84baa162fe3862bd8 +README.md: ff5acebe2a11eb9f1c2d6ef2717a7eada050a201 +README.zh.md: b5ff8b957d532bfd6f513bde32a7fd3ca9ee082c diff --git a/packages/test-support/session-snapshot/README.md b/packages/test-support/session-snapshot/README.md index ad75128bc2..ff5acebe2a 100644 --- a/packages/test-support/session-snapshot/README.md +++ b/packages/test-support/session-snapshot/README.md @@ -58,7 +58,7 @@ defineAcpSnapshotSuite({ }) ``` -A scenario booting a differently composed profile sets its own `configPath` patch (its basename still ends in `cordis.yml`, so the launcher finds the sibling `*cordis.snapshot.yml`) and, when that composition changes the request header, its own `headerClass` with its own pinning scenario — the acp-agent example's Code Mode and filesystem scenarios are templates. Default generated workspaces are stored in session fixtures as `{{cwd}}` so platform temp roots and random basenames do not affect recordings; `workspaceParent` moves the generated cwd outside the platform temp area when temporary-directory grants are themselves under test, keeps that explicit path in the fixture, and remains parent-owned while the harness removes only the generated child. A scenario's committed `workspace/` is copied into that child first, then `prepareWorkspace` runs against the generated cwd before the agent starts. Reserve this hook for fixtures Git cannot represent portably, keep ordinary seeds in `workspace/`, and pair it with `posixOnly` when the generated paths are invalid on Windows. +A scenario booting a differently composed profile sets its own `configPath` patch (its basename still ends in `cordis.yml`, so the launcher finds the sibling `*cordis.snapshot.yml`) and, when that composition changes the request header, its own `headerClass` with its own pinning scenario; the top-level profile corpora contain the current templates. Default generated workspaces are stored in session fixtures as `{{cwd}}` so platform temp roots and random basenames do not affect recordings; `workspaceParent` moves the generated cwd outside the platform temp area when temporary-directory grants are themselves under test, keeps that explicit path in the fixture, and remains parent-owned while the harness removes only the generated child. A scenario's committed `workspace/` is copied into that child first, then `prepareWorkspace` runs against the generated cwd before the agent starts. Reserve this hook for fixtures Git cannot represent portably, keep ordinary seeds in `workspace/`, and pair it with `posixOnly` when the generated paths are invalid on Windows. A pin owns its generated `system-prompt.expected.md` or `tool-schemas.expected.json` by default; `systemPromptSource` and `toolSchemasSource` name another pin when the complete corresponding sequence is identical, so each distinct version is committed once. The pin's `session.jsonl` stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A pin with legitimate mid-run header changes declares `expectedHeaderChanges`; a shared source must declare the same count, and record/refresh rejects claimants that generate different bytes. @@ -66,7 +66,7 @@ A child session whose own scope composes a different request declares it per fix Every scenario compares `stdout.expected.jsonl` with cwd-rooted separators canonicalized to `/`. On Windows, `pinsNativeWindowsStdout` additionally compares the complete `stdout.expected.windows.jsonl` after the shared expected output and requires that sidecar exactly when enabled. A scenario requiring a non-Windows host declares `posixOnly`, which skips its run test on Windows while the fixture guards keep covering its committed files everywhere; examples include POSIX process semantics (e.g. cancelling a live bash call kills a detached process group) and generated paths Windows cannot represent. A scenario whose composition needs a usable `pwsh` declares `pwshOnly`; the caller-supplied `hasPwsh` probe (the shipped acp-agent suite follows the executor's own resolution, so Program Files installs count) skips the run test when no usable `pwsh` resolves while the fixture guards keep covering its committed files everywhere. -The example also ships a `cordis.snapshot.yml` replay patch next to its live patch. The launcher applies the live base patch and the selected replay sibling under `DSH_SNAPSHOT=replay` ([single-source replay config Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md)); [`dsh-llm-replay`](../llm-replay/README.md) serves fixtures named by the `DSH_SNAPSHOT_*` environment values. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay patch, and rewrites stdout, comparable session-log expected outputs, and owned prompt and tool-schema sidecars from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md). +Each composition owner ships a `cordis.snapshot.yml` replay patch next to its live patch. The launcher applies the live base patch and the selected replay sibling under `DSH_SNAPSHOT=replay` ([single-source replay config Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md)); [`dsh-llm-replay`](../llm-replay/README.md) serves fixtures named by the `DSH_SNAPSHOT_*` environment values. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay patch, and rewrites stdout, comparable session-log expected outputs, and owned prompt and tool-schema sidecars from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md). Constraints: `suite.ts` and `harness.ts` import vitest (the harness polls its durable-boundary waits through `vi.waitFor`), so the package entry is importable only inside a vitest run (the launcher and normalizers have no such dependency but ship from the same entry). The launcher and suite factory are ACP-specific by design — the launcher speaks the SDK's `ClientSideConnection` — while the normalizers are transport-neutral session-log/text helpers also consumed by the JSON-RPC and Web snapshot recorders. Input scripts cover initialization, fresh-session creation, shorthand text prompts, exact structured ACP prompt blocks, cancellation, expected RPC failures, and durable turn-boundary waits. Permission round-trips are a FIFO queue of option-kind selections (`allow_once`, `reject_once`, …) mapped to the agent-issued `optionId`; an absent or exhausted queue answers `cancelled`, and an unoffered kind rejects the run. diff --git a/packages/test-support/session-snapshot/README.zh.md b/packages/test-support/session-snapshot/README.zh.md index f5167a26f1..b5ff8b957d 100644 --- a/packages/test-support/session-snapshot/README.zh.md +++ b/packages/test-support/session-snapshot/README.zh.md @@ -58,7 +58,7 @@ defineAcpSnapshotSuite({ }) ``` -启动不同 profile 组合的场景会设置自己的 `configPath` patch(其 basename 仍以 `cordis.yml` 结尾,使启动器可找到同级 `*cordis.snapshot.yml`);当该组合改变请求 header 时,还会设置自己的 `headerClass` 和 pin 场景,acp-agent 示例的 Code Mode 与文件系统场景是模板。默认生成的 workspace 在会话 fixture 中存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;当临时目录授权自身待测时,`workspaceParent` 将生成 cwd 移出平台临时区域,在 fixture 中保留该显式路径,并仍归父级所有,而 harness 只移除生成的子级。场景签入的 `workspace/` 会先复制到该子级,随后 `prepareWorkspace` 在 agent 启动前针对生成 cwd 运行。此 hook 仅用于 Git 无法跨平台表示的 fixture;普通种子应留在 `workspace/` 中,而生成路径在 Windows 上无效时还必须搭配 `posixOnly`。 +启动不同 profile 组合的场景会设置自己的 `configPath` patch(其 basename 仍以 `cordis.yml` 结尾,使启动器可找到同级 `*cordis.snapshot.yml`);当该组合改变请求 header 时,还会设置自己的 `headerClass` 和 pin 场景,顶层各 profile 语料包含当前模板。默认生成的 workspace 在会话 fixture 中存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;当临时目录授权自身待测时,`workspaceParent` 将生成 cwd 移出平台临时区域,在 fixture 中保留该显式路径,并仍归父级所有,而 harness 只移除生成的子级。场景签入的 `workspace/` 会先复制到该子级,随后 `prepareWorkspace` 在 agent 启动前针对生成 cwd 运行。此 hook 仅用于 Git 无法跨平台表示的 fixture;普通种子应留在 `workspace/` 中,而生成路径在 Windows 上无效时还必须搭配 `posixOnly`。 每个 pin 默认拥有其生成的 `system-prompt.expected.md` 或 `tool-schemas.expected.json`;当完整的对应序列相同时,`systemPromptSource` 和 `toolSchemasSource` 指定另一个 pin 作为来源,因此每个不同版本只提交一次。该 pin 的 `session.jsonl` 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因和任何模型可见前缀。具有合法运行中 header 变更的 pin 声明 `expectedHeaderChanges`;共享来源必须声明相同的 header 变更数量,录制/刷新会拒绝生成不同字节的共享引用方。 @@ -66,7 +66,7 @@ defineAcpSnapshotSuite({ 每个场景都比较 `stdout.expected.jsonl`,其中以 cwd 为根的分隔符规范化为 `/`。在 Windows 上,`pinsNativeWindowsStdout` 还会在共享预期输出之后比较完整 `stdout.expected.windows.jsonl`,并且仅在启用时要求存在该伴随文件。需要非 Windows 主机的场景声明 `posixOnly`,在 Windows 上跳过运行测试,但 fixture 保护仍在所有平台覆盖其已提交文件;示例包括 POSIX 进程语义(例如取消正在运行的 bash 调用会终止一个已脱离的进程组)和 Windows 无法表示的生成路径。组合需要可用 `pwsh` 的场景声明 `pwshOnly`;调用方提供的 `hasPwsh` 探测(随附的 acp-agent 套件遵循执行器自身的解析,因此 Program Files 安装也计入)在解析不到可用 `pwsh` 时跳过运行测试,而 fixture 保护仍处处覆盖其已提交文件。 -示例还在 live patch 旁提供 `cordis.snapshot.yml` replay patch。`DSH_SNAPSHOT=replay` 下,启动器应用 live 基础 patch 和所选场景的 replay 同级文件(见[单源回放配置 Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md));[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供由 `DSH_SNAPSHOT_*` 环境值指向的 fixture。`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并重写已记录场景的模型 fixture;`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay,并从已提交模型脚本重写 stdout、可比较会话日志预期输出,以及各 pin 自有的提示词与工具 schema 伴随文件。Fixture 角色、录制/回放/刷新语义和场景表字段记录在 `Scenario` 以及[快照 Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md) 中。 +每个组合所有者都在 live patch 旁提供 `cordis.snapshot.yml` replay patch。`DSH_SNAPSHOT=replay` 下,启动器应用 live 基础 patch 和所选场景的 replay 同级文件(见[单源回放配置 Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md));[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供由 `DSH_SNAPSHOT_*` 环境值指向的 fixture。`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并重写已记录场景的模型 fixture;`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay,并从已提交模型脚本重写 stdout、可比较会话日志预期输出,以及各 pin 自有的提示词与工具 schema 伴随文件。Fixture 角色、录制/回放/刷新语义和场景表字段记录在 `Scenario` 以及[快照 Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md) 中。 约束:`suite.ts` 与 `harness.ts` 导入 vitest(harness 通过 `vi.waitFor` 轮询其持久边界等待),因此包入口只能在 vitest 运行中导入(启动器和规范化器没有此依赖,但从同一入口发布)。启动器和套件工厂按设计专用于 ACP,启动器使用 SDK 的 `ClientSideConnection`;规范化器是与传输无关的会话日志/文本辅助工具,还由 JSON-RPC 和 Web 快照录制器消费。输入脚本覆盖初始化、新建会话、文本提示简写、精确结构化 ACP 提示词块、取消、预期 RPC 失败和持久轮次边界等待。权限往返是选项类别选择(`allow_once`、`reject_once` 等)的 FIFO 队列,映射到 agent 发出的 `optionId`;缺少或耗尽的队列回答 `cancelled`,未提供类别会拒绝运行。 diff --git a/packages/test-support/session-snapshot/src/harness.ts b/packages/test-support/session-snapshot/src/harness.ts index 495ea5c90c..628d334337 100644 --- a/packages/test-support/session-snapshot/src/harness.ts +++ b/packages/test-support/session-snapshot/src/harness.ts @@ -1,7 +1,7 @@ /** * Shared subprocess harness for ACP snapshot suites. A library module driven by * the suite factory in ./suite.ts (and directly by harness-level specs); each - * example's `*.snapshot.ts` names its own agent-under-test paths. + * profile adapter names its own agent-under-test paths. * * It boots the REAL agent bin subprocess via the cordis Loader (so the * export-shape bug class stays guarded — see docs/postmortem/0001), drives it diff --git a/packages/test-support/session-snapshot/src/index.ts b/packages/test-support/session-snapshot/src/index.ts index ea5b926357..053ab53493 100644 --- a/packages/test-support/session-snapshot/src/index.ts +++ b/packages/test-support/session-snapshot/src/index.ts @@ -8,7 +8,7 @@ * factory ({@link defineAcpSnapshotSuite}) that registers a scenario table as a * full describe/it tree. Transport-neutral normalizers and fixture invariants * remain reusable by other profile adapters. Ordinary ACP e2e tests can use the launcher directly; - * an example's `*.snapshot.ts` supplies only its {@link AgentUnderTest} paths, + * the ACP corpus adapter supplies only its {@link AgentUnderTest} paths, * snapshots directory, and {@link Scenario} table. * * NOTE: ./suite.ts imports vitest, so this package is importable only inside a diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 1535b27ba3..a5fcffb72d 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -387,7 +387,7 @@ describe('Node 24 lane ownership', () => { const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers')) expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({ - workers: 10, + workers: 11, source: 'ci-consumers gate count', }) expect(subject.map(item => item.id)).toEqual([ @@ -397,6 +397,7 @@ describe('Node 24 lane ownership', () => { 'built-package-invariants', 'lint-and-duplication', 'snapshot', + 'golden', 'web-snapshot', 'doc-typecheck', 'node-next-types', @@ -413,6 +414,7 @@ describe('Node 24 lane ownership', () => { expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants']) for (const id of [ 'snapshot', + 'golden', 'web-snapshot', 'doc-typecheck', 'node-next-types', @@ -421,6 +423,7 @@ describe('Node 24 lane ownership', () => { expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants']) } expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' }) + expect(subject.find(item => item.id === 'golden')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' }) expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({ DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1', }) diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 45af4dc99e..8da69710d8 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -241,6 +241,7 @@ export function gatesForMode(selected: Mode): Gate[] { pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }), pnpmScript('duplication', 'duplication'), snapshotGate(), + goldenGate(), pnpmScript('build', 'build'), pnpmScript('build:web', 'build:web'), ...hygieneLeafGates({ artifactNeeds: ['build'] }), @@ -427,6 +428,7 @@ function ciConsumerGates(): Gate[] { needs: validatedBuild, }), snapshotGate(validatedBuild), + goldenGate(validatedBuild), webSnapshotGate(validatedBuild), pnpmScript('doc-typecheck', 'doc-typecheck:contracts-ready', { needs: validatedBuild, @@ -585,9 +587,8 @@ function coverageGates(): Gate[] { ] } -// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node, -// plugins via real exports); script snapshots execute their real source entry path. -// Callers wait either on `build` or on a validation gate that transitively owns that build. +// Recorded-session adapters boot process scenarios in `lib` mode. Callers wait +// either on `build` or on a validation gate that transitively owns that build. function snapshotGate(needs: string[] = ['build']): Gate { return pnpmScript('snapshot', 'test:snapshot', { env: { DSH_EXAMPLE_MODE: 'lib' }, @@ -595,6 +596,15 @@ function snapshotGate(needs: string[] = ['build']): Gate { }) } +// Owner-local process goldens consume built package exports without entering +// the recorded-session corpus or the credentialed provider lane. +function goldenGate(needs: string[] = ['build']): Gate { + return pnpmScript('golden', 'test:golden', { + env: { DSH_EXAMPLE_MODE: 'lib' }, + needs, + }) +} + function builtPackageInvariantsGate(needs?: string[]): Gate { return pnpmScript('built-package-invariants', 'verify-built-package-invariants', { label: 'built package invariants', diff --git a/scripts/session-fixture-layout.snapshot.ts b/scripts/session-fixture-layout.snapshot.ts deleted file mode 100644 index ae8d11c907..0000000000 --- a/scripts/session-fixture-layout.snapshot.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** Repository-wide canonical-layout check for committed session snapshots. */ - -import { resolve } from 'node:path' -import { expect, it } from 'vitest' -import { inspectSessionFixtureLayouts } from './session-fixture-layout.ts' - -const root = resolve(import.meta.dirname, '..') - -it('keeps every session-format JSONL fixture projected into canonical packed layout', () => { - const nonCanonical = inspectSessionFixtureLayouts(root) - .filter(fixture => fixture.source !== fixture.canonical) - .map(fixture => fixture.path) - expect( - nonCanonical, - 'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.', - ).toEqual([]) -}) diff --git a/scripts/session-fixture-layout.spec.ts b/scripts/session-fixture-layout.spec.ts index 05dfd7a4d0..2be3f469ca 100644 --- a/scripts/session-fixture-layout.spec.ts +++ b/scripts/session-fixture-layout.spec.ts @@ -1,9 +1,15 @@ +import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { type SessionEvent } from '@deepseek-ai/dsh-session' import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay' -import { canonicalSessionFixture, isPhysicalSessionFixture } from './session-fixture-layout.ts' +import { + canonicalSessionFixture, + inspectSessionFixtureLayouts, + isPhysicalSessionFixture, +} from './session-fixture-layout.ts' const HEADER = ' {"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0} ' +const root = resolve(import.meta.dirname, '..') function chunkRun(): SessionEvent[] { return Array.from({ length: 4 }, (_, index) => ({ @@ -80,3 +86,13 @@ describe('isPhysicalSessionFixture', () => { expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false) }) }) + +it('keeps every session-format JSONL fixture projected into canonical packed layout', () => { + const nonCanonical = inspectSessionFixtureLayouts(root) + .filter(fixture => fixture.source !== fixture.canonical) + .map(fixture => fixture.path) + expect( + nonCanonical, + 'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.', + ).toEqual([]) +}) diff --git a/scripts/session-snapshot-corpus.snapshot.ts b/scripts/session-snapshot-corpus.corpus.ts similarity index 85% rename from scripts/session-snapshot-corpus.snapshot.ts rename to scripts/session-snapshot-corpus.corpus.ts index fb89d36173..ebabe08d2f 100644 --- a/scripts/session-snapshot-corpus.snapshot.ts +++ b/scripts/session-snapshot-corpus.corpus.ts @@ -18,6 +18,13 @@ import { const repoRoot = resolve(import.meta.dirname, '..') const corpusRoot = join(repoRoot, 'snapshots') const profiles = ['acp', 'sdk', 'session', 'web'] as const +const snapshotAdapters = [ + 'apps/web/tests/message-feedback-protocol.snapshot.ts', + 'apps/web/tests/minimal-preset.snapshot.ts', + 'snapshots/acp/acp.snapshot.ts', + 'snapshots/sdk/sdk.snapshot.ts', + 'snapshots/session/headless.snapshot.ts', +] as const interface Scenario { readonly key: string @@ -67,6 +74,28 @@ function referencedScenario(owner: Scenario, source: string): string { return source.includes('/') ? source : `${owner.profile}/${source}` } +async function snapshotNamedTests(): Promise { + const files: string[] = [] + const visit = async (directory: string, relativeDir: string): Promise => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (['dist', 'lib', 'node_modules'].includes(entry.name)) continue + await visit(join(directory, entry.name), join(relativeDir, entry.name)) + } else if (entry.isFile() && /\.snapshot\.tsx?$/u.test(entry.name)) { + files.push(join(relativeDir, entry.name).split(/[/\\]/u).join('/')) + } + } + } + for (const root of ['apps', 'examples', 'scripts', 'snapshots']) { + await visit(join(repoRoot, root), root) + } + return files.sort() +} + +it('reserves the snapshot test suffix for recorded-session adapters', async () => { + expect(await snapshotNamedTests()).toEqual([...snapshotAdapters]) +}) + it('keeps every recorded session owned, pinned, redacted, and header-scrubbed', async () => { const all = await scenarios() const byKey = new Map(all.map(scenario => [scenario.key, scenario])) diff --git a/scripts/translation-prompt.snapshot.ts b/scripts/translation-prompt.golden.spec.ts similarity index 100% rename from scripts/translation-prompt.snapshot.ts rename to scripts/translation-prompt.golden.spec.ts diff --git a/snapshots/AGENTS.md b/snapshots/AGENTS.md index c68adaa7d6..5bf49fe4e3 100644 --- a/snapshots/AGENTS.md +++ b/snapshots/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — Recorded-session snapshots -This tree contains only tests whose committed session JSONL is replay input and expected persisted output. Keep non-session ARIA, geometry, generator, CLI, and unit expected output with its owning app, script, or package as a golden. +This tree contains only tests whose committed session JSONL is replay input and expected persisted output. Keep non-session ARIA, geometry, generator, CLI, and unit expected output with its owning app, script, or package as a golden; use `test:golden`, `test:web`, or `test` for its owning tier. Every process under test starts through the `dsh` CLI with a shipped profile and optional scenario patches. Test clients may drive a public protocol or browser interface; do not add another application entrypoint, hidden CLI mode, or executable scenario driver. diff --git a/snapshots/acp/acp.snapshot.ts b/snapshots/acp/acp.snapshot.ts new file mode 100644 index 0000000000..6587170fee --- /dev/null +++ b/snapshots/acp/acp.snapshot.ts @@ -0,0 +1,63 @@ +/** Recorded ACP protocol behavior through the shipped `dsh --profile acp` interface. */ + +import { fileURLToPath } from 'node:url' +import { + defineAcpSnapshotSuite, + type Scenario, + type SnapshotSuiteOptions, +} from '@deepseek-ai/dsh-session-snapshot' + +const corpusDir = fileURLToPath(new URL('./', import.meta.url)) +const compositionDir = fileURLToPath(new URL('./escalation-approved/', import.meta.url)) + +function snapshotMode(value: string | undefined): SnapshotSuiteOptions['mode'] { + switch (value) { + case undefined: + case '': + case 'replay': return 'replay' + case 'record': return 'record' + case 'refresh': return 'refresh' + default: throw new Error(`unknown DSH_SNAPSHOT mode: ${value}`) + } +} + +const scenarios: Scenario[] = [ + { name: 'handshake', hasModelTurn: false, recorded: false, headerClass: 'sandbox' }, + { name: 'reject-extra-dirs', hasModelTurn: false, recorded: false, headerClass: 'sandbox' }, + { name: 'cancel', hasModelTurn: true, recorded: false, overridden: true, headerClass: 'sandbox' }, + { name: 'cancel-tool-calls', hasModelTurn: true, recorded: false, overridden: true, headerClass: 'sandbox', posixOnly: true }, + { + name: 'escalation-approved', + hasModelTurn: true, + recorded: true, + pinsHeader: true, + headerClass: 'sandbox', + env: { DSH_PERMISSION_MODE: 'workspace-write' }, + }, + { + name: 'escalation-rejected', + hasModelTurn: true, + recorded: true, + headerClass: 'sandbox', + env: { DSH_PERMISSION_MODE: 'workspace-write' }, + }, + { + name: 'fs-escalation-approved', + hasModelTurn: true, + recorded: true, + headerClass: 'sandbox', + env: { DSH_PERMISSION_MODE: 'workspace-write' }, + }, +] + +defineAcpSnapshotSuite({ + agent: { + binScript: fileURLToPath(new URL('../../apps/cli/src/bin.ts', import.meta.url)), + configPath: fileURLToPath(new URL('./escalation-approved/cordis.yml', import.meta.url)), + profile: 'acp', + tsconfigPath: fileURLToPath(new URL('../../tsconfig.json', import.meta.url)), + }, + snapshotsDir: corpusDir, + scenarios, + mode: snapshotMode(process.env.DSH_SNAPSHOT), +}) diff --git a/examples/acp-agent/cordis.snapshot.yml b/snapshots/acp/escalation-approved/cordis.snapshot.yml similarity index 94% rename from examples/acp-agent/cordis.snapshot.yml rename to snapshots/acp/escalation-approved/cordis.snapshot.yml index 37cca61429..512708ce64 100644 --- a/examples/acp-agent/cordis.snapshot.yml +++ b/snapshots/acp/escalation-approved/cordis.snapshot.yml @@ -41,4 +41,4 @@ - id: deepseek-v4-pro - id: subagent-settlement-marker - name: './tests/fixtures/subagent-settlement-marker.ts' + name: './subagent-settlement-marker.ts' diff --git a/snapshots/acp/escalation-approved/cordis.yml b/snapshots/acp/escalation-approved/cordis.yml new file mode 100644 index 0000000000..0f07b6fec5 --- /dev/null +++ b/snapshots/acp/escalation-approved/cordis.yml @@ -0,0 +1,91 @@ +# ACP snapshot-record patch over the shipped `acp` profile. The dsh +# launcher owns environment loading, plugin resolution, and process shutdown; +# stdout remains reserved for ACP JSON-RPC. + +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + thinking: enabled + reasoningEffort: max + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro + - id: deepseek-v4-flash-vision-exp + inputModalities: [text, image] + +- id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')" + workspaceRoot: !!js process.cwd() + +- id: approval + name: '@deepseek-ai/dsh-user-approval' + config: + policy: !!js "(process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')) === 'danger-full-access' ? 'never' : 'ask'" + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? dshHomePath('sessions') + compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'" + +- id: acp + name: '@deepseek-ai/dsh-acp' + config: + provider: deepseek-official + model: deepseek-v4-pro + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + config: + persona: | + You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. + + Verify your work by running the code or tests. Keep answers brief and factual. + +- id: agent-instructions + name: '@deepseek-ai/dsh-agent-instructions' + config: + maxBytes: 65536 + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + enableModelSelection: true + backgroundMode: continuable + maxDepth: 1 + +# Fork omits model selection so provider/model stay equal to the parent and the +# inherited history remains eligible for KV Cache reuse. It stays one-shot because +# a continuable child's `report` tool and prompt section precede that history and +# invalidate the same prefix. `run_in_background` is off as an explicit foreground-only +# choice even though the shipped ACP profile mounts the generic Job runtime. +# See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md +# and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. +- id: tool-subagent-fork + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork + backgroundMode: one-shot + enableRunInBackground: false + maxDepth: 1 + +- id: fs-sandbox + name: '@deepseek-ai/dsh-fs-sandbox' + config: + cwd: !!js process.cwd() + +- insert: + - id: hooks-claude-code + name: '@deepseek-ai/dsh-hooks-claude-code' + config: + configPath: ./hooks.json + + - id: hooks-codex + name: '@deepseek-ai/dsh-hooks-codex' + config: + configPath: ./codex-hooks.json diff --git a/examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts b/snapshots/acp/escalation-approved/subagent-settlement-marker.ts similarity index 100% rename from examples/acp-agent/tests/fixtures/subagent-settlement-marker.ts rename to snapshots/acp/escalation-approved/subagent-settlement-marker.ts diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index f898d2d9da..aa275d44e4 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -43,6 +43,7 @@ export default defineConfig({ // apps/cli only, not apps/*: apps/web/tests/*.e2e.ts needs the built // frontend dist and runs under vitest.web.config.ts (the test:web job). include: ['packages/*/*/tests/**/*.e2e.ts', 'apps/cli/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'], + exclude: ['**/*.golden.e2e.ts'], // Real model calls: generous timeouts, and retries for transient flakes // (the shared internal key hits concurrency quotas). No coverage — the // unit suites own the coverage gate. diff --git a/vitest.golden.config.ts b/vitest.golden.config.ts new file mode 100644 index 0000000000..64cb3b7694 --- /dev/null +++ b/vitest.golden.config.ts @@ -0,0 +1,20 @@ +import { availableParallelism } from 'node:os' +import tsconfigPaths from 'vite-tsconfig-paths' +import { defineConfig } from 'vitest/config' +import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts' + +/** Owner-local assembled expected-output tests that do not use a recorded session as their input. */ +export default defineConfig({ + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] }), standardDecoratorPlugin()], + test: { + execArgv: vitestExecArgv, + setupFiles: ['./scripts/test-invariants.ts'], + include: [ + 'apps/cli/tests/**/*.golden.e2e.ts', + 'examples/*/tests/**/*.golden.e2e.ts', + ], + testTimeout: 120_000, + hookTimeout: 30_000, + maxWorkers: Math.min(5, availableParallelism()), + }, +}) diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index 1a89c7607e..8295960ca2 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -45,12 +45,10 @@ export default defineConfig({ execArgv: vitestExecArgv, setupFiles: ['./scripts/test-invariants.ts'], include: [ - 'scripts/**/*.snapshot.ts', + 'scripts/session-snapshot-corpus.corpus.ts', // The assembled Web snapshot executes generated client bundles; source // mode remains the zero-build path, while lib mode requires a prior build. ...(process.env.DSH_EXAMPLE_MODE === 'lib' ? ['apps/web/tests/**/*.snapshot.ts'] : []), - 'apps/cli/tests/**/*.snapshot.ts', - 'examples/*/tests/**/*.snapshot.ts', 'snapshots/**/*.snapshot.ts', ], // Replay never writes committed outputs and every scenario owns its