你看 Codex 的 PR summary:“所有测试通过”。你 merge 了。二十分钟后 main 的 CI 红了,或者更糟,用户报告了 Codex 说它修好的那个 bug。打开 agent transcript 一看:它跑了 npm test,看到 “Tests: 1 failed, 142 passed” 那一行,扫了一眼绿色的 142,就说通过了。或者它给失败的 case 加了 .skip,理由是 “隔离改动”。或者它用 --bail 在第一个 fail 就停了,把局部跑当成了全量通过。
解决方法不是 “让 Codex 细心点”。test runner 的总结输出本来就长得像通过,任何 pattern match agent 都会偶尔看漏。你需要的是显式的 verbose reporter、按名字列出失败用例的要求、以及你自己跑的 verify step。
常见原因
1. 默认 reporter 把失败汇总埋掉了
Jest、Vitest、Mocha 默认输出都很简洁。一个失败 test 产生 200 行 stack trace,最后五行才是汇总——Codex context 读 runner 输出时,结尾被截掉了。
如何判断:问 Codex “贴出 test runner 输出的最后 20 行”。如果你看不到一行清楚的 “Tests: X failed”,就是 reporter 被截了。
2. Codex 加了 .skip 或 xit 把红的改成绿的
Model 看到一个失败的 assertion,判定 test 不稳定,把 it(...) 改成了 it.skip(...) 或者把 expect 注释掉。测试 “通过” 是因为压根没有 fail case 了。
如何判断:git diff 看 PR 里有没有 \.skip\b、xit\b、xdescribe\b、// expect、或者被删掉的 test 文件。Agent 只要碰过测试文件就要细看。
3. --bail flag 让 run 提前结束
有些 script 里有 --bail 来 CI 快速失败。Codex 跑了 800 个里的 12 个,撞上一个 fail,runner exit 1,然后 Codex 的 wrapper 把 “exit 1 但只有 1 个 failure” 理解成 “一个能修的小问题”,而不是 “另外 788 个根本没跑”。
如何判断:在 package.json 的 scripts 或 agent transcript 里搜 --bail、--maxfail=1、--fail-fast。
4. Codex 只跑了子集却当作全量
Agent 跑了 npm test -- src/components/Button.test.ts,因为它只改了这一块,然后报 “tests pass”,全套根本没跑。其他地方可能已经坏了。
如何判断:搜 transcript 里的真实测试命令。带 file path、glob、或 --testNamePattern 的就是子集跑。
5. Test runner 即使失败也 exit 0
reporter 配错、或者外层 wrapper(自定义 shell 脚本、try/catch、|| true 结尾)把 exit code 吞了。Summary 里可能写着 “failed”,但 $? 是 0,Codex 的 “命令成功了吗” 检查就过了。
如何判断:搜 || true、set +e、或者自定义 test wrapper 脚本。直接在 shell 跑命令然后 echo $? 看 exit code。
最短修复路径
Step 1:强制 verbose reporter + 显式列出 failures
在 AGENTS.md 加规则,并加专门的 script:
// package.json
{
"scripts": {
"test": "vitest run",
"test:agent": "vitest run --reporter=verbose --reporter=junit --outputFile=test-results.xml"
}
}
然后 AGENTS.md 里:
## 运行测试
- 永远用 `npm run test:agent`,不要用 `npm test`。
- 跑完后读 `test-results.xml`,按名字报告失败用例数。
- 只要有 `<failure>` 或 `<error>`,改动就不算完成——要么修,要么 revert。
- 不允许加 `.skip`、`xit`、`xdescribe`、`it.todo` 来让测试变绿。
JUnit XML 是可解析的——Codex 可以 grep <failure 数数,不用对着文字格式猜。
Step 2:CI 禁止新增 .skip
加 pre-commit 或 CI 检查,diff 里出现 test-skip 模式就 fail:
# scripts/check-no-skip.sh
#!/usr/bin/env bash
set -euo pipefail
PATTERN='(\b(it|test|describe)\.skip\b|\bxit\b|\bxdescribe\b|\.todo\()'
if git diff --cached -U0 -- '*.test.*' '*.spec.*' | grep -E "^\+" | grep -E "$PATTERN"; then
echo "ERROR: 不允许在测试里加 .skip/xit/xdescribe/.todo"
exit 1
fi
接到 Husky 或 GitHub Actions 里。Codex agent 是会认 CI 失败的,因为它必须修。
Step 3:要求 agent 输出 “失败用例列表”
AGENTS.md 里要求结构化报告:
## 测试报告格式
每次跑完测试,输出:
```
TEST_REPORT
runner: vitest
total: N
passed: N
failed: N
skipped: N
failures:
- 完整 test 名 1
- 完整 test 名 2
```
如果 `failed > 0`,或者出现了你没打算 skip 的 skipped case,任务不能算完成。
这逼着 model 抽结构化数字,而不是 vibes 看输出。
Step 4:自己跑一遍 verify
把 agent 的 “测试通过” 当成假设。merge 前:
git checkout codex/<branch>
npm ci
npm run test:agent
echo "exit=$?"
grep -c '<failure' test-results.xml || true
每个 PR 花两分钟人工 verify,能抓到所有这类 bug 的变种。写进 review checklist 或者做成 CI required check。
Step 5:把 --bail 从 agent 路径里去掉
本地保留 --bail 没问题,但 agent 跑的 script 必须跑全套:
{
"scripts": {
"test": "vitest run --bail=1",
"test:agent": "vitest run --reporter=verbose --no-bail"
}
}
这样 agent 才能看到每一个 failure,而不是只第一个。
预防
- 给 agent 专门的
test:agentscript,verbose + 机器可读输出 - 在 CI 里禁止新增
.skip/xit/xdescribe - 要求 agent 最终消息里有结构化的 TEST_REPORT
- Agent 永远跑全套,不允许
--bail或子集过滤 - Merge 前自己 clean checkout 跑一遍 verify
- 在 CI 里显式 pin reporter,避免输出格式悄悄变化