测试与扩展开发
本教程共 32 篇 · 第 30 篇 · 更新于 2026-08-15 · 约 7 分钟阅读
本节目标:搞懂 dsh 仓库「怎么保证质量」——四层测试怎么分工、invariants 与快照/重放是什么思路,以及想动手扩展时从哪些文档和命令入手。
想深入一个开源项目,先读它的测试策略和开发指南。dsh 官方文档里 testing.md 和 development.md 就是这两份地图。这一章把它们翻译成人话。
测试四层:每层抓不同的盲区
仓库测试分四层,命令和职责如下:
| 层级 | 命令 | 抓什么 |
|---|---|---|
| 单元测试 | pnpm run test | vitest 跑包内 tests/**,优先边界、错误路径、事件顺序、并发竞态 |
| 覆盖率门禁 | pnpm run test:coverage | 对 packages/*/*/src 按文件 100% 行覆盖 |
| 真实 API e2e | pnpm run test:e2e | 带密钥调用真实提供方 API(DeepSeek 等),缺密钥自动跳过 |
| 快照 | pnpm run test:snapshot / test:web | 无密钥预期输出固定对外行为;浏览器快照用 Chromium 回放比较 |
关键认知:覆盖率是必要条件,永远不是充分条件。它证明行被执行过,不证明功能按交付预期工作。0001 事故里 100% 行覆盖率照样放过两个集成 bug,就是这个道理的注脚。
三个核心思路:invariants、快照、重放
运行时不变量(invariants)。 ctx.invariants 是一个由包自己拥有的检查注册表:每个包通过独立的 ./invariant 文件向 InvariantService 登记检查,失败时带明确的包归属。它检查的是「组合事实」——比如「模型可见的输入必须能从日志重建」这类架构约束,由运行时断言兜底,而不是只写在文档里。
快照(snapshot)。 快照用无密钥的预期输出固定对外行为:ACP 场景回放录制会话,对归一化 JSON-RPC 与重新持久化的日志做 diff;headless 场景通过 JSONL 测试驱动;浏览器快照把回放后的渲染与 apps/web/tests/snapshots/ 比较。模型 transcript 变化时用 pnpm run test:snapshot:record 重录,回放输入仍有效时用 test:snapshot:refresh。
重放(replay)。 会话日志是事件流(session/event → JSONL),重放就是 sessions.create(id, { seed })。恢复测试按步骤区分分片前/分片后的失败,证明失败分片不会派生出消息或工具副作用。崩溃恢复、fork、遥测都派生自这条事件流——所以官方有一条硬规则:模型可见即已记录,抵达模型请求的一切都必须能从日志重建。
Warning快照刷新是 fixture 的生产过程,不是正确性审查。每处 diff 都要人工评审;0002 事故就是快照把
UNKNOWN_TOOL结果当成新预期接受下来的。
三条测试铁律
1. 测试真实入口路径。 手动构建的 ctx.plugin(...) 套件不够。产品可见的插件必须通过 Loader 和真实 cordis.yml 启动,只 mock 外部服务或非确定性输入。发布的产物跑的是构建后的 lib/bin.js 加普通 node,从而暴露 tsx 会掩盖的失败。
2. 验证外部世界,而非自我报告。 e2e 断言应重新运行命令或从外部重新读文件;对 agent 自身输出做关键词探测,会让作弊的 agent 通过。断言未修改的文件逐字节一致。
3. mock 只用于高开销或不确定的边界。 只 mock LLM 适配器、网络、时钟;下游一切保持真实。手写替身只能证明桥接层在搬运字节,不能证明交付的工具行为符合断言。
开发环境搭建
想自己编译、测试、改源码,按官方 development.md 来:
前置条件:
- Node.js 22.19+ 或 24+(CI 覆盖 22.19 / 24 / 26)
- pnpm(仓库固定 pnpm@11.7.0;pnpm --version 解析不了就先 corepack enable)
- Git 2.26+(钩子会启用 worktree 专属配置扩展)
- 可选:DeepSeek API key(真实 API e2e 与演示用)
首次搭建:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run typecheck # 成功退出即表示搭建完成
Tip依赖从缓存恢复或 postinstall 被跳过导致钩子缺失时,手动补一次:
node scripts/install-lefthook.mjs。移动检出目录后也要重跑。
日常命令速查:
pnpm run build # 依赖构建产物的检查前先构建
pnpm run check:all # 全面本地门禁集(独立于 Git 钩子)
pnpm run test # 单元测试
pnpm run test:coverage
pnpm run test:e2e # 需要 DEEPSEEK_API_KEY
pnpm run test:snapshot
真实密钥放仓库根目录被 gitignore 的 .env 文件:
DEEPSEEK_API_KEY=***
DEEPSEEK_BASE_URL=https://... # 可选,默认公开 API
cookbook 扩展食谱导航
官方 cookbook 把「加什么功能」对应到「哪篇食谱」:
| 想做什么 | 读哪篇 |
|---|---|
| 加一个工具 | adding-a-tool.md(工具定义的真源) |
| 加一个 LLM 适配器 | adding-an-llm-adapter.md |
| 加一个 Chat 节点/UI 业务行 | adding-a-conversation-node.md |
| 加一个 workspace 包 | adding-a-package.md(逐文件清单) |
| 功能→机制映射总表 | extension-cookbook.md |
extension-cookbook 里最值钱的是那张「产品功能 → 插件机制」映射表:钩子系统挂在 agent/* + tools/* 事件上、上下文压缩用 ctx.compaction seam、subagent 委派用 ctx.subagents 注册表、定时任务用「定时器触发 → 空闲时 followup / 忙碌时 inject」……每一项产品功能都映射到已文档化扩展点上的监听器,没有一行修改循环本身——这就是「无特权内核」声明可验证的原因。
加包时注意 adding-a-package.md 的检查清单:包名用符合实际角色的词(Registry/Runtime/Provider 各有严格定义,不按 Cordis 基类命名),package.json 有一组 invariant(private: true、main: "lib/index.js"、@deepseek-ai/cordis 同时出现在 peer 与 devDependencies 等),由 pnpm run constraints 强制执行。
PR 审查与工程纪律
仓库的审查文化值得一提:maintaining-dsh-code-review.md 描述了一个半自动化的 PR 反馈闭环——维护工具收集合并前的人工评审反馈,比对反馈时与最终落地的 patch,用双适配器分类「是否被采纳」,再小周期更新审查 skill。规则是:根据 diff 本身作判断,不因为「评审者已批准」就直接接受。
贡献代码时还有三条纪律:
- 用 TODO 标记已知问题,按紧急度分级:
FIXME(发布阻塞)、TODO(尽快修)、XXX(有空再说)。 - lefthook 钩子负责本地快检(pre-commit 校验配对记录与暂存文件,pre-push 跑 typecheck);CI 负责全量门禁与 Node 兼容矩阵。
- 文档改动用
pnpm run doc-sync;改公开行为还需更新所属 README;双语文档由配对合并驱动维护。
Note版本基线
@deepseek-ai/dsh0.1.0-rc.6 处于 Developer Preview。clone 源码后以仓库实际状态为准,命令与包名可能已经变化。
小结
测试四层各有盲区,invariants + 快照 + 重放是质量底座;开发环境一条 pnpm install && pnpm run typecheck 就能站起来;扩展开发先查 cookbook 映射表,再按包清单动手。到这一章,dsh 从使用到架构、从防御到测试的完整拼图就齐了。