DeepSeek Harness 测试子进程管件重构:用 execa 取代手写 spawn-collect-timeout 编排 DeepSeek Harness 测试子进程管件重构用 execa 取代手写 spawn-collect-timeout 编排【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读本文基于 DeepSeek Harness 仓库中的归档技术笔记.agents/notes/archived/testing/2026-07-26-execa-for-test-subprocess-plumbing.md完整还原一次发生在测试基础设施层的架构决策仓库中约十处 e2e/smoke 测试文件曾各自手写「spawn → 收集输出 → 超时 kill」三段式编排本次重构统一改用经过实战检验的execa同时用 Node 内置的parseArgs、process.loadEnvFile与 Vitest 的vi.waitFor清除三组重复管件。读完本文你将理解这类测试子进程脚手架为何值得交给成熟依赖、哪些「真正自定义」的逻辑必须保留在仓库内以及替换后错误报告、覆盖率门槛和跨平台行为发生了哪些可验证的变化。背景e2e/smoke 测试里被反复重造的轮子在 DeepSeek Harness 这类「Everything is a Plugin」的多包仓库中大量测试需要真正拉起子进程启动一个 app bin、跑一轮真实 Loader 会话、验证 CLI 退出码、观察 mock 服务的输出。笔记指出约有十处 e2e/smoke 文件各自重新推导了同一套「spawn-collect-timeout」编排let stdout 累加 setEncodingdata事件处理器setTimeout→kill(SIGKILL)的截止期限逻辑once(exit)/once(error)的结果结算。涉及的站点包括runLoaderSmoke的内部 spawn 块、多个包中的runBuiltBin/runBinExpectingExit助手、lsp-local与code-runtime-worker的 built-lib e2e 助手、pty-harness.ts的外层收集器以及smoke-real.e2e.ts、crash-recovery.e2e.ts的部分逻辑。每一处实现都有微小差异——这正是典型的「重复带来的维护税」deadline 语义、kill 信号、错误分支的覆盖率标注各自为政。与这条主线相关的还有两类「连环手写」llm-mock-server的 CLI 手工分词了 17 个取值型--flag value选项外加布尔标志约 45~60 行循环与取值辅助函数而node:util的parseArgs早已是仓库惯用做法cli-demo、acp-demo、verify-runtime-closure.ts、packages/sdk/scripts都在用smoke-real.e2e.ts与scaffold.ts携带了两份逐字拷贝的正则.env解析器约 20 行而process.loadEnvFile恰好具备所需的 no-override 语义且 vitest 的 e2e/snapshot/web 配置在这些文件运行前就已经用它加载了仓库根.env使这两份拷贝成为死代码。决策把子进程所有权交给 execa笔记给出的核心决策非常明确execa是仓库根 devDependency也是deepseek-ai/dsh-loader-smoke唯一一个src/消费者的运行时依赖。这可以从仓库当前状态直接验证package.json 的devDependencies中声明了execa: ^10.0.0loader-smoke/package.json 的dependencies同样声明execa: ^10.0.0。统一后的调用形态是await execa(cmd, args, { cwd, env, timeout, killSignal: SIGKILL, reject: false, })其返回结果的字段{ stdout, stderr, exitCode, signal, timedOut, failed }相互独立恰好匹配仓库自身 defensive-patterns 的规则正交的子进程结果应独立上报而不是混在一个笼统的异常里。源码实证runLoaderSmoke 中的 execa 落地仓库中现存的 packages/test-support/loader-smoke/src/index.ts 是这份决策最完整的落地样本。runLoaderSmoke在隔离的临时目录中启动一个真实 Loader 树关闭 stdin 并等待干净退出它在任何结果路径上都拥有进程 kill 与临时目录清理权。其 execa 调用const result await execa(launch.command, launch.args, { cwd, env: launch.env, input: , timeout: processTimeoutMs, killSignal: SIGKILL, reject: false, stripFinalNewline: false, })四个选项各有讲究input: 写入空内容并立即关闭 stdin这是 fixture 可见的「stdin 关闭契约」。源码注释明确说明这一点timeout: processTimeoutMskillSignal: SIGKILL用 execa 的截止期限取代手写的setTimeout→kill(SIGKILL)且该包导出DEFAULT_PROCESS_TIMEOUT_MS 30_000并把LOADER_SMOKE_TEST_TIMEOUT_MS设为 45 秒为子进程自带的 30 秒诊断超时留出余地reject: false把 spawn 错误、SIGKILL 截止和退出码全部折叠进独立结果字段使下面这段诊断在任何失败形态下都能同时嵌入 stdout 与 stderrif (result.timedOut) { throw new Error(${options.label} did not exit within ${processTimeoutMs / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}) } const expectedExitCode options.expectedExitCode ?? 0 if (result.exitCode ! expectedExitCode) { throw new Error(${options.label} exited ${String(result.exitCode)} (expected ${expectedExitCode}). stdout:\n${result.stdout}\nstderr:\n${result.stderr}) }stripFinalNewline: false凡是断言精确流字节的站点都需要保留原始尾部换行。可以看到原先「accumulate → deadline → settle」的手写骨架被彻底删除spawn 失败、超时、非零退出分别落到timedOut/exitCode字段上由统一的诊断代码处理。这直接带来一个可验证的后果——原loader-smoke中两处标记/* v8 ignore */的不可诱导 OS 错误分支消失了spawn 与流失败现在经由 execa 的结果字段结算src/文件不再携带覆盖率豁免单文件门槛覆盖了剩余的每个分支。解析器替换parseArgs 与 loadEnvFilemock 服务 CLI 改用 parseArgspackages/test-support/llm-mock-server/src/cli.ts 如今从node:util导入parseArgs并以此维护整套词汇表--sequence、--host、--port、--api-key、--listen-delay-ms、--repeat-last、--seed、--random-weights以及全部 response 类选项。调用形态为const { values } parseArgs({ args: [...argv], options: CLI_OPTIONS, strict: true, allowPositionals: false })决策边界被刻意划清tokenizer 层未知选项、缺失值、多余位置参数交给parseArgs严格模式、不允许位置参数语义层仍保持手工数字强转、取值范围、跨选项约束如connection_refused要求端口非零继续由代码自行校验。随之而来的后果是 tokenizer 层的错误文案不再由本仓库决定未知选项、缺失值、多余位置参数将直接报告parseArgs自己的措辞并被固化在tests/cli.spec.ts的错误消息测试中。这意味着升级 Node 大版本时若parseArgs文案变化该测试会显式捕获这一变动。死代码 .env 解析器被整体删除两份逐字拷贝的正则.env解析器被直接删除所属的 vitest 配置vitest.web.config.ts无条件、vitest.snapshot.config.ts在 record 模式下在这些测试文件运行前已经用process.loadEnvFile加载了仓库根.env。loadEnvFile内建的 no-override 语义与原有拷贝完全一致因此这些拷贝属于纯粹冗余。轮询循环交给 vi.waitFor快照 harness 曾手写三个「poll 直到 deadline」循环waitForPersistedTurnStart/waitForPersistedTurnEnd/waitForWorkspaceFile位于 packages/test-support/session-snapshot/src/harness.ts约 55 行外加crash-recovery.e2e.ts中的waitForFile。由于 Vitest 已是dsh-acp-snapshot的运行时依赖这些循环统一改用vi.waitFor并显式给出{ interval, timeout }与从回调中抛出的描述性错误。有一个值得注意的细节waitForPersistedTurnStart把「畸形记录校验错误」从重试循环中单独捕获出来使其立即失败整个运行而不是一直被重试到 deadline——这说明替换不只是换 API还顺势修正了错误分类。哪些「真正自定义」的部分保留原样决策强调execa 只接管子进程本身真正属于仓库逻辑的部分仍然留在仓库里只是从「在裸 spawn 之上手写」变成「在 execa 拥有的子进程之上手写」cli-demo的「marker 出现即中断」mid-stream 逻辑jsonrpc的按行谓词驱动协议crash-recovery的「failpoint 处 SIGKILL」编排。而smoke-real.e2e.ts的三个长生命周期交互式服务器完全保留裸spawn——其全部需求就是跨两个流的 ready-line 监听外加分阶段的 SIGTERM→await→SIGKILL 拆除execa 在这里删不掉任何东西该文件在本变更中分享的只有那个已死掉的.env解析器。备选方案对比为什么不是 tinyexec笔记记录了三条被评估的替代路径tinyexec已通过 vitest 传递存在于 node_modulesAPI 更小——但没有 kill 升级、没有富错误输出嵌入而且「传递依赖」本身不是契约。若日后倾向更轻量的包替换形态完全一致仓库内共享 spawn 辅助函数不引入新依赖——供应链上更省但 deadline/kill/结算逻辑的维护成本留在仓库内且违背依赖策略中「与其手搓不如交给久经考验的包」的立场跨平台超时、终止与结果归一化行为都要重新挣一遍get-port、wait-on、tempy、tree-kill——被逐一否决仓库唯一的端口探测是盈亏平衡的、文件等待已被vi.waitFor主导、临时目录处理本就用mkdtemprm {recursive}内建、acp-snapshot 的close()是排空顺序逻辑而非进程树遍历。后果清单从覆盖率到输出上限重构的完整影响可以从笔记的 Consequences 逐条核对覆盖率豁免清零手写 collect/timeout 块消失包括loader-smoke中两个/* v8 ignore */的 OS 错误分支src/文件不再需要豁免单文件门槛覆盖每个剩余分支捕获输出有了上限由 execa 默认的 100 MBmaxBuffer约束溢出会终止子进程而此前是无界的loader-smokeREADME 的 limitation 条目已反映这一点跨平台行为统一直接子进程的超时终止与 exit/signal 结果归一化由 execa 在各平台统一负责取代各站点的自研实现进程树终止不在这些辅助函数范围内loader-smokeREADME 已声明。各重写套件在本变更中均于 POSIX 重跑Windows CI 通道负责另一平台依赖变更execa 成为新的根 devDependency此前不在 lockfile 中它是 npm 上被依赖最多的包之一且维护活跃而 exe/runtime 闭包不受影响仅测试使用。总结可复用的测试子进程决策模板这次重构的启示可以抽象为三条可迁移的判断标准把「通用编排」交给「拥有该能力的依赖」spawn-collect-timeout 属于通用子进程语义execa 这种久经考验的包天然拥有跨平台 timeout、kill 升级与结果归一化比每处各写一遍更可靠、更省维护「真正自定义」与「通用编排」分层marker 中断、行谓词协议、failpoint 编排属于业务逻辑留在仓库stdin 关闭、超时、SIGKILL、结果字段化属于通用能力交给依赖顺手清掉同类手写物parseArgs、process.loadEnvFile、vi.waitFor是 Node/Vitest 内建能力凡是语义完全覆盖手写拷贝的都应删除死代码——同时把内建的错误文案固化进测试让升级行为变化显式可见。对于在 DeepSeek Harness 中新增 e2e/smoke 测试的开发者正确的起点是直接复用 loader-smoke 的runLoaderSmoke/resolveExampleLaunch支持src/lib双模式、隔离 cwd、stdin 关闭契约与统一诊断而不是在裸spawn之上再造一套 collect-timeout 脚手架。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考