一个 bug 的价值,不在于那行修复,而在于为什么流程放过了它,以及新增了什么防护让同类问题下次明确报错。
复盘文化:四问
| 四问 | 要回答什么 |
|---|---|
| 什么坏了 | 用简短段落让忙碌的读者三十秒内吸收要点 |
| 机制是什么 | 用直白的话说清根因,不归咎个人 |
| 为什么每道安全网都没拦住 | 找出测试、工具、约定的缺口,而非一次性笔误 |
| 新增了什么防护 | 测试、AGENTS.md 规则、ADR,让同类 bug 下次明确报错 |
三个条件同时满足才写复盘:隐蔽(机制不显而易见)、系统性(逃逸原因是测试/工具/约定缺口)、重新发现的代价高。
四个真实案例
复盘 0001:ACP 服务器在连接时崩溃——export default 丢掉了插件的 inject。
- 什么坏了:编辑器(Zed)一连上,第一个
session/new报cannot get property "agents" without inject。 - 机制:插件多写了一个
export default apply,Loader 的unwrapExports取到裸函数,把命名空间上的inject整个丢掉。 - 为什么没拦住:178 个绿色单元测试 + 100% 行覆盖率都在,但所有测试都通过手动
ctx.plugin(...)挂载,绕过真实 Loader 加载路径。 - 新增防护:删除 default export;增加无需 key 的真实 Loader 冒烟测试;规则"测试真实入口路径,行覆盖率不等于行为覆盖率"。
复盘 0002:文件系统快照工具被一个字面量 !!js 对象永久禁用。
- 什么坏了:七个文件系统场景调用注册表里不存在的工具,返回
UNKNOWN_TOOL。 - 机制:作者用
disabled: !!js ...想条件启用文件系统插件,但 Cordis 只在插件config内部对 JS 表达式求值;直接读disabled配置项时看到的是 truthy 对象。 - 为什么没拦住:快照刷新把"确定性回放"当成了"行为正确"——证明了回归稳定复现,却没证明文件系统工具真的注册了。
- 新增防护:改用显式文件系统 overlay;静态配置守卫拒绝 Loader 配置项元数据里的表达式节点;快照框架拒绝结构化
UNKNOWN_TOOL结果。
复盘 0003:Web agent 验收了替代服务器,而非承载其会话的 GUI。
- 什么坏了:agent 修改了 GUI 源码,却不知道当前会话对应哪个 URL、由哪个进程承载。
- 机制:它把裸 Vite 返回的 HTTP 200 当作成功(其实白屏),随后去验收另一端口上的替代
dsh web服务器,从未探测 3081 端口。 - 为什么没拦住:Web 组合没向模型提供当前 GUI、规范 URL 或运行模式的身份信息;第一个回归测试还用"进程超时"冒充"快速失败"产生误报。
- 新增防护:启动器发布规范环回 URL 与实际生产/开发模式(环境变量 + 提示词区段);独立 Vite 服务模式在配置阶段拒绝启动;分层真实路径测试覆盖 CLI、提示词、运行时事实与浏览器 HMR。
复盘 0004:Landlock 部分强制执行通知导致子进程失败被误归类。
- 什么坏了:较旧 Landlock ABI 内核上,ripgrep 无匹配时以退出码 1 正常结束,却被呈现为
SANDBOX_UNAVAILABLE沙箱故障。 - 机制:launcher 打印无害的
landlock-run: partial enforcement (older Landlock ABI)通知;harness 用不区分大小写的landlock-run:子串把通知与任意非零退出组合,误判为 runner 失败。 - 为什么没拦住:沙箱结果类型只能表达一组子字符串,无法表达"Landlock 失败必须退出码 125 + 一行致命诊断";测试矩阵从不构造"通知后跟非零子进程退出"的组合。
- 新增防护:
RunnerFailureRule携带允许退出码、逐行致命签名与精确排除的信息性行;文件系统搜索改用ctx.subprocess跑打包的 ripgrep,不再经过沙箱化 bash。
共同经验:测试必须走真实入口路径。手动挂载、mock 一切、把快照刷新当验收——都会让"单元全绿、产品却坏了"成为可能。
测试四层
| 层级 | 命令 | 抓什么 |
|---|---|---|
| 单元测试 | pnpm run test | vitest 跑包内测试,优先边界、错误路径、事件顺序、并发竞态 |
| 覆盖率门禁 | pnpm run test:coverage | 按文件 100% 覆盖;未覆盖行往往是该删除的死代码 |
| 真实 API e2e | pnpm run test:e2e | 带密钥调用真实提供方 API;缺密钥自动跳过,keyless CI 保持绿色 |
| 快照 | pnpm run test:snapshot / test:web | 无密钥预期输出覆盖对外行为;浏览器快照用 Chromium 回放比较 |
- 推理在这里很便宜,不要吝惜真实 API 测试;无密钥测试只能证明底层通路,带密钥运行才能证明 agent 对接真实模型正常。价值最高的是冒烟测试(启动真实示例、发一条提示词、检查外部世界)。
- 行覆盖率是必要条件,永远不是充分条件(0001 案例 100% 覆盖率仍放过两个集成 bug)。
- 验证外部世界而非自我报告:e2e 断言应重新运行命令或从外部重新读取文件;对 agent 输出做关键词探测会让作弊的 agent 通过;断言未修改的文件应逐字节一致。
文档纪律:一个事实一个家
verify-type-equiv门禁:用 TypeScript 解析器从源码提取类型声明符号及 JSDoc,断言文档代码块同时匹配两者;改动已记录类型或 JSDoc 时门禁失败直到更新粘贴内容——文档类型定义与源码永远一致。- 每个事实只在一个文件维护,其余文件引用它(如工具 schema 真源在
adding-a-tool.md)。 - 中英文档双语配对维护:先
pnpm run gen-doc-graphs更新英文,再更新中文并验证配对。
动手示例:三十秒执行摘要模板
1# 复盘 0005:runoob 演示环境的工具黑名单没有生效
2
3## 摘要
4demo 环境里模型仍能调用被禁用的 fs_write,直接写入了工作区。
5根因是黑名单插件用 `tools/pre-execute` 返回了 `next()`,
6而真正负责拦截的守卫注册在插件卸载之后——加载顺序错了。
7逃逸原因是加载顺序没有测试覆盖,`cordis.yml` 只测了「能起来」。
8新增防护:给黑名单插件补一条「顺序敏感」的装配测试,
9并在 AGENTS.md 记录「pre-execute 与 guard 的注册顺序」规则。
三十秒摘要公式:坏什么 → 直白说根因 → 为什么逃逸 → 可长期沿用的教训。
系列结语(四句带走)
- 模型负责聪明,Harness 负责可靠。约束不是限制,而是让 Agent 可预测、可审计、可回放的基石。
- 一切皆插件。把策略放在扩展点上,而不是写进循环里。
- 真实入口路径胜过一切 mock。覆盖率、快照、冒烟测试各司其职,共同防止"全绿却坏了"。
- 故障不可怕,可怕的是不知道为什么。复盘文化把一次事故变成一套防护。
跨篇速查表(关键命令汇总)
1# 安装插件到 profile
2dsh plugin --profile demo add ./hello-plugin
3dsh plugin --profile demo add github:you/hello-plugin
4dsh plugin --profile demo add github:you/hello-plugin#<sha> # 锁定 commit
5dsh plugin --profile demo add ./hello-plugin-0.1.0.tgz
6dsh plugin --profile demo add your-package
7
8# 移除 / 验证
9dsh plugin --profile demo remove dsh-hello-plugin
10dsh --profile demo --dump-config
11dsh --profile demo
12
13# 发布
14pnpm build && pnpm publish # npm 发布
15pnpm pack # tarball
关键运行时可组合要素:ctx.sandbox.confine(沙箱 argv 包装)、ctx.sandboxPolicy.resolve(策略解析)、ctx.approval / approval/request waterfall、ctx.tools.guard()(单调守卫)、ctx.permissionPresets(具名预设)、Session.deriveMessages()(日志投影)、tools/* / agent/* / session/event 事件流。