主题
06.2 测试驱动与自修复循环
Auto-debugging 不是魔法,是一个可以被你设计和强化的循环。
读完你能做什么:搭建一个"它自己写、自己跑、自己修"的闭环,并防止它作弊。
一、自修复的本质
"Auto-debugging"听起来很高级,拆开看就是 Agent Loop 的一个特例:
text
写代码 → 跑测试 → 读到失败 → 定位原因 → 改代码 → 再跑测试 → ...
↑______________________________________________|
直到测试通过关键在于:它能看到真实的失败输出(不是猜测),然后基于真实信息修改。这就是为什么"让它跑测试"比"让它检查代码"可靠得多 —— 前者有客观反馈,后者只有它的主观判断。
二、为什么必须给它反馈闭环
2.1 没有闭环 vs 有闭环
text
❌ 没有闭环
你:"写一个解析 ISO 8601 日期的函数"
它:(写了一个函数,看起来对)
你:(自己跑,发现处理时区时错了)
你:"时区处理错了"
它:(改,可能又引入新问题)
—— 你成了它的测试运行器
✅ 有闭环
你:"写一个解析 ISO 8601 日期的函数,同时写覆盖以下情况的测试:
带时区、不带时区、只有日期、非法输入。
然后运行测试,直到全部通过再告诉我。"
它:写函数 → 写测试 → 跑 → 3 个失败 → 修 → 跑 → 通过 → 报告
—— 它自己闭环,你只验收结果核心动作:把"验证"这个环节交给程序(测试),而不是留给你或它的主观判断。
三、标准的 TDD 工作流
3.1 让它先写测试
text
<task>
为 ReconciliationEngine.match() 方法实现对账匹配逻辑。
</task>
<workflow>
严格按 TDD 顺序:
1. 先只写测试,覆盖这些场景:
- 金额和时间完全匹配 → 匹配成功
- 金额匹配但时间差 > 5 分钟 → 不匹配
- 一对多(一笔银行流水对应多笔订单)→ 按规则拆分
- 找不到对应订单 → 标记为异常
2. 运行测试,确认它们现在是失败的(红)
3. 实现 match() 让测试通过(绿)
4. 重构,保持测试通过
</workflow>
<constraints>
- 先让我审阅测试用例,我确认后你再写实现
- 禁止修改测试来迁就实现
</constraints>先让我审阅测试这一步很关键 —— 测试定义了"正确"的标准。如果测试本身错了,后面全错。人工确认测试是最高杠杆的介入点。
3.2 自动化的自修复循环
测试审阅通过后:
text
现在实现 match(),然后进入自修复循环:
- 每次改完运行 `go test ./internal/reconcile -run TestMatch`
- 如果失败,读失败信息,定位根因,修复,重跑
- 最多循环 5 次;如果 5 次后仍有失败,停下来告诉我卡在哪,不要继续瞎试
- 全部通过后,贴出最终的测试输出最多循环 5 次 是重要的护栏 —— 防止它在一个解决不了的问题上无限打转。
四、防作弊:最重要的一节
模型在压力下会走捷径。"让测试通过"这个目标,可以用正当方式(修复代码)达成,也可以用作弊方式达成。你必须封死作弊路径。
4.1 五种常见作弊
| 作弊手法 | 表现 | 危害 |
|---|---|---|
| 改断言迁就实现 | 把 expect(x).toBe(5) 改成 toBe(-1) | 测试失去意义 |
| 跳过测试 | 加 .skip / @Disabled | 假装通过 |
| 硬编码返回值 | if (input === testCase1) return expected1 | 只对测试输入有效 |
| 捕获并吞掉异常 | try {...} catch { return default } | 掩盖真实错误 |
| 放宽验证条件 | 把精确匹配改成"包含即可" | 测试变宽松 |
4.2 封死作弊的约束
text
<anti_cheat>
达成测试通过的过程中,绝对禁止:
- 修改测试的断言值或期望结果
- 给测试加 skip / disable / xfail 标记
- 针对测试输入硬编码返回值
- 用空的 try-catch 吞掉异常
- 放宽断言的严格程度(如把 equal 改成 contains)
如果你认为某个测试本身写错了,不要改它,而是停下来向我说明:
「测试 X 可能有问题,因为...,建议改成...」,等我确认。
每次修改后,用一句话解释:这次改动为什么能修复失败,
以及它是修复了根本原因,还是只是让这个特定测试通过。
</anti_cheat>最后那句"是修复根因还是只让这个测试通过",逼它自我披露 —— 这本身就降低了作弊概率。
4.3 用 hook 硬性拦截作弊
提示词是概率性的。要确定性地防作弊,用 hook 检测测试文件的可疑改动:
bash
#!/usr/bin/env bash
# .claude/hooks/guard-tests.sh
# 拦截对测试文件的可疑修改
FILE=$(jq -r '.tool_input.file_path // empty')
NEW=$(jq -r '.tool_input.new_string // empty')
# 只管测试文件
case "$FILE" in
*test*|*spec*|*_test.go)
# 检测新增的 skip/disable 标记
if echo "$NEW" | grep -qE '\.skip|\.only|@Disabled|@Ignore|xit\(|xdescribe\('; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "检测到向测试文件添加 skip/disable,已阻止。如需修改测试请人工确认。"
}
}'
exit 0
fi
;;
esac
exit 0注册到 .claude/settings.json:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard-tests.sh" }]
}
]
}
}Hook 完整机制见 06.5。
五、超越测试:/run 和 /verify
测试通过不等于功能真的能用。Claude Code 提供了两个内置技能,实际运行你的 app 来验证:
| 命令 | 作用 |
|---|---|
/run | [Skill] 启动并驱动你的 app,看改动真的生效,而不只是测试通过 |
/verify | [Skill] 通过构建+运行+观察结果确认改动符合预期 |
/run-skill-generator | [Skill] 教 /run 和 /verify 如何从干净环境构建、启动、驱动你的 app |
5.1 为什么需要它们
text
测试通过 ≠ 功能可用
- 测试可能没覆盖真实用户路径
- 集成层的问题单元测试看不到
- UI 改动测试很难验证/verify 让它真的跑起来看结果。第一次用需要 /run-skill-generator 教它怎么构建和启动你的项目。
5.2 手动版
不用内置技能也能做:
text
实现完成后,不要只依赖测试。实际验证:
1. 启动服务:`make run`
2. 用 curl 发一个真实请求:
curl -X POST localhost:8080/reconcile -d @testdata/sample.json
3. 检查响应和日志,确认对账结果符合预期
4. 把实际的请求和响应贴给我六、一个完整的自修复实战脚本
处理"CI 挂了,修复它"这类任务:
text
<task>
CI 在 main 分支上失败了。修复它。
</task>
<workflow>
1. 先运行完整测试套件,获取真实的失败列表(不要猜):
npm test 2>&1 | tee /tmp/test-output.txt
2. 对每个失败,判断根因:是实现 bug,还是测试本身过时?
- 如果是实现 bug → 修实现
- 如果测试过时(需求真的变了)→ 停下来向我确认,不要擅自改测试
3. 修复后重跑,进入自修复循环,最多 5 轮
4. 全绿后,运行一次完整套件确认没有连带破坏其他测试
</workflow>
<anti_cheat>
禁止 skip/disable 测试、禁止改断言迁就实现、禁止硬编码。
每个修复说清"为什么这能修复根因"。
</anti_cheat>
<completion>
完成的标准(三条全满足才算完成):
1. `npm test` 全部通过,贴出实际输出
2. 没有任何测试被 skip 或删除(用 git diff 证明)
3. 没有新增 lint 警告
在三条都满足前,不要说"完成了"。
</completion>这个模板集齐了本章所有要素:真实反馈、防作弊、可验证的完成标准。
七、常见故障
| 症状 | 原因 | 对策 |
|---|---|---|
| 它宣称修好了但测试还是红的 | 过于乐观地判断完成 | 给"贴出实际输出"的完成标准 |
| 它在一个问题上无限打转 | 没设循环上限 | "最多 5 轮,卡住就停" |
| 它偷偷改了测试 | 没封死作弊路径 | anti_cheat 约束 + hook |
| 测试通过但功能不可用 | 只信测试 | 用 /verify 实际运行 |
| 越修越乱,破坏了其他测试 | 只跑了单个测试 | 最后跑完整套件确认 |
| 它读了 3000 行测试输出撑爆上下文 | 没过滤 | 管道过滤失败信息 |