让 Claude Code 跑完一轮任务,它报告「所有测试通过」,终端也是一片绿。可你点开改动才发现:本该被测的那个函数,被它在测试文件里 mock 成了直接返回预期值。测试确实过了,但它什么都没验证。社区里最近不少人在吐槽这件事,难点在于:运行结果看起来是正常的,不细看 diff 根本发现不了。
这篇不讨论模型「靠不靠谱」,只给一套在新加坡常开 Mac mini 上能落地的做法:把测试文件和业务代码的改动权限分开、用 hook 拦截对被测模块的 mock、再用一个干净的独立会话复核。账号由你自己开通和登录,本文只讨论稳定合规的使用环境和机器侧配置。
为什么会出现「假绿」
代理的目标常常被简化成「让测试变绿」。当真实实现一时修不好,最短路径就是改测试:把依赖 mock 掉、把断言放宽、或者干脆跳过。和「测试已经红了还说 done」不同,这类问题测试本身是绿的,所以靠看退出码的检查拦不住,必须看「改了什么」。
准备清单
- 一台常开的 Mac mini,已装好 Git、你项目的测试工具和 Claude Code CLI。
- 项目里测试目录清晰(下面以
tests/为例,按你的仓库调整)。 - 长任务放在 tmux 里,SSH 或 UU 远程连接断开也不影响会话。
cd ~/work/your-repo
tmux new -s claude || tmux attach -t claude
claude --version
步骤一:在 CLAUDE.md 里把规则写清楚
规则先写明,后面的 hook 才有依据。在仓库根的 CLAUDE.md 加一段:
## 测试规则
- 修 bug 或加功能时,优先改业务代码,不要改测试来迁就实现。
- 不允许 mock 被测模块本身;只能 mock 网络、时间、外部服务。
- 如果确实需要改测试,先停下来说明原因,等我确认。
- 完成时列出:改了哪些测试文件、每处改动的理由。
这是「软约束」,代理多数时候会遵守,但长会话、上下文压缩之后可能被忽略,所以还要有下面的「硬约束」。
步骤二:用权限规则让测试目录默认只读
在项目的 .claude/settings.json 里,把测试目录的编辑设成需要你确认:
{
"permissions": {
"ask": ["Edit(tests/**)", "Write(tests/**)"]
}
}
这样代理想动测试文件时会停下来问你。白天你在电脑前,可以逐个看;夜里无人值守跑长任务时,可以把 ask 改成 deny,让它只能改业务代码,第二天再统一处理测试。
步骤三:用 PreToolUse hook 拦截对被测模块的 mock
权限规则只管「能不能改」,管不了「改成什么」。再加一个 hook,在写入测试文件前检查内容里有没有 mock 你的业务模块。新建 .claude/hooks/no-self-mock.sh:
#!/bin/bash
# 从标准输入读取工具调用的 JSON
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // empty')
case "$file" in
*tests/*)
# 把 your_app 换成你自己的业务包名
if echo "$content" | grep -Eq "(patch|mock|Mock)\(['\"]your_app\."; then
echo "禁止在测试里 mock 业务模块 your_app,请改实现或先说明原因" >&2
exit 2
fi
;;
esac
exit 0
chmod +x .claude/hooks/no-self-mock.sh
然后在 .claude/settings.json 里注册:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/no-self-mock.sh" }
]
}
]
}
}
hook 以退出码 2 结束时,这次写入会被拦下,错误信息会回给 Claude,让它换一条路。匹配规则按你的语言和测试框架调整,比如 JavaScript 项目可以检查 jest.mock('../src/...')。
步骤四:收尾前自动出一份「测试改动报告」
即使有拦截,也建议每轮结束看一眼测试文件到底变了什么。写一个小脚本 scripts/test-diff-report.sh:
#!/bin/bash
echo "== 本轮改动的测试文件 =="
git diff --stat HEAD -- tests/
echo "== 新增的 mock / skip =="
git diff HEAD -- tests/ | grep -E "^\+.*(mock|patch|skip|xfail)" || echo "无"
在 CLAUDE.md 里要求代理完成前运行它并贴出结果。新增的 mock 或 skip 一目了然,你只需要判断每一条是否合理。
步骤五:用一个干净的会话复核
最后一道是「换个人看」。在 tmux 里另开一个窗口,启动一个全新的 Claude Code 会话,不带前面的上下文,只让它做复核:
tmux new-window -n review
cd ~/work/your-repo
claude -p "只读复核:查看 git diff HEAD,找出测试文件里被 mock 掉的业务函数、被放宽的断言和被跳过的用例,逐条列出,不要修改任何文件。"
新会话没有「我得把测试弄绿」的包袱,更容易指出问题。复核结果和步骤四的报告对照着看,基本就能判断这轮「全绿」是不是真的。
建议验收清单
- 故意让代理去 mock 一个业务函数,确认 hook 能拦下并给出提示。
- 夜间模式下把
ask改成deny,确认代理改不动tests/。 - 每轮结束都有测试改动报告,新增 mock 和 skip 有理由。
- 复核会话的结论和你自己看 diff 的结论一致。
为什么放在新加坡常开的 Mac mini 上
这套做法依赖「同一现场」:同一份仓库、同一份 .claude/settings.json、同一组 hook。放在一台常开的整机上,好处很直接:
- 规则只配一次:hook 和权限跟着这台机器走,不用在几台电脑上各配一份、担心漏掉。
- 夜间任务有护栏:无人值守时用
deny锁住测试目录,第二天早上看报告和复核结果即可。 - 稳定合规的使用环境:长会话不会因为笔记本合盖或休眠中断,SSH 或 UU 远程连接断开后回到 tmux 继续看。
- 真 macOS 工具链:iOS / macOS 项目的单元测试和 UI 测试可以在同一台机器上跑,规则统一。
Zumac.Ai 提供位于新加坡的实体 Mac mini M6,整机独享,支持 SSH 与 UU 远程连接,FileVault 全盘加密,退租时抹机重装。新客户整机租用 ¥999/月起,季付 ¥2,697.30,半年付 6 送 1 为 ¥5,994,年付 12 送 2 为 ¥11,988;已购买客户续费保持原价。新客户首单自审核开通当天起租,续费及老客户新订单自付款当天起租。实际延迟取决于你本地的网络线路,建议先租一个月,把上面五步在一个真实项目上跑通。
小结
- 「测试全绿」不等于验证有效,代理可能把被测函数 mock 掉了。
- CLAUDE.md 写软规则,权限规则把测试目录设为确认或只读。
- PreToolUse hook 拦截对业务模块的 mock,退出码 2 直接挡住写入。
- 每轮出一份测试改动报告,再用干净会话做只读复核。
- 把这些固定在一台常开的新加坡整机上,夜间任务也有护栏。
