Agent 测试日志不能只看退出码
作者分析约 1.6 万次公开 Agent 运行,发现管道、命令串联及超时等情况会干扰测试结果判断;某模型的表面失败数经修正从 88 次降至 59 次。文章提出结合命令结构和退出码语义,将无法确定的结果标为未知。
AI 深度解读
从 coding agent 的日志里判断「最后一次测试有没有通过」,并不是找到测试命令、读取退出码就够了。作者分析了约 16,000 次公开运行记录:某个模型原先有 88 次被判为最后测试失败,修正规则后只剩 59 次,约三分之一的「失败」判错了。
问题出在证据的解释上:退出码可能属于别的命令,测试可能根本没执行,agent 说的「通过」也可能只是一项计划。对正在做 AI 编程审计、CI 日志分析的团队,这篇最有用的是教你分清:程序返回了什么,以及你究竟能据此得出什么结论。
退出码究竟是谁的结果
下面这条命令用来保留 pytest 输出的最后几行:
pytest | tail -5
要盯住的是 | tail -5:即使测试失败,整条命令也可能退出 0,因为拿到的是 tail 的退出码。pytest; echo done 和 pytest || true 也有类似问题。
作者的处理方式是按 |、&&、||、; 拆分命令,找到测试运行器。只有运行器没接管道、后续连接符全是 &&,才信任成功退出;只有运行器还是最后一个片段,才信任失败退出。其余情况归为 unknown。
非零退出也不能直接等于测试断言失败。作者指出,pytest 的退出码 1 才表示测试实际失败;数据里的 timeout 30s go test 返回 124、pytest 返回 4 或 5,以及 Qt 异常中止返回 134,都曾被错误计入测试失败。
这条规则不能照搬给所有工具。npm test 返回 2,仍可能是真实测试失败。
退出码首先说明命令如何结束,测试是否通过还需要额外证据。
优先读测试摘要,但先排除这些陷阱
作者把运行器的摘要放在退出码前面:如果输出明确写着 1 failed, 5 passed,即使退出码属于 tail,也已经有测试失败的证据。
不过,摘要不能靠一个宽泛正则解决。
ANSI 颜色转义序列要先去掉,否则 \x1b[32m12 passed\x1b[0m 可能匹配不到。覆盖率门禁也要单独处理:FAIL Required test coverage of 90% not reached 来自 pytest-cov,它说明覆盖率未达标,测试用例可能全部通过。识别测试失败前,应先排除这类行。
不同工具的计数顺序也会坑人。ospec 输出 passing: 95 failing: 0,粗糙的「数字 + failing」匹配可能把中间的 95 failing 认成失败数。同样,0 failed 不能被判为失败,作者建议先排除零计数表达。
还要保留第三种答案。collected 0 items、no tests ran、ERROR: usage: 都不能证明测试通过或失败。
这里最好区分两个问题:测试用例有没有失败,以及整个验证流程有没有成功。覆盖率未达标、超时、用法错误,可能都需要处理,但不该被塞进同一个「测试失败」计数。
测试跑过,也未必验证了当前修改
作者列出的另一组误判,来自测试对象搞错了。下面两条命令分别用于收集测试和检查修改前的基线:
pytest --collect-only -q # lists tests, runs none
git stash && pytest -q; git stash pop # tests the code before the change
第一行要看 --collect-only,它只列出测试,不执行。第二行要看 git stash 到 git stash pop 之间的代码状态:此时测试的是暂存修改后的基线,用来判断失败是否早已存在。
日志分析器因此需要跨命令、跨工具调用跟踪 stash 状态,不能只找文本里最后一次 pytest。作者还踩过存储截断的坑:每条命令只留前 500 个字符,结果长 heredoc 后面的 pytest 被截掉了。
判断 agent 有没有「声称通过」更难。to verify that all tests pass 是计划;a focused test pass 里的 pass 是名词;the test passes a tf.ragged.constant as input 说的是传参;some tests are passing 只表示部分进展。
反过来,消息里出现 error,也不代表它向用户披露了测试失败。作者抽取 50 条关键词命中结果,由 AI 模型复核,其中 11 条被标记为没有真正披露失败。这里的复核者不是人,不能把它当成人工标注的标准答案。
我的判断
我认同作者最关键的取舍:证据不足时保留 unknown。否则,一段看似确定的统计,可能只是解析器把复杂情况强行分成了两类。
作者最初算出某个模型有 15% 的声明自相矛盾,修正后约为 2%;共有三项主要统计数字因抽样检查而改变。这足以说明审计规则也需要审计,却不能证明修正版已经覆盖所有情况。原文没给出完整的人工标注集,也没给出与其他解析方法的对照数据。
按连接符拆命令,是实用的经验规则。一般团队如果能直接接入测试运行器的结构化报告,我更倾向于先读报告,再用日志补充上下文;只能拿到历史文本记录时,作者的方法才更有参考价值。
作者把检查做成了 Claude Code 和 Codex 的 Stop hook:最后一次测试失败、最终回复又没披露时,让 agent 返回处理一次。原文说实现位于 src/audit.mjs 和 src/evidence.mjs,附有测试,但没有展开 hook 的接入方式。文章本身也由维护仓库的 AI agent 自主撰写发布,这些结果适合作为复查线索,暂不宜当作模型可靠性排名。
能拿来做什么
- 给前端或 Node 项目的 AI 编程日志增加 pass、fail、unknown 三态,并单独记录覆盖率门禁、超时和未执行测试,避免把不同问题混成一个失败率。
- 检查 agent 使用的
npm test、pytest 等命令时,同时保存完整命令和输出摘要;遇到管道、容错表达式或 stash 基线测试,要核对结果归属。 - 上线「最终回复是否隐瞒测试失败」检查前,随机抽查命中记录,分别确认测试证据和声明语义,并记录规则修正前后的数字。