
DeepSeek Harness 测试基建演进用 execa 统一替换手写的子进程管道代码【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读DeepSeek Harness 的 e2e 与冒烟测试需要频繁地「spawn 子进程、收集 stdout/stderr、超时后 SIGKILL 终止、按退出码结算」过去约十个测试文件各自手工重复实现这套编排细微差异造成维护与诊断负担。本文以仓库中的归档 Agent Note2026-07-26-execa-for-test-subprocess-plumbing.zh.md为骨架结合 loader-smoke、built-bin.e2e.ts、llm-mock-server CLI 等源码完整还原这次「以 execa 为核心、连带 parseArgs / vi.waitFor 等配套重构」的工程决策与落地形态读者可据此理解该仓库测试子进程的标准写法、参数语义与取舍边界。背景同一套 spawn 编排被复制了约十次手写模式的四段式样板在引入 execa 之前仓库内大约十个 e2e/冒烟测试文件各自用 Node 原生child_process手工重写同一套「spawn、收集输出、超时终止」编排各处只有细微差别收集输出setEncodingdata事件处理器做let stdout 式的逐块拼接超时截止setTimeout到期后调用kill(SIGKILL)强杀结算结果用once(exit)/once(error)收尾判定成败手工比对退出码与收集到的流内容。原 Agent Note 明确列出的位置包括runLoaderSmoke的内层 spawn 代码块packages/test-support/loader-smoke/src/index.tsapps/cli/tests/built-bin.e2e.ts 与 cli-demo 测试中的runBuiltBinacp-demo 测试中的runBinExpectingExitlsp-stdio与code-runtime-worker-thread中基于构建产物的 e2e 辅助函数可参见 packages/lsp/lsp-stdio/tests/built-lib.e2e.ts、packages/code-runtime/code-runtime-worker-thread/tests/built-lib.e2e.ts 等当前仍在使用的 execa 消费方tui-agent 测试的 pty-harness 外层收集器、jsonrpc-agent 的 keyless 冒烟测试部分涉及的 apps/web/tests/smoke-real.e2e.ts 与 crash-recovery e2epackages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts。三处相邻的手写基础设施除了 spawn 样板还有三处相邻的测试基础设施手写代码进一步强化了替换理由mock-server 的 CLI 切分llm-mock-server曾手工逐个切分 17 个带值的--flag value选项加若干布尔标志约 45–60 行的循环与取值辅助函数而node:util内置的parseArgs早已是仓库惯用写法cli-demo、acp-demo、verify-runtime-closure.ts、sdk 脚本等。两份逐字相同的.env解析器死代码apps/web/tests/smoke-real.e2e.ts 与 apps/web/tests/scaffold.ts 各带一份约 20 行的正则.env解析器拷贝而内置的process.loadEnvFile恰好具备所需的「不覆盖已有值」语义并且 vitest 的 e2e/snapshot/web 配置在这些文件运行之前就已加载仓库根.env这两份拷贝实为死代码。三个手写轮询循环快照 harnesspackages/test-support/acp-snapshot/src/harness.ts 的waitForPersistedTurnStart/waitForPersistedTurnEnd/waitForWorkspaceFile约 55 行加 crash-recovery 中的waitForFile而vi.waitFor/expect.poll正好覆盖这种「轮询直到截止时间」的形态vitest 本来就是dsh-acp-snapshot的运行时依赖因此改用它不新增任何依赖。决策统一经由await execa(...)运行并独立上报结果字段依赖归属execa同时作为根 devDependency见仓库根 package.jsonexeca: ^10.0.0和deepseek-ai/dsh-loader-smoke的运行时依赖见 packages/test-support/loader-smoke/package.json其是唯一在src/中直接消费 execa 的包。改造前 execa 完全不存在于 lockfile 中属本次新增。统一调用形态所有上述 spawn、收集、超时位置统一改为const result await execa(cmd, args, { cwd, env, timeout, killSignal: SIGKILL, reject: false, })关键在于reject: false与结果字段的正交性{ stdout, stderr, exitCode, signal, timedOut, failed }相互独立上报不需要再在exit/error事件与setTimeout之间手工编排竞态。这与本仓库防御模式中「正交的子进程结果各自独立上报」的规则一致——spawn 失败、超时被 kill、非零退出分别落在不同字段上调用方按需组合诊断。两个补充约定stdin 关闭契约runLoaderSmoke传input: 只写空内容并立即关闭 stdin兑现 fixture 可见的「stdin 已关闭」契约精确字节断言需要固定精确流字节的位置传stripFinalNewline: false防止 execa 默认剥掉末尾换行导致断言失配。真正定制的部分继续保留定制execa 只接管「spawn、收集、超时、规范化」而协议级/信号级的定制编排仍然自持只是架在 execa 拥有的子进程之上cli-demo在流中遇到标记即中断的逻辑jsonrpc-agent基于行谓词的协议驱动可参考 apps/cli/tests/built-bin.e2e.ts 中 SDK profile 测试对child.stdout建立readline异步迭代、逐行解析 JSON-RPC 的做法crash-recovery在故障点向子进程发送 SIGKILL 的编排见 packages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts其中child.kill(SIGKILL)后断言{ code: null, signal: SIGKILL }的结算仍然是测试自身的语义。特别地smoke-real.e2e.ts的三个长驻交互式服务器保留原生spawn——跨双流监听就绪行、加上分级的 SIGTERM→等待→SIGKILL 拆除就是该处全部内容execa 在那里删不掉任何东西。原 Agent Note 涉及该文件的部分仅是那份已成为死代码的.env解析器。连带重构一llm-mock-serverCLI 改用node:utilparseArgsllm-mock-server的 CLI 解析器现在以parseArgs为分词底座strict、不允许位置参数见 packages/test-support/llm-mock-server/src/cli.tsimport { parseArgs } from node:util const { values } parseArgs({ args: [...argv], options: CLI_OPTIONS, strict: true, allowPositionals: false })CLI_OPTIONS声明了--sequence、--host、--port、--api-key、--listen-delay-ms、--repeat-last布尔、--seed、--random-weights、--success-text、--tool-name等完整词表。分词交给 parseArgs但语义校验仍手工实现数值转换numberValue必须有限数边界检查boundedIntegerValue必须是整数且在[min, max]内如--listen-delay-ms上限为MAX_MOCK_LLM_TIMER_DELAY_MS跨选项约束connection_refused只允许作为序列首项、必须显式非零--port--seed/--random-weights要求序列包含random。一个直接的后果未知选项、缺失取值与多余位置参数的报错文本不再由仓库决定而是报告parseArgs自己的措辞并在 packages/test-support/llm-mock-server/tests/cli.spec.ts 中如此固定该文件注释明确写着 Tokenizer-level failures carry node:util parseArgss own messages。连带重构二删除两份.env解析死代码两份loadRootEnv拷贝被整体删除。理由来自 vitest 配置的加载顺序vitest.web.config.ts无条件、vitest.snapshot.config.ts在 record 模式下都会在这些测试文件运行之前就加载仓库根的.env测试运行时的process.env已经具备「不覆盖已有值」语义这正是process.loadEnvFile的行为两份手写正则拷贝实属死代码。连带重构三四个轮询循环改用vi.waitFor四个「轮询直到截止时间」的手写循环waitForPersistedTurnStart/waitForPersistedTurnEnd/waitForWorkspaceFile加 crash-recovery 的waitForFile统一改用vi.waitFor显式传入{ interval, timeout }并在回调中抛出带描述信息的错误。一个值得注意的细节waitForPersistedTurnStart把「持久化记录格式非法」的校验错误捕获到重试循环之外使其立即让运行失败而不是被vi.waitFor重试到截止时间——因为格式非法属于确定性故障重试不会自愈早失败比晚失败更具诊断价值。crash-recovery 中同类场景可见 packages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts 对vi.waitFor的使用其注释亦说明vi.waitFor会重试回调抛出的每个错误因此终态应解析出来而非抛出。曾考虑的替代方案方案评估结论tinyexec代替 execa它已作为 vitest 的传递依赖存在于node_modulesAPI 也更小但没有终止信号逐级升级、不会把丰富的输出嵌入错误对象且传递依赖并不构成契约。若最终倾向更轻的包替换形态完全相同仓库内共享 spawn 辅助函数不引入新依赖可行、供应链成本更低但截止时限、终止与结算逻辑的维护会留在仓库内与依赖策略中「优先使用久经实战的依赖而非手写」的取向相悖还得重新踩坑换来 execa 已自带的跨平台超时、终止与结果规范化行为get-port/wait-on/tempy/tree-kill逐一不予采纳仓库仅有的一处端口探测替换后收支相抵文件等待场景已由vi.waitFor更优地覆盖临时目录处理各处已用内置mkdtemprm {recursive}acp-snapshot 的close()是排空顺序逻辑不是进程树遍历落地形态runLoaderSmoke的源码级拆解deepseek-ai/dsh-loader-smoke是这次改造的核心载体其runLoaderSmokepackages/test-support/loader-smoke/src/index.ts完整展示了统一调用形态const result await execa(launch.command, launch.args, { cwd, env: launch.env, input: , // 写空内容并关闭 stdin兑现 stdin-close 契约 timeout: processTimeoutMs, // 默认 30_000见 DEFAULT_PROCESS_TIMEOUT_MS killSignal: SIGKILL, // 超时后的终止信号 reject: false, // spawn 错误/超时/非零退出折叠为独立结果字段 stripFinalNewline: false, // 保留精确流字节 }) if (result.timedOut) { throw new Error(${options.label} did not exit within ${processTimeoutMs / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}) } if (result.exitCode ! expectedExitCode) { throw new Error(${options.label} exited ${String(result.exitCode)} (expected ${expectedExitCode}). stdout:\n${result.stdout}\nstderr:\n${result.stderr}) }诊断信息把两条流都嵌入错误消息失败时无需重新运行即可定位。配套的LoaderSmokeOptions支持label、tempDirPrefix、binScript、configPath、expectedExitCode、prepare/inspect钩子等expectedExitCode用于固定「设计好的失败面」——以任何其他方式退出包括成功退出都判定冒烟失败。整个函数在finally中无条件rm(cwd, { recursive: true, force: true })清理隔离目录。它启动的是真实的应用可执行文件 真实 Cordis Loader 组合插件加载、服务接线、agent loop 全走真实路径而非手工搭建的测试上下文。该 harness 还提供模式感知的启动解析resolveExampleLaunchsrc模式经 tsx 以 tsconfigpaths映射解析工作区导入零构建开发路径lib模式在普通 Node 下运行构建后的lib/产物、经真实包exports解析与已安装消费方一致模式由DSH_EXAMPLE_MODE环境变量选择CI 设lib开发保持未设非法值显式报错。完整用法与限制见 packages/test-support/loader-smoke/README.zh.md。后果与影响覆盖率豁免清零手写收集/超时代码块全部移除包括loader-smoke中两个标注/* v8 ignore */、无法人为诱发的 OS 错误分支——spawn 与流故障如今经由 execa 的结果字段结算这个src/文件不再携带任何覆盖率豁免逐文件门禁覆盖其余全部分支。输出有了硬上限捕获的输出受 execa 默认 100 MBmaxBuffer约束溢出即终止子进程此前无界loader-smokeREADME 的「已知限制」条目如实反映了这一点。跨平台行为归一直接子进程的超时终止以及退出/信号结果规范化均由 execa 跨平台负责不再逐处手写但这些辅助函数依然不负责终止进程树失控 fixture 自产的子进程可能比冒烟存活更久需外部清理。每个改写后的套件在本次变更中已在 POSIX 上重新运行另一平台由 Windows CI 通道负责。依赖面变化execa 是新增的根 devDependency此前完全不存在于 lockfile按原 Agent Note 所述它是 npm 上被依赖最多的包之一且维护活跃由于仅测试使用exe/运行时闭包不受影响。错误文本归属变化mock-server CLI 切分器层面的错误文本不再由仓库决定未知选项、缺失取值与多余位置参数均报告parseArgs的措辞并在tests/cli.spec.ts中固定。可复用的实践模板结合 apps/cli/tests/built-bin.e2e.ts 中的runBuiltBin可以把这套模式提炼为仓库内测试的通用写法仅作阅读与参照不涉及修改仓库const result await execa(process.execPath, [bin, ...args], { input: , timeout: SPAWN_TIMEOUT_MS, killSignal: SIGKILL, reject: false, env: childEnv, extendEnv: false, // 只使用显式合并后的 env }) if (result.timedOut) { throw new Error(bin did not exit within ${SPAWN_TIMEOUT_MS / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}) } return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }需要驱动长驻进程如 SDK/ACP profile时则保留对child.stdout/child.stderr流的直接消费如readline逐行异步迭代 JSON-RPC、ndJsonStream包装 ACP 协议流同时依赖 execa 的timeoutkillSignal兜底并在finally中child.kill(SIGKILL)与await child双保险收尾。小结这次改造的本质是把「spawn、收集、超时、结算」这层机械性的进程管道工作整体委托给久经实战的 execa而把「协议驱动、信号编排、流中断、就绪行监听」这类真正的测试语义继续留在测试代码里。配合parseArgs接手 CLI 分词、vi.waitFor接手轮询、删除.env解析死代码仓库的测试子进程基础设施从「十处各异的复制粘贴」收敛为「一个统一形态 少数自持的定制点」同时获得了跨平台规范化、100 MB 输出上限与更干净的覆盖率账目。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考