docs: move execa Agent Note to implemented; update inbound links and README contracts

- proposed/testing -> implemented/testing with the lifecycle rewrite
  (Proposal->Decision in present tense, Acceptance criteria + Risks
  folded into Consequences); zh counterpart mirrored and both pairs
  re-recorded.
- the rejected NIH-audit roll-up pair now links the implemented/ path.
- loader-smoke README: captured output is bounded by execa's default
  100 MB maxBuffer, no longer unbounded.
- acp-snapshot README: harness.ts now also imports vitest (vi.waitFor),
  so the vitest-run-only constraint names both modules.
- jsonrpc keyless smoke: raise the invalid-env case's subprocess
  deadline to 25s (the 9s pick starved a cold tsx boot on slow NFS).
This commit is contained in:
Tianyi Cui
2026-07-26 23:10:38 +08:00
parent c4647a8609
commit a8a1ada183
15 changed files with 90 additions and 98 deletions

View File

@@ -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
2026-07-26-execa-for-test-subprocess-plumbing.md: 99a86258fe4d59db6a0e144dbcee94c095f70f8f
2026-07-26-execa-for-test-subprocess-plumbing.zh.md: 525e09f07ce3e5dc61f1cadab5c11ea0790cccee
2026-07-26-execa-for-test-subprocess-plumbing.md: a25010b1cab7012cf9c659cfd8272d17e33618c5
2026-07-26-execa-for-test-subprocess-plumbing.zh.md: 733c9f7e7f666052f030ed3b0f916e4832aaa120

View File

@@ -0,0 +1,37 @@
# Agent Note: Adopt execa for hand-rolled test subprocess plumbing
Status: implemented
English | [中文](2026-07-26-execa-for-test-subprocess-plumbing.zh.md)
## Problem
Roughly ten e2e/smoke files re-derived the same spawn-collect-timeout choreography by hand: `let stdout = ''` accumulation with `setEncoding` and `data` handlers, a `setTimeout``kill('SIGKILL')` deadline, and `once('exit')`/`once('error')` settlement, each with small variations. The sites: the inner spawn block of `runLoaderSmoke` (`packages/support/loader-smoke/src/index.ts`), `runBuiltBin` in `apps/cli/tests/built-bin.e2e.ts` and `packages/examples/cli-demo/tests/built-bin.e2e.ts`, `runBinExpectingExit` in `packages/examples/acp-demo/tests/built-bin.e2e.ts`, the built-lib e2e helpers in `lsp-local` and `code-runtime-worker`, the outer collector of `examples/tui-agent/tests/pty-harness.ts`, `examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts`, and partially `apps/web/tests/smoke-real.e2e.ts` and `session-checkpoint-policy/tests/crash-recovery.e2e.ts`.
Two related test-infra hand-rolls compounded the case:
- `packages/support/llm-mock-server/src/cli.ts` hand-tokenized 17 value-taking `--flag value` options plus boolean flags (~4560 lines of loop and value-extraction helpers) where the `node:util` `parseArgs` builtin is already the repo idiom (`cli-demo`, `acp-demo`, `verify-runtime-closure.ts`, `packages/sdk/scripts`).
- `apps/web/tests/smoke-real.e2e.ts` and `apps/web/tests/scaffold.ts` carried two verbatim copies of a regex `.env` parser (~20 lines) where the `process.loadEnvFile` builtin has exactly the required no-override semantics — and the vitest e2e/snapshot/web configs already load root `.env` with it before these files run, making the copies dead.
- The snapshot harness hand-rolled three poll-until-deadline loops (`waitForPersistedTurnStart`/`waitForPersistedTurnEnd`/`waitForWorkspaceFile` in `packages/support/acp-snapshot/src/harness.ts`, ~55 lines) plus `waitForFile` in `crash-recovery.e2e.ts`, where `vi.waitFor`/`expect.poll` cover the shape — vitest is already a runtime dependency of `dsh-acp-snapshot`, so this adds nothing.
## Decision
- `execa` is a root devDependency and a runtime dependency of `@deepseek-ai/dsh-loader-smoke` (the one `src/` consumer). The listed spawn-collect-timeout sites run through `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })`, whose result reports `{ stdout, stderr, exitCode, signal, timedOut, failed }` as independent fields — matching the repo's own defensive-patterns rule to report orthogonal subprocess outcomes independently. `runLoaderSmoke` passes `input: ''` for its stdin-close contract, and sites whose assertions pin exact stream bytes pass `stripFinalNewline: false`.
- The genuinely custom parts stay custom on top of an execa-owned subprocess: cli-demo's interrupt-on-marker mid-stream logic, jsonrpc's line-predicate protocol driving, and crash-recovery's SIGKILL-at-failpoint choreography. `smoke-real.e2e.ts` keeps raw `spawn` for its three long-lived interactive servers — ready-line watching across both streams plus a staged SIGTERM→await→SIGKILL teardown are the whole site, so execa would delete nothing there; its share of this note is the dead `.env` parser.
- `llm-mock-server`'s CLI tokenizes via `parseArgs` (strict, no positionals); numeric coercion, bounds, and cross-option constraints stay manual, and the pinned error-message tests carry `parseArgs`'s own tokenizer texts.
- Both `loadRootEnv` copies are deleted outright: the owning vitest configs (`vitest.web.config.ts` unconditionally, `vitest.snapshot.config.ts` in record mode) load the repo-root `.env` before those files run.
- The four poll loops ride `vi.waitFor` with explicit `{ interval, timeout }` and descriptive errors thrown from the callback; `waitForPersistedTurnStart` captures its malformed-record validation error out of the retry loop so it fails the run immediately instead of being retried until the deadline.
## Alternatives considered
- **`tinyexec` instead of execa.** Already in `node_modules` transitively via vitest, smaller API — but no kill-escalation, no rich error output embedding, and being transitive is not a contract; if the lighter package is preferred the swap shape is identical.
- **A repo-local shared spawn helper (no new dep).** Viable and cheaper on supply chain, but it keeps the maintenance of deadline/kill/settlement logic in-repo when a battle-tested package owns exactly this; contrary to the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md), it also has to re-earn Windows behavior (taskkill, exit codes) that execa already carries.
- **`get-port`, `wait-on`, `tempy`, `tree-kill`.** Rejected individually: the repo's single port probe is break-even, the file waits are dominated by `vi.waitFor`, temp-dir handling already uses `mkdtemp` + `rm {recursive}` builtins everywhere, and acp-snapshot's `close()` is drain-ordering logic, not tree traversal.
## Consequences
- The hand-rolled collect/timeout blocks are gone, including the two `/* v8 ignore */` un-inducible OS-error branches in `loader-smoke`: spawn and stream failures settle through execa's result fields, so the `src/` file carries no coverage exemptions and the per-file gate covers every remaining branch.
- Captured output is bounded by execa's default 100 MB `maxBuffer` (overflow terminates the subprocess) where it was previously unbounded; the `loader-smoke` README's limitation entry reflects this.
- Windows termination behavior (taskkill, exit-code mapping) is owned by execa instead of per-site hand-rolls; each rewritten suite was re-run on POSIX in this change, and the Windows CI lanes own the other platform.
- execa is a new root devDependency (previously absent from the lockfile); it is one of the most-depended-on packages on npm and actively maintained, and the exe/runtime closure is unaffected (tests only).
- The mock-server CLI's tokenizer-level error texts are no longer this repo's to choose: unknown options, missing values, and stray positionals report `parseArgs`'s wording, pinned as such in `tests/cli.spec.ts`.

View File

@@ -0,0 +1,37 @@
# Agent Note: 采用 execa 替换手写的测试子进程管道代码
Status: implemented
[English](2026-07-26-execa-for-test-subprocess-plumbing.md) | 中文
## 问题
大约十个 e2e/冒烟测试文件各自手工重写过同一套「spawn、收集输出、超时终止」编排`setEncoding``data` 处理器做 `let stdout = ''` 式累积,用 `setTimeout``kill('SIGKILL')` 设定超时截止,再以 `once('exit')`/`once('error')` 结算结果,各处只有细微差别。这些位置是:`runLoaderSmoke` 的内层 spawn 代码块(`packages/support/loader-smoke/src/index.ts`)、`apps/cli/tests/built-bin.e2e.ts``packages/examples/cli-demo/tests/built-bin.e2e.ts` 中的 `runBuiltBin``packages/examples/acp-demo/tests/built-bin.e2e.ts` 中的 `runBinExpectingExit``lsp-local``code-runtime-worker` 中基于构建产物的 e2e 辅助函数、`examples/tui-agent/tests/pty-harness.ts` 的外层收集器、`examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts`,以及部分涉及的 `apps/web/tests/smoke-real.e2e.ts``session-checkpoint-policy/tests/crash-recovery.e2e.ts`
另有两处相关的测试基础设施手写代码进一步强化了替换的理由:
- `packages/support/llm-mock-server/src/cli.ts` 曾手工逐个切分 17 个带值的 `--flag value` 选项外加若干布尔标志(约 4560 行的循环与取值辅助函数),而 `node:util` 内置的 `parseArgs` 早已是本仓库的惯用写法(`cli-demo``acp-demo``verify-runtime-closure.ts``packages/sdk/scripts`)。
- `apps/web/tests/smoke-real.e2e.ts``apps/web/tests/scaffold.ts` 曾携带两份逐字相同的正则 `.env` 解析器拷贝(约 20 行),而内置的 `process.loadEnvFile` 恰好具备所需的「不覆盖已有值」语义;并且 vitest 的 e2e/snapshot/web 配置在这些文件运行之前就已用它加载了根 `.env`,这两份拷贝实为死代码。
- 快照 harness 曾手写三个「轮询直到截止时间」的循环(`packages/support/acp-snapshot/src/harness.ts` 中的 `waitForPersistedTurnStart`/`waitForPersistedTurnEnd`/`waitForWorkspaceFile`,约 55 行),外加 `crash-recovery.e2e.ts` 中的 `waitForFile`,而 `vi.waitFor`/`expect.poll` 正好覆盖这种形态vitest 本来就是 `dsh-acp-snapshot` 的运行时依赖,因此这不新增任何东西。
## 决定
- `execa` 是根 devDependency同时是 `@deepseek-ai/dsh-loader-smoke`(唯一的 `src/` 消费者)的运行时依赖。上述 spawn、收集、超时的代码位置统一经由 `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })` 运行:其结果以相互独立的字段报告 `{ stdout, stderr, exitCode, signal, timedOut, failed }`,与本仓库防御模式中「正交的子进程结果各自独立上报」的规则一致。`runLoaderSmoke``input: ''` 以兑现其 stdin 关闭契约;断言固定精确流字节的位置传 `stripFinalNewline: false`
- 真正定制的部分继续保持定制,只是架在 execa 拥有的子进程之上cli-demo 在流中遇到标记即中断的逻辑、jsonrpc 基于行谓词的协议驱动,以及 crash-recovery 在故障点发送 SIGKILL 的编排。`smoke-real.e2e.ts` 的三个长驻交互式服务器保留原生 `spawn`——跨双流监听就绪行加上分级的 SIGTERM→等待→SIGKILL 拆除就是该处的全部内容execa 在那里删不掉任何东西;它在本 note 中的份额是那份死的 `.env` 解析器。
- `llm-mock-server` 的 CLI 经由 `parseArgs` 切分strict、不允许位置参数数值转换、边界检查与跨选项约束仍手工实现被固定的错误消息测试改为携带 `parseArgs` 自己的切分器文本。
- 两份 `loadRootEnv` 拷贝被整体删除:拥有它们的 vitest 配置(`vitest.web.config.ts` 无条件、`vitest.snapshot.config.ts` 在 record 模式下)在这些文件运行之前就加载了仓库根部的 `.env`
- 那四个轮询循环改乘 `vi.waitFor`,显式传入 `{ interval, timeout }`,并在回调中抛出带描述信息的错误;`waitForPersistedTurnStart` 把「持久化记录格式非法」的校验错误捕获到重试循环之外,使其立即让运行失败,而不是被重试到截止时间。
## 曾考虑的替代方案
- **用 `tinyexec` 代替 execa。**它已经作为 vitest 的传递依赖存在于 `node_modules`API 也更小;但它没有终止信号逐级升级,不会把丰富的输出嵌入错误对象,而且传递依赖并不构成契约。如果最终更倾向这个更轻的包,替换的形态完全相同。
- **仓库内共享的 spawn 辅助函数(不引入新依赖)。**可行,供应链成本也更低,但当一个久经实战的包恰好负责这件事时,它把截止时限、终止与结算逻辑的维护留在了仓库内;这与[依赖策略](../process/2026-07-26-dependencies-over-hand-rolling.md)背道而驰,它还得重新踩坑换来 execa 已经自带的 Windows 行为taskkill、退出码
- **`get-port``wait-on``tempy``tree-kill`。**逐一不予采纳:仓库仅有的一处端口探测替换后收支相抵;文件等待场景已由 `vi.waitFor` 更优地覆盖;临时目录处理在各处已经使用内置的 `mkdtemp` + `rm {recursive}`acp-snapshot 的 `close()` 是排空顺序逻辑,不是进程树遍历。
## 后果
- 手写的收集/超时代码块全部移除,包括 `loader-smoke` 中两个标注 `/* v8 ignore */`、无法人为诱发的 OS 错误分支spawn 与流故障如今经由 execa 的结果字段结算,这个 `src/` 文件不再携带任何覆盖率豁免,逐文件门禁覆盖其余全部分支。
- 捕获的输出如今受 execa 默认 100 MB `maxBuffer` 约束(溢出即终止子进程),此前是无界的;`loader-smoke` README 的局限条目反映了这一点。
- Windows 终止行为taskkill、退出码映射由 execa 拥有,不再逐处手写;每个改写后的套件在本次变更中已在 POSIX 上重新运行,另一平台由 Windows CI 车道负责。
- execa 是新增的根 devDependency此前完全不存在于 lockfile 中);它是 npm 上被依赖最多的包之一且维护活跃exe/运行时闭包不受影响(仅测试使用)。
- mock-server CLI 切分器层面的错误文本不再由本仓库决定:未知选项、缺失取值与多余位置参数报告 `parseArgs` 的措辞,并在 `tests/cli.spec.ts` 中如此固定。

View File

@@ -1,41 +0,0 @@
# Agent Note: Adopt execa for hand-rolled test subprocess plumbing
Status: proposed
English | [中文](2026-07-26-execa-for-test-subprocess-plumbing.zh.md)
## Problem
Roughly ten e2e/smoke files re-derive the same spawn-collect-timeout choreography by hand: `let stdout = ''` accumulation with `setEncoding` and `data` handlers, a `setTimeout``kill('SIGKILL')` deadline, and `once('exit')`/`once('error')` settlement, each with small variations. The sites: the inner spawn block of `runLoaderSmoke` (`packages/support/loader-smoke/src/index.ts`), `runBuiltBin` in `apps/cli/tests/built-bin.e2e.ts` and `packages/examples/cli-demo/tests/built-bin.e2e.ts`, `runBinExpectingExit` in `packages/examples/acp-demo/tests/built-bin.e2e.ts`, the built-lib e2e helpers in `lsp-local` and `code-runtime-worker`, the outer collector of `examples/tui-agent/tests/pty-harness.ts`, `examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts`, and partially `apps/web/tests/smoke-real.e2e.ts` and `session-checkpoint-policy/tests/crash-recovery.e2e.ts`. Net deletable: ~100150 lines of test infrastructure.
Two related test-infra hand-rolls compound the case:
- `packages/support/llm-mock-server/src/cli.ts` hand-tokenizes 17 value-taking `--flag value` options plus boolean flags (~4560 lines of loop and value-extraction helpers) where the `node:util` `parseArgs` builtin is already the repo idiom (`cli-demo`, `acp-demo`, `verify-runtime-closure.ts`, `packages/sdk/scripts`).
- `apps/web/tests/smoke-real.e2e.ts` and `apps/web/tests/scaffold.ts` carry two verbatim copies of a regex `.env` parser (~20 lines) where the `process.loadEnvFile` builtin has exactly the required no-override semantics — and the vitest e2e/snapshot/web configs already load root `.env` with it before these files run, making the copies arguably dead.
- The snapshot harness hand-rolls three poll-until-deadline loops (`waitForPersistedTurnStart`/`waitForPersistedTurnEnd`/`waitForWorkspaceFile` in `packages/support/acp-snapshot/src/harness.ts`, ~55 lines) plus `waitForFile` in `crash-recovery.e2e.ts`, where `vi.waitFor`/`expect.poll` cover the shape — vitest is already a runtime dependency of `dsh-acp-snapshot`, so this adds nothing.
## Proposal
- Add `execa` as a root devDependency and rewrite the spawn-collect-timeout sites onto `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })`, whose result reports `{ stdout, stderr, exitCode, signal, timedOut }` as independent fields — matching the repo's own defensive-patterns rule to report orthogonal subprocess outcomes independently. Keep the genuinely custom parts custom: cli-demo's interrupt-on-marker mid-stream logic, jsonrpc's line-predicate protocol driving, and crash-recovery's SIGKILL-at-failpoint choreography.
- Swap `llm-mock-server`'s CLI tokenizer for `parseArgs` (numeric coercion, bounds, and cross-option constraints stay manual; pinned error-message texts update with the tests).
- Delete both `loadRootEnv` copies in favor of `process.loadEnvFile` in a try/catch, or remove them outright if the vitest-config loading already covers them.
- Replace the four poll loops with `vi.waitFor`/`expect.poll`, passing explicit `{ interval, timeout }` and throwing descriptive errors from the callback.
## Alternatives considered
- **`tinyexec` instead of execa.** Already in `node_modules` transitively via vitest, smaller API — but no kill-escalation, no rich error output embedding, and being transitive is not a contract; if the lighter package is preferred the swap shape is identical.
- **A repo-local shared spawn helper (no new dep).** Viable and cheaper on supply chain, but it keeps the maintenance of deadline/kill/settlement logic in-repo when a battle-tested package owns exactly this; contrary to the [dependency policy](../../implemented/process/2026-07-26-dependencies-over-hand-rolling.md), it also has to re-earn Windows behavior (taskkill, exit codes) that execa already carries.
- **`get-port`, `wait-on`, `tempy`, `tree-kill`.** Rejected individually: the repo's single port probe is break-even, the file waits are dominated by `vi.waitFor`, temp-dir handling already uses `mkdtemp` + `rm {recursive}` builtins everywhere, and acp-snapshot's `close()` is drain-ordering logic, not tree traversal.
## Acceptance criteria
- The listed sites spawn through execa (or the chosen equivalent); the hand-rolled collect/timeout blocks and the two `/* v8 ignore */` un-inducible OS-error branches in `loader-smoke` are gone.
- `llm-mock-server` CLI parses via `parseArgs`; its cli spec passes with updated message expectations.
- No hand-rolled `.env` parser remains under `apps/web/tests`.
- The affected e2e and snapshot suites pass on both POSIX and Windows CI lanes.
## Risks
- `loader-smoke` is a `src/` file under the per-file-100% coverage gate; the swap actually simplifies its coverage story (removes un-inducible branches) but the new call shape needs coverage.
- Each rewritten e2e must be re-run on both platforms; subtle differences in kill escalation or stdin-close semantics (`input: ''` for loader-smoke's stdin-close contract) are the risk to verify per site.
- execa is a new root devDependency (currently absent from the lockfile entirely); it is one of the most-depended-on packages on npm and actively maintained, so health is not a concern, but the exe/runtime closure is unaffected either way (tests only).

View File

@@ -1,41 +0,0 @@
# Agent Note: 采用 execa 替换手写的测试子进程管道代码
Status: proposed
[English](2026-07-26-execa-for-test-subprocess-plumbing.md) | 中文
## 问题
大约十个 e2e/冒烟测试文件各自手工重写同一套「spawn、收集输出、超时终止」编排`setEncoding``data` 处理器做 `let stdout = ''` 式累积,用 `setTimeout``kill('SIGKILL')` 设定超时截止,再以 `once('exit')`/`once('error')` 结算结果,各处只有细微差别。这些位置是:`runLoaderSmoke` 的内层 spawn 代码块(`packages/support/loader-smoke/src/index.ts`)、`apps/cli/tests/built-bin.e2e.ts``packages/examples/cli-demo/tests/built-bin.e2e.ts` 中的 `runBuiltBin``packages/examples/acp-demo/tests/built-bin.e2e.ts` 中的 `runBinExpectingExit``lsp-local``code-runtime-worker` 中基于构建产物的 e2e 辅助函数、`examples/tui-agent/tests/pty-harness.ts` 的外层收集器、`examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts`,以及部分涉及的 `apps/web/tests/smoke-real.e2e.ts``session-checkpoint-policy/tests/crash-recovery.e2e.ts`。净可删除量:约 100150 行测试基础设施代码。
另有两处相关的测试基础设施手写代码进一步强化了替换的理由:
- `packages/support/llm-mock-server/src/cli.ts` 手工逐个切分 17 个带值的 `--flag value` 选项外加若干布尔标志(约 4560 行的循环与取值辅助函数),而 `node:util` 内置的 `parseArgs` 早已是本仓库的惯用写法(`cli-demo``acp-demo``verify-runtime-closure.ts``packages/sdk/scripts`)。
- `apps/web/tests/smoke-real.e2e.ts``apps/web/tests/scaffold.ts` 携带两份逐字相同的正则 `.env` 解析器拷贝(约 20 行),而内置的 `process.loadEnvFile` 恰好具备所需的「不覆盖已有值」语义;并且 vitest 的 e2e/snapshot/web 配置在这些文件运行之前就已用它加载了根 `.env`,这两份拷贝几乎可以视为死代码。
- 快照 harness 手写了三个「轮询直到截止时间」的循环(`packages/support/acp-snapshot/src/harness.ts` 中的 `waitForPersistedTurnStart`/`waitForPersistedTurnEnd`/`waitForWorkspaceFile`,约 55 行),外加 `crash-recovery.e2e.ts` 中的 `waitForFile`,而 `vi.waitFor`/`expect.poll` 正好覆盖这种形态vitest 本来就是 `dsh-acp-snapshot` 的运行时依赖,因此这不新增任何东西。
## 提案
-`execa` 添加为根 devDependency把上述 spawn、收集、超时的代码位置改写到 `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })` 上:其结果以相互独立的字段报告 `{ stdout, stderr, exitCode, signal, timedOut }`与本仓库防御模式中「正交的子进程结果各自独立上报」的规则一致。真正定制的部分继续保持定制cli-demo 在流中遇到标记即中断的逻辑、jsonrpc 基于行谓词的协议驱动,以及 crash-recovery 在故障点发送 SIGKILL 的编排。
-`llm-mock-server` 的 CLI 切分器换成 `parseArgs`(数值转换、边界检查与跨选项约束仍手工实现;被固定的错误消息文本随测试一并更新)。
- 删除两份 `loadRootEnv` 拷贝,改用包在 try/catch 中的 `process.loadEnvFile`;如果 vitest 配置的加载已经覆盖了它们,则直接整体移除。
-`vi.waitFor`/`expect.poll` 替换那四个轮询循环,显式传入 `{ interval, timeout }`,并在回调中抛出带描述信息的错误。
## 曾考虑的替代方案
- **用 `tinyexec` 代替 execa。**它已经作为 vitest 的传递依赖存在于 `node_modules`API 也更小;但它没有终止信号逐级升级,不会把丰富的输出嵌入错误对象,而且传递依赖并不构成契约。如果最终更倾向这个更轻的包,替换的形态完全相同。
- **仓库内共享的 spawn 辅助函数(不引入新依赖)。**可行,供应链成本也更低,但当一个久经实战的包恰好负责这件事时,它把截止时限、终止与结算逻辑的维护留在了仓库内;这与[依赖策略](../../implemented/process/2026-07-26-dependencies-over-hand-rolling.md)背道而驰,它还得重新踩坑换来 execa 已经自带的 Windows 行为taskkill、退出码
- **`get-port``wait-on``tempy``tree-kill`。**逐一不予采纳:仓库仅有的一处端口探测替换后收支相抵;文件等待场景已由 `vi.waitFor` 更优地覆盖;临时目录处理在各处已经使用内置的 `mkdtemp` + `rm {recursive}`acp-snapshot 的 `close()` 是排空顺序逻辑,不是进程树遍历。
## 验收标准
- 所列位置全部通过 execa或最终选定的等价包spawn 子进程;手写的收集/超时代码块,连同 `loader-smoke` 中两个标注 `/* v8 ignore */`、无法人为诱发的 OS 错误分支,全部移除。
- `llm-mock-server` 的 CLI 经由 `parseArgs` 解析;其 cli 测试文件在更新消息期望后通过。
- `apps/web/tests` 下不再存在手写的 `.env` 解析器。
- 受影响的 e2e 与快照测试套件在 POSIX 与 Windows 两条 CI 车道上均通过。
## 风险
- `loader-smoke` 是逐文件 100% 覆盖率门禁下的 `src/` 文件;这次替换实际上简化了它的覆盖率问题(移除了无法人为诱发的分支),但新的调用形态需要补齐覆盖。
- 每个改写后的 e2e 都必须在两个平台上重新运行;终止信号升级或 stdin 关闭语义上的细微差异loader-smoke 的 stdin 关闭契约对应 `input: ''`)是需要逐处核验的风险。
- execa 是新增的根 devDependency当前完全不存在于 lockfile 中);它是 npm 上被依赖最多的包之一且维护活跃,健康度不是顾虑;至于 exe/运行时闭包,无论选哪个包都不受影响(仅测试使用)。

View File

@@ -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
2026-07-26-dependency-swaps-rejected-by-nih-audit.md: c988ca0c75e9c50686551f3be1971d736b971e2a
2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: e85161cb2ee616d388aa2a9dd065c315c60cd44a
2026-07-26-dependency-swaps-rejected-by-nih-audit.md: 31c925cd7bfe21e2020ae8bd3ba8f9e2b0398641
2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: 097ba6c879a9eab7ae25f9a3020c403380842014

View File

@@ -46,7 +46,7 @@ Adopt the following dependency swaps. Rejected — per-item evidence below; a fu
- **`shell-quote` for POSIX single-quoting**: two 1-line quoting helpers with exhaustive tests versus a maintenance-mode package with a CVE history and different escaping output — a safety boundary is the wrong place to save one line.
- **`strip-ansi` for pty sanitization**: the pty sanitizer is a streaming state machine with split-sequence carry across chunks and OSC `133;D` prompt-marker extraction (the shell-readiness signal); stateless strippers replace ~20 inner lines while all state machinery stays. `stripVTControlCharacters` also demonstrably leaks unterminated-OSC payloads the session-title normalizer must strip (anti-spoofing).
- **`pidtree`/`ps-tree` for the pty process inspector**: bare PID trees; the code needs start-time identity against PID reuse plus `/proc` stdin-wait detection no package does.
- **`execa` for the subagent-subprocess dispose ladder**: `forceKillAfterDelay` covers SIGTERM→SIGKILL but not the stdin-EOF-first cooperative tier or the reject-if-no-exit-edge contract; adopting it here rewrites spawn sites while keeping the ladder. (Test-infrastructure spawn plumbing is different — see the [execa proposal](../../proposed/testing/2026-07-26-execa-for-test-subprocess-plumbing.md).)
- **`execa` for the subagent-subprocess dispose ladder**: `forceKillAfterDelay` covers SIGTERM→SIGKILL but not the stdin-EOF-first cooperative tier or the reject-if-no-exit-edge contract; adopting it here rewrites spawn sites while keeping the ladder. (Test-infrastructure spawn plumbing is different — see the [execa Agent Note](../../implemented/testing/2026-07-26-execa-for-test-subprocess-plumbing.md).)
- **`tree-kill` for acp-snapshot teardown and lsp process kill**: the lines are drain-ordering/error-propagation, not tree traversal; lsp/bash already use detached process groups + taskkill.
- **node-pty everywhere for the TUI test driver**: [Windows-TUI note](../../implemented/feature/2026-07-20-windows-tui-support.md) explicitly rejected node-pty-on-every-host; it is already the Windows leg.

View File

@@ -46,7 +46,7 @@ Status: rejected — 下列每一项替换在证据上都未达到净简化门
- **以 `shell-quote` 承担 POSIX 单引号包裹**:两个各 1 行、测试详尽的引号辅助函数,对上一个处于维护模式、有 CVE 历史、转义输出还不一样的包——安全边界不是省一行代码的地方。
- **以 `strip-ansi` 承担 pty 净化**pty 净化器是一台流式状态机,带跨分片的断裂序列续接和 OSC `133;D` 提示符标记提取shell 就绪信号);无状态的剥离器只能替掉约 20 行内层代码,全部状态机构件原样保留。`stripVTControlCharacters` 还被实证会泄漏未终止的 OSC 载荷,会话标题归一化器必须剥除它们(反欺骗)。
- **以 `pidtree`/`ps-tree` 承担 pty 进程巡检器**:它们只给裸 PID 树;这段代码需要对抗 PID 复用的启动时间身份校验,加上 `/proc` stdin 等待检测,没有包做这些。
- **以 `execa` 承担 subagent-subprocess 的 dispose资源释放阶梯**`forceKillAfterDelay` 覆盖 SIGTERM→SIGKILL但覆盖不了先发 stdin EOF 的协作层级,也覆盖不了「无退出沿即 reject」契约在这里采用它意味着重写各 spawn 调用点、同时阶梯照旧保留。(测试基础设施的 spawn 管线是另一回事——见 [execa 提案](../../proposed/testing/2026-07-26-execa-for-test-subprocess-plumbing.md)。)
- **以 `execa` 承担 subagent-subprocess 的 dispose资源释放阶梯**`forceKillAfterDelay` 覆盖 SIGTERM→SIGKILL但覆盖不了先发 stdin EOF 的协作层级,也覆盖不了「无退出沿即 reject」契约在这里采用它意味着重写各 spawn 调用点、同时阶梯照旧保留。(测试基础设施的 spawn 管线是另一回事——见 [execa Agent Note](../../implemented/testing/2026-07-26-execa-for-test-subprocess-plumbing.md)。)
- **以 `tree-kill` 承担 acp-snapshot 拆除与 lsp 进程终止**那些代码行做的是排空顺序与错误传播不是进程树遍历lsp/bash 已经使用分离的进程组加 taskkill。
- **在 TUI 测试驱动器上到处使用 node-pty**[Windows TUI 决策](../../implemented/feature/2026-07-20-windows-tui-support.md)已明确否决在每个宿主上都用 node-pty它已经是 Windows 那一条腿。

View File

@@ -176,7 +176,7 @@ describe('jsonrpc-agent keyless smoke', () => {
DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes',
},
stdin: 'ignore',
timeout: 9_000,
timeout: 25_000,
killSignal: 'SIGKILL',
reject: false,
})
@@ -184,5 +184,5 @@ describe('jsonrpc-agent keyless smoke', () => {
expect(exitCode, stderr).toBe(1)
expect(stdout).toBe('')
expect(stderr).toContain('plugin(s) failed to load: @deepseek-ai/dsh-jsonrpc')
}, 10_000)
}, 30_000)
})

View File

@@ -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
README.md: f3817a386a286e1dca40334fed7cb169643cb7e4
README.zh.md: 2f87e9ef7b29f65f81f8b464f59725c13a057003
README.md: 8babb67c30aed87ace4cfff81b2494a03a5b0335
README.zh.md: 3f43627740054e200e928ffe527f827c710599b0

View File

@@ -55,7 +55,7 @@ Every scenario compares `stdout.expected.jsonl` with cwd-rooted separators canon
The example also ships a `cordis.snapshot.yml` replay overlay next to its `cordis.yml` (the bin swaps them under `DSH_SNAPSHOT=replay` — [single-source replay config Agent Note](../../../.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md)); replay fixtures are served by [`dsh-llm-replay`](../llm-replay/README.md), which this package points at via the `DSH_SNAPSHOT_*` env vars it sets on the child. `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 overlay, and rewrites stdout, comparable session-log expected outputs, and each pin's 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` imports vitest, so the package entry is importable only inside a vitest run (the launcher, harness, 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 TUI snapshot suite and the web browser e2e lane. Input scripts cover initialization, fresh-session creation, text prompting, 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.
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 TUI snapshot suite and the web browser e2e lane. Input scripts cover initialization, fresh-session creation, text prompting, 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.
## Model Experience

View File

@@ -55,7 +55,7 @@ defineAcpSnapshotSuite({
示例还发布 `cordis.snapshot.yml` 回放 overlay位于 `cordis.yml` 旁边bin 在 `DSH_SNAPSHOT=replay` 下交换它们,见[单源回放配置 Agent Note](../../../.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md));回放 fixture 由 [`dsh-llm-replay`](../llm-replay/README.md) 提供,该包通过对子级设置的 `DSH_SNAPSHOT_*` env var 指向它。`pnpm run test:snapshot:record` 调用实时 LLM并重写已记录场景的模型 fixture`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay并从已提交模型脚本重写 stdout、可比较会话日志预期输出以及每个 pin 的提示词与工具 schema sidecar。Fixture 角色、录制/回放/刷新语义和场景表字段记录在 `Scenario` 以及[快照 Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) 中。
约束:`suite.ts` 导入 vitest,因此包入口只能在 vitest 运行中导入(启动器、harness 和规范化器没有此依赖,但从同一入口发布)。启动器和套件工厂按设计专用于 ACP启动器使用 SDK 的 `ClientSideConnection`;规范化器是与传输无关的会话日志/文本辅助工具,还由 TUI 快照套件和 web 浏览器 e2e lane 消费。输入脚本覆盖初始化、新建会话、文本提示、取消、预期 RPC 失败和持久轮次边界等待。权限往返是选项类别选择(`allow_once``reject_once`等)的 FIFO 队列,映射到 agent 发出的 `optionId`;缺少或耗尽的队列回答 `cancelled`,未提供类别会拒绝运行。
约束:`suite.ts` `harness.ts` 导入 vitestharness 通过 `vi.waitFor` 轮询其持久边界等待),因此包入口只能在 vitest 运行中导入(启动器和规范化器没有此依赖,但从同一入口发布)。启动器和套件工厂按设计专用于 ACP启动器使用 SDK 的 `ClientSideConnection`;规范化器是与传输无关的会话日志/文本辅助工具,还由 TUI 快照套件和 web 浏览器 e2e lane 消费。输入脚本覆盖初始化、新建会话、文本提示、取消、预期 RPC 失败和持久轮次边界等待。权限往返是选项类别选择(`allow_once``reject_once`等)的 FIFO 队列,映射到 agent 发出的 `optionId`;缺少或耗尽的队列回答 `cancelled`,未提供类别会拒绝运行。
## 模型体验

View File

@@ -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
README.md: 8e53550608037a3c9a272db825933b7224ab24db
README.zh.md: 5310429ab59cf3cd04ac024746f5ed557e003637
README.md: 73610ce50ebac4c6fc7bb9135f7b41b347c60685
README.zh.md: 17f8481220136e8edf9fccd23fabfca5ccf41dfc

View File

@@ -19,5 +19,5 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Built mode requires a prior build** — the config must also resolve every named package upward through `examples/node_modules`.
- **Captured stdout and stderr are unbounded** — a runaway child can consume memory until the deadline kills it.
- **Captured stdout and stderr are bounded only by execa's default 100 MB `maxBuffer`** — a runaway child is terminated at that ceiling rather than at a smoke-chosen budget.
- **Timeout kills only the direct child** — a process tree spawned by a faulty fixture can outlive the smoke and needs external cleanup.

View File

@@ -19,5 +19,5 @@
## 已知限制与待完成工作
- **构建 mode 需要事先构建**:配置还必须能够通过 `examples/node_modules` 向上解析每个命名包。
- **捕获的 stdout 和 stderr 无界**:失控子进程可以消耗内存,直到 deadline 将其终止
- **捕获的 stdout 和 stderr 仅受 execa 默认 100 MB `maxBuffer` 约束**:失控子进程会在该上限处被终止,而不是在冒烟测试自选的预算处
- **超时只终止直接子进程**:故障 fixture 生成的进程树可以比冒烟测试存活更久,需要外部清理。