
字节笔记本
2026年10月6日 · 约 9 分钟读完
单测全绿不算数:DeepSeek Harness 的测试策略
DeepSeek Harness(dsh)是 DeepSeek AI 开源的 agent 框架,主张一切皆插件,底层由 Cordis 组合式框架驱动。这个项目的工程文档里有一章专门讲测试策略,核心判断非常直接:单元测试全绿只能证明底层通路存在,不能证明 agent 真的能用。整套体系就围绕这个判断展开。

五个测试层级,各管一段
单元测试用 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:缺 pwsh 的主机自动跳过执行器套件,vitest.config.ts 豁免该文件保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行。
真实 API e2e 用 pnpm run test:e2e 跑:带密钥调用真实提供方 API,包括 DeepSeek 模型和各提供方特有的冒烟测试。每条测试由自己的密钥控制(EXA_API_KEY、PERPLEXITY_API_KEY 等),缺密钥自动跳过,无密钥 CI 保持绿色。
快照用 pnpm run test:snapshot 跑:无密钥的预期输出固定对外行为(传输约定与呈现),持久化日志固定组装后的后端行为。ACP 场景会启动真实的自动化服务器示例、回放录制会话,对归一化 JSON-RPC 与重新持久化的日志做 diff。模型 transcript 变化时用 test:snapshot:record,回放输入仍有效时用 test:snapshot:refresh,每一处 JSONL 与预期输出的差异都要人工审查。一个 text-turn 场景固定完整的系统提示词与工具 schema,其他 fixture 把它 token 化,因此改动只会扰动一行。
Web 浏览器快照用 pnpm run test:web 跑,是 Linux PR 的必需门禁:Chromium 把回放后的浏览器输出与 apps/web/tests/snapshots/ 比较。CI 强制只读的 DSH_SNAPSHOT=replay,绝不写入预期输出;record 与 refresh 留在本地。test:web 会先构建,以交付插件 CSS。
另外,签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁靠 session header 发现每一份 fixture,仓库自带的迁移脚本负责改写旧版布局。
带密钥策略:真实 API 在这里不便宜
DeepSeek 团队在这份文档里写得毫不含糊:不要吝惜真实 API 测试。无密钥测试只能证明底层通路,只有带密钥运行才能证明 agent 对接真实模型能正常工作,覆盖文件写入提示词、多轮对话、工具使用和流中取消。价值最高的是冒烟测试:启动真实示例、发送一条提示词、检查外部世界。官方的事故复盘里记录过这样的案例:单元测试全绿,产品却是坏的,mock 抓不住这类问题,冒烟能抓住。
自动跳过是为了让无密钥 CI 和无密钥贡献者不被阻塞,它不是成本信号。每个示例都同时提供无密钥和带密钥冒烟测试。
mock 只留给高开销或不确定的边界
只 mock LLM 适配器、网络、时钟这类边界,下游一切保持真实。手写替身只能证明桥接层在搬运字节,不能证明交付的工具行为符合断言。桥接工具调用测试用脚本化 mock 模型搭配真实工具与执行器:makeBridgeHarness({ withBash: true }) 接入 dsh-bash-local 与 dsh-tool-bash,然后运行一条 echo。恢复测试按步骤区分分片前与分片后的失败,证明失败分片不会派生消息或工具副作用,覆盖耗尽、取消、策略组合、持久化、状态、协议计数、会关闭传输的空闲超时,以及交付的 Loader 组合。

验证外部世界,而不是自我报告
e2e 断言应该重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测,等于给作弊的 agent 开门。断言未修改的文件逐字节一致。资源由测试自理:在测试中创建 harness,在 afterEach 中 dispose,失败、重试、超时也要释放。共享 fixture 放在普通的 tests/harness.ts 里,绝不放进另一个 *.e2e.ts:导入一个 spec 会重新注册它的 describe,导致真实 API 调用重复执行。
测试真实入口路径
产品可见的插件必须有一个非单元的真实组合测试。手动构建的 ctx.plugin(...) 套件不算数:要通过 Loader 和 app/process 启动仅用于测试的 cordis.yml,只 mock 外部服务或非确定性输入,断言模型可见的请求与日志、持久状态或用户可见输出。opt-in 选项不要混进交付默认值。
守卫只有在回归真的能让它失败时才有效。对于没有 inject 的 bundle 插件,Loader 冒烟测试在默认导出替换必需具名导出时仍然绿,需要显式加 expect('default' in mod).toBe(false) 与 unwrapExports 往返断言,并且要证明它有效:引入回归、观察变红、回退。
「真实入口路径」指已发布的产物:包的 bin 运行的是构建后的 lib/bin.js,由普通 node 执行,从而暴露 tsx 会掩盖的失败,比如结算竞态、模块解析、被吞掉的加载失败。非 index 运行时入口(worker-thread 的同级文件 lib/worker.cjs)和多个 bundle 共享的单例模块同理。保持构建产物冒烟测试绿色,并断言真正缺失的配置以非零状态退出。
解析、子进程与快照纪律
解析仅限源码:每个 vitest 配置用 vite-tsconfig-paths 指向 tsconfig.base.json,工作区包的裸导入解析到 src,绝不经由包的 exports 解析到构建后的 lib/,因为陈旧产物会加载第二份模块单例。构建产物只在显式指定时使用。
子进程启动模式:CI 与已有构建产物的测试通道通过共享双模式启动器,从构建后的 lib/ 运行每个示例或 Cordis 配置子进程,不要手写 --import tsx;不加载 Cordis 的协议与操作系统 fixture 直接用 Node 跑可擦除语法的 .ts 文件;只有测试对象本身是源码路径解析时才可以用 src,并在测试中写明这一约定。
何时需要快照测试:每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 内通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock、仅测试组合与 PR 理由都不能取代组装后的 transcript,必要时应扩展 harness。ACP 自动化场景使用 examples/<name>/tests/snapshots/ 下基于套件工厂的场景表;headless-agent 有内部规范事件 JSONL 快照;pwsh-tool-turn 场景启动真实 pwsh,无 pwsh 主机自动跳过;交互式终端旅程使用 apps/cli/tests/snapshots/ 下 JSONL 驱动的场景;瞬态呈现使用包内语义矩阵,输入、Loader 选择或终端清理变化时还要加 PTY 用例。新的能力 seam、生命周期变体或 transcript 呈现接口,要在计划阶段列出每个覆盖层级,并在实现前验证 harness 能表达它们。
这套策略可以浓缩成三句话:mock 越少越好;断言离外部世界越近越好;测试要跑在用户真正使用的入口上。绿色套件只有在这些前提下才有意义。



