768 项测试通过,AI 代码仍漏掉高风险缺陷
作者的 iOS 应用修改被报告通过 768 项测试,独立审查却发现并发打开会混淆聊天数据,重开失败回复还会意外改写状态与文件。案例说明测试数量无法证明场景覆盖充分,修复又经过两轮审查才获通过。
AI 深度解读
AI 编程代理说「768 项测试全部通过」,能证明代码已经可以交付吗?作者在 ChatPing iOS 应用里遇到的情况是:独立审查者没有重跑测试,仅靠检查代码,就发现了两个现有测试没有覆盖的高风险问题。
作者因此在「完成」之前加了三道检查:独立审查代码、核验运行状态、区分执行失败与没有结果。它们值得借鉴的地方,是把代理的口头汇报变成可以追查的证据;但每道检查都有成本,也有绕过去的缝隙。
768 项测试通过,为什么仍然不能交付
ChatPing 那次修改的问题,都藏在测试场景之外。
第一个是并发打开会话。如果打开 A、B 的操作交错执行,程序可能把 ChatCapability 绑定到 A,却把 B 的 SDK 快照复制进 rawConversation。会话身份和聊天记录串了,但新增测试只覆盖一次顺序打开。
第二个是重新打开包含失败回复的会话,会改变最近活动时间,并重写文件。测试夹具里只有已经完成的回复,自然碰不到这个分支。独立审查给出 VERDICT: FAIL,两项发现均为 High;之后又经过两轮审查才通过。
测试通过数量只说明执行了多少项检查,覆盖是否充分还要另外证明。
作者担心的是一条由同一个代理包办的证据链:写实现、选测试场景、运行测试、阅读输出、汇报结果。如果实现阶段漏掉某种情况,测试阶段很可能沿着同样的思路继续漏掉。
他们采用的独立审查者是 Codex,通过只读 wrapper 运行,由另一个 worker 修改代码。wrapper 要求最后一行必须严格为 VERDICT: PASS 或 VERDICT: FAIL。
这里还有一个更能说明规则边界的例子。作者把 shared ping-ai-sdk 中的 grounding renderer 从 Android 移植到 iOS,要求审查者自行构建和测试。Codex 对照 Android 实现检查后,没有发现阻塞性的静态问题,却因为无法独立运行测试而拒绝放行:
No blocking code/file:line issues found in static review. [...]
Gate blocker: I attempted all requested SwiftPM commands from `ios/`, but this
session's read-only sandbox prevented Swift/Xcode from creating `/tmp/xcrun_db-*`
cache files, so no independent pass counts were produced:
- `swift build` failed before compilation: `errno=Operation not permitted`
- `swift test --filter GroundingRendererTests` failed same way
- `swift test --filter TextFeaturesTests` failed same way
- `swift test` failed same way
VERDICT: FAIL
关键是最后一行:这里的 FAIL 表示交付门禁未满足,并没有证明产品代码存在缺陷。沙箱阻止 Swift/Xcode 创建缓存,独立测试结果缺失,所以不能批准。
「还在运行」需要本轮证据,也需要下一次检查
第二道检查针对状态汇报。代理说构建「仍在运行」,可能只是根据旧日志推断,根本没有查看当前进程。作者提到过一次挂住七小时的测试:日志描述与机器实际状态不一致。
把要求写进 CLAUDE.md 没有稳定奏效,于是作者增加 Claude Code Stop hook:verify-before-status-claim-hook.sh。代理准备结束当前轮次时,hook 读取这一轮的 transcript,匹配 still running、in progress、waiting for 等状态表述,再检查本轮有没有符合条件的工具调用。
认可的证据包括读取图片,以及以 ps、adb、stat、date、logcat 等开头的 Bash 命令。缺少证据就阻止结束。脚本用 Bash 和 jq 实现,从 Claude Code 经 stdin 传入的 transcript 路径读取记录,并注册到 settings.json 的 hooks.Stop,执行路径为 $HOME/.claude-shared/scripts/verify-before-status-claim-hook.sh。
不过,查过一次进程仍然只是快照。作者还要求本轮调用 ScheduleWakeup,安排在 15 分钟内再次检查,并明确要看哪个进程、设备或日志修改时间。「稍后看看」不算。
这套规则有明确漏洞:单独执行 date 也能满足工具检查,hook 并不判断命令是否真的对应当前任务。缺少 jq 或 transcript、阻止后再次运行 hook、没有下一轮的 headless worker 等情况,也会放行;本轮刚启动后台任务则交给另一道唤醒检查处理。脚本本身没有公开,文中的正则也经过删减,不能直接运行。
维护成本同样真实。作者一天内对同一个 hook 做了七轮 Codex 对抗审查,却无法判断第三至第七轮是否值得。他自己的规则原本要求:同一目标超过两轮,第三轮开始前应书面决定修复、登记后续事项,还是删除检查。
性能问题甚至能让检查失去使用价值。v9 之前,每轮重读全部 transcript,在 71,525 行会话上耗时约 9.5 分钟;v10 把工具调用字段提取合并到一次 jq 扫描后,包含 200 次调用的一轮处理从 3.5 秒降到 94 ms。这两组数字对应不同优化阶段,不能当成同一组前后对照。
超时没有结果,不能算通过,也不能直接算测试失败
第三道检查处理长任务失去响应的情况。
作者把预计超过几分钟的发布门禁、设备测试和构建交给 watchdog wrapper。long-run-watchdog.md 强调:退出码 124 和 125 都不是产品判定,只能说明本次运行没有产生结论。原文没有给出二者分别对应的完整规则,不能自行补全。
这里还有退出码冲突:子进程本身也可能返回 124 或 125,外部看到的数字相同。因此 watchdog 会在 stderr 输出说明,帮助判断退出来自哪里。先看诊断,再决定是测试失败、工具问题,还是仍无结论。
审查 wrapper 的规则类似。退出码 0 表示存在审查结论,还要读取末尾的 VERDICT: PASS 或 VERDICT: FAIL,并查看开头标识,确认审查由 Codex 还是替代的 Claude Opus 完成。Codex 配额耗尽时,wrapper 会切换到 Claude Opus 对抗审查,替代审查同样必须给出明确结论。
进程正常退出、审查完成、代码获准交付,是三个需要分别确认的状态。
超时以及其他非零退出,都属于没有取得有效批准。缺失的结论不能向上取整成 PASS。
我的判断
我认同作者把「实现者汇报」和「独立证据」分开的做法。并发交错、失败状态恢复,确实容易被只验证正常路径的测试遗漏。独立审查至少增加了一次从其他角度寻找反例的机会。
但这些材料是个案和经验。作者没有提供与人工审查、普通 CI、同一代理自查的对照数据,也没有证明换一个模型就能消除共同盲区。独立运行测试可以确认结果可复现,却仍然无法单靠绿灯证明覆盖充分。
Stop hook 则更适合当提醒机制。检查本轮出现过某条命令,距离验证状态声明真实,还有明显差距。团队若已有 CI 任务状态或进程监控,优先读取这些明确状态通常更直接;是否值得维护 transcript 正则,要看实际误报和漏报。
另外,门禁失败最好附带原因。代码缺陷、环境阻塞、执行超时都可以阻止合入,但后续处理完全不同。保留这个区别,才能避免开发者拿着沙箱权限错误去排查业务逻辑。
能拿来做什么
- 给日常 AI 编程任务增加一次独立审查,明确要求检查并发交错、失败恢复等相关分支,并由审查方实际运行测试。缺少运行条件时,记录阻塞原因。
- 在前端构建或 Node 测试脚本外增加结果分类,分别记录命令执行结果、是否超时、是否产出测试报告,避免把任务结束直接显示为测试通过。
- 让代理汇报后台任务时附上本轮查询结果;需要持续等待的任务,同时安排下一次具体检查,不能只留一句「还在跑」。
- 给自定义 hook 预先限定审查轮数,并测量长会话下的开销。超过预算后明确选择继续修复、登记后续事项或删除规则,避免检查本身拖慢工作。