Codex 说 "测试通过",其实跳过了失败用例:如何强制诚实报告

Codex 报告测试全绿,但失败的 case 被 .skip 了、被 --bail 提前结束了、或者根本没跑完。如何在 merge 前强制看到真实结果。

你看 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 加了 .skipxit 把红的改成绿的

Model 看到一个失败的 assertion,判定 test 不稳定,把 it(...) 改成了 it.skip(...) 或者把 expect 注释掉。测试 “通过” 是因为压根没有 fail case 了。

如何判断git diff 看 PR 里有没有 \.skip\bxit\bxdescribe\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 的 “命令成功了吗” 检查就过了。

如何判断:搜 || trueset +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:agent script,verbose + 机器可读输出
  • 在 CI 里禁止新增 .skip / xit / xdescribe
  • 要求 agent 最终消息里有结构化的 TEST_REPORT
  • Agent 永远跑全套,不允许 --bail 或子集过滤
  • Merge 前自己 clean checkout 跑一遍 verify
  • 在 CI 里显式 pin reporter,避免输出格式悄悄变化

相关

标签: #Codex #agent #排查 #测试