一、先纠正一个危险想法:测试变绿不是目标

很多人第一次让 Codex 修 CI,会这样说:

CI 挂了,帮我修一下,让测试通过。

这句话看起来没毛病,但在工程上很危险。

因为“让测试通过”有很多办法:

  • 修复真实业务逻辑
  • 修复测试隔离问题
  • 修复环境配置问题
  • 更新过期快照
  • 降低断言强度
  • 跳过失败测试
  • 把 timeout 加大
  • 把异常吞掉
  • 把失败分支删掉

前四种可能是合理修复。

后几种就很可能是在掩盖问题。

所以你给 Codex 的目标不应该是:

让测试绿。

而应该是:

找出 CI 失败的最小原因,修复真实问题,保留或增强能防止回归的测试证据。

这就是本篇的核心。

CI 修复不是“灭红灯”,而是“解释红灯为什么亮,然后用最小补丁让系统恢复正确”。

二、官方推荐的可靠性闭环:测试、检查、Review

OpenAI Codex 官方最佳实践里有一个很关键的建议:不要只让 Codex 写代码,还要让它创建或更新测试、运行相关检查、确认结果,并 Review 改动。

官方工作流示例里也强调,修 bug 时应该提供复现步骤和约束,让 Codex 先复现,再打补丁并运行检查。

这跟 CI 修复非常贴合。

你可以把 Codex 修 CI 的流程固定成 7 步:

确认失败 Job 截取关键日志 定位失败测试 判断失败归因 最小复现 最小修复 回跑验证

这套流程的价值在于:它让 Codex 先做诊断,而不是直接改代码。

三、让 Codex 读日志:不要把整段 CI 全丢给它

CI 日志通常很长。

一整个 GitHub Actions log 可能有几万行:

  • install 依赖
  • build 缓存
  • lint 输出
  • 多个测试套件
  • warning
  • retry
  • coverage
  • 最后的失败汇总

如果你直接把所有日志丢给 Codex,然后说“修一下”,它很容易被噪音带偏。

更好的做法是先让它提取结构化信息。

第 1 步:拿到失败日志

如果你用 GitHub CLI,可以先看失败 run:

gh run list --limit 10 gh run view <run-id> --log

如果日志太长,可以先截取:

gh run view <run-id> --log | tail -n 300

也可以把日志保存成文件:

gh run view <run-id> --log > ci-failure.log

然后让 Codex 读文件。

第 2 步:第一轮只分析,不修改

推荐 Prompt:

请读取 ci-failure.log。 先不要修改文件。 请输出: 1. 失败的 workflow、job、step。 2. 首个真正失败的错误,不要只看最后的汇总。 3. 失败的测试文件、测试用例名、断言差异。 4. 可能根因,按概率排序。 5. 最小复现命令。 6. 需要查看哪些源码或测试文件。

注意这句:

先不要修改文件。

修 CI 的第一步不是写补丁,而是把日志读明白。

第 3 步:让 Codex 区分“首个错误”和“连锁错误”

测试失败经常会出现这种情况:

Test A failed Test B failed Test C failed Process exited with code 1 Coverage threshold not met

真正的问题可能是 Test A。

后面的 coverage 失败只是连锁反应。

所以你可以这样追问:

请区分首个根因错误和后续连锁错误。 要求: 1. 不要把 coverage threshold、process exit code 这类结果当成根因。 2. 找到最早出现的 stack trace 或 assertion diff。 3. 说明为什么它更可能是根因。

这一步非常重要。

因为 Codex 如果误把“最后一个错误”当成根因,修复方向就会偏。

四、最小复现:让 Codex 先把失败缩小

CI 失败通常是这样开始的:

pnpm test

或者:

npm test pytest mvn test go test ./...

但你不应该让 Codex 每次都从全量测试开始修。

更稳的流程是让它找到最小复现命令。

JavaScript / TypeScript 项目

常见最小命令可能是:

pnpm test auth-callback pnpm vitest run src/auth/callback.test.ts npm test -- callback.test.ts npx jest tests/auth/callback.test.ts -t "expired session"

Python 项目

可能是:

pytest tests/test_auth_callback.py pytest tests/test_auth_callback.py::test_expired_session pytest -k expired_session

Go 项目

可能是:

go test ./internal/auth -run TestExpiredSession

Java / Maven 项目

可能是:

mvn -Dtest=AuthCallbackTest#expiredSession test

你可以让 Codex 自己根据项目识别命令:

请根据项目中的 package.json、Makefile、README、CI 配置,找出最小复现命令。 要求: 1. 优先只跑失败测试文件。 2. 如果能定位到单个 case,就只跑这个 case。 3. 先运行最小命令复现失败。 4. 如果本地无法复现,请说明可能原因,例如环境变量、数据库、时区、Node/Python 版本、网络依赖。 5. 在复现失败前不要修改代码。

最小复现的意义是:

把“CI 挂了”变成“这个测试在这个输入下失败”。

这时 Codex 才有足够清楚的靶子。

五、把失败归因分成 6 类

CI 失败不等于代码 bug。

让 Codex 修之前,先让它分类。

1. 真实业务回归

比如:

expected status 401 received status 500

或者:

Cannot read properties of null

这类通常要修生产代码,并补回归测试。

2. 测试代码本身过期

比如业务文案、快照、API shape 有意改变,但测试还在按旧行为断言。

这时可以更新测试,但必须确认行为变化是有意的。

Prompt:

请判断这是产品行为回归,还是测试期望已经过期。 要求: 1. 查找相关需求、代码注释、PR 说明或邻近测试。 2. 如果要更新测试,说明为什么新行为是正确的。 3. 不要为了通过测试直接放宽断言。

3. 测试隔离问题

典型表现:

  • 单独跑通过,全量跑失败
  • 本地偶发失败
  • 测试顺序影响结果
  • 全局 mock 没清理
  • 数据库状态残留
  • fake timer 没恢复

这类要修测试隔离,而不是修业务逻辑。

4. 环境问题

比如:

  • CI 缺少环境变量
  • Node/Python/Java 版本不一致
  • 时区不同
  • 文件路径大小写问题
  • 缓存污染
  • 系统依赖缺失

这类通常要修 CI 配置或 setup script。

5. 网络或外部服务依赖

测试里真实请求了外部 API,CI 上偶发超时。

这类不要简单加 timeout。

更好的做法是 mock 外部服务,或者把集成测试与单元测试分开。

6. Flaky test

Flaky test 是最难处理的一类。

因为它不是稳定失败,而是“有时候失败”。

下一节单独讲。

六、Flaky test:不要一上来就加 retry

Flaky test 很容易让人烦躁。

你会很想直接:

加个 retry。 扩大 timeout。 把断言改宽。 先 skip。

但这几种都可能掩盖真实问题。

正确做法是先判断 flaky 的来源。

先让 Codex 做重复运行实验

Prompt:

这个测试疑似 flaky。 请先不要修改文件。 请做以下判断: 1. 单独重复运行 10 次是否稳定。 2. 与同目录测试一起运行是否失败。 3. 与全量测试一起运行是否失败。 4. 失败是否与测试顺序、时间、随机数、网络、数据库状态或全局 mock 有关。 5. 输出最可能原因和证据。

对应命令可能是:

for i in {1..10}; do pnpm vitest run src/auth/callback.test.ts; done

或者:

pytest tests/test_auth_callback.py --count=10

具体命令让 Codex 根据项目工具链调整。

常见 flaky 根因

时间依赖

比如测试断言“刚刚创建”的时间,CI 跑得慢就失败。

修法通常是:

  • 使用 fake timer
  • 固定当前时间
  • 避免比较真实 wall-clock time
异步未等待

比如页面测试里点击后马上断言,但状态还没更新。

修法是等待明确条件,而不是 sleep。

全局状态污染

比如前一个测试改了全局配置,后一个测试继承了污染状态。

修法是 afterEach 清理状态、恢复 mock、重建容器。

随机数或排序不稳定

比如列表排序没有稳定规则,测试却断言固定顺序。

修法是让业务排序稳定,或者测试按语义断言。

网络依赖

单元测试请求真实接口。

修法是 mock 或 test double,不是增加 timeout。

什么时候可以加 retry?

只有在你已经证明失败来自外部不可控系统,并且该测试本身不是核心逻辑验证时,才考虑 retry。

比如:

  • 浏览器驱动偶发启动失败
  • 第三方沙箱环境偶发 503
  • 临时网络抖动

即便如此,也要在 PR 说明里写清楚:

这是测试基础设施层面的 retry,不是业务断言变弱。

七、测试层级:让 Codex 选“最能证明修复”的测试

修测试时,另一个常见误区是:

每次都跑全量测试。

全量测试当然重要,但它不适合每一步都跑。

你要让 Codex 先选正确的验证层级。

推荐顺序:

1. 最小失败用例

先跑失败的单个 case。

这能最快证明你的修复是否命中根因。

2. 同文件或同模块测试

防止你只修了一个 case,却破坏邻近行为。

3. 相关集成测试

如果改动跨模块,比如 auth、API、DB,就跑相关集成测试。

4. lint / typecheck

很多测试修复会引入类型、格式、未使用变量问题。

5. CI 全量

最后交给远端 CI 或本地全量跑。

你可以这样要求 Codex:

请为本次修复选择验证命令。 要求: 1. 先运行最快能证明修复的最小测试。 2. 再运行同模块测试。 3. 如果改动影响类型或 lint,请运行 typecheck/lint。 4. 不要用全量测试替代最小复现。 5. 最后说明还需要 CI 覆盖哪些全量检查。

八、让 Codex 修 CI 的标准 Prompt

下面这组模板可以直接复制。

模板 1:读日志

请读取这次 CI 失败日志。 先不要修改文件。 请输出: 1. 失败的 workflow / job / step。 2. 首个真正失败的错误。 3. 失败测试文件和测试用例名。 4. 断言差异或 stack trace 关键行。 5. 最可能根因,按概率排序。 6. 最小复现命令。 7. 下一步需要查看的文件。

模板 2:最小复现

请根据项目配置找出最小复现命令,并运行它。 要求: 1. 优先只跑失败测试文件。 2. 能定位单个 case 时,只跑单个 case。 3. 如果本地无法复现,请说明差异来源。 4. 在复现失败前不要修改代码。

模板 3:修复真实逻辑

请基于已经复现的失败做最小修复。 约束: 1. 优先修复生产代码中的真实 bug。 2. 如果需要改测试,必须说明旧断言为什么不再正确。 3. 禁止跳过测试、降低断言、吞掉异常、扩大 timeout 来掩盖失败。 4. 保持改动最小,不做无关重构。 5. 补充或保留能防止回归的测试。 6. 修复后运行最小测试和相关检查。

模板 4:修复 flaky test

这个测试疑似 flaky。 先不要修改文件。 请判断失败来源: 1. 时间依赖 2. 异步等待不足 3. 测试顺序或全局状态污染 4. 随机数或排序不稳定 5. 网络或外部服务依赖 6. CI 环境差异 请给出证据、最小复现命令和修复方案。 禁止直接 skip、放宽断言或盲目加 retry。

模板 5:验证汇总

请汇总本次 CI 修复结果。 格式: 1. 根因。 2. 改动文件。 3. 改动说明。 4. 运行命令和结果。 5. 未运行检查及原因。 6. 为什么没有削弱测试有效性。 7. 剩余风险。

模板 6:提交前复查

请审查当前 diff。 重点确认: 1. 是否真的修复了 CI 失败根因。 2. 是否降低了断言强度。 3. 是否跳过或删除了重要测试。 4. 是否扩大 timeout 或 retry 掩盖问题。 5. 是否引入无关重构。 6. 是否有足够回归测试。 不要修改文件,只输出 Review 发现。

九、用 codex exec 处理 CI 日志

如果你在 CLI 或脚本里处理日志,官方文档里提到可以用 codex exec 做非交互任务。

它适合:

  • 在 CI 或脚本里运行
  • 接收日志作为输入
  • 输出最终总结
  • 和其他命令管道组合

比如把测试输出直接交给 Codex 总结:

npm test 2>&1 \ | codex exec "请总结失败测试、最可能根因和最小修复方向。不要修改文件。" \ | tee test-summary.md

或者把 GitHub Actions 日志交给 Codex:

gh run view 123456 --log \ | codex exec "请用 5 条 bullet 总结 CI 失败原因,并给出下一步排查命令。" \ > ci-summary.md

如果要让 Codex 输出给脚本继续处理,可以用 JSON 模式或输出文件。

官方文档提到,codex exec --json 会输出 JSON Lines 事件流;只需要最终消息时,也可以把最后消息写入文件。

示例:

codex exec \ "请分析 ci-failure.log,并输出根因、最小复现命令和建议修复。" \ -o ci-analysis.md

这里有一个安全细节也很重要:

官方文档说明,codex exec 默认在只读 sandbox 中运行。自动化里应该按需要设置最小权限。

如果只是分析日志,不需要写权限。

如果要让它生成补丁,才考虑:

codex exec --sandbox workspace-write "..."

不要一上来就给全权限。

十、把 Codex 放进 GitHub Action:推荐“生成 patch,再开 PR”

上一篇我们讲过 PR Review。

这一篇再往前走一步:CI 失败后,能不能让 Codex 自动生成修复?

可以,但不要一开始就做“自动推送到原分支”。

更稳的方式是:

CI 失败 只读 checkout Codex 生成最小补丁 把 diff 保存为 patch artifact 另一个 job 应用 patch 开一个新的修复 PR 人类 Review 后再合并

官方非交互模式文档里也给了类似模式:Codex job 只用 read 权限生成 patch artifact,后续 open_pr job 再拿写权限应用 patch 并创建 PR。这样可以把 API key 暴露面和写权限隔离开。

一个简化版 Prompt 可以这样写在 Action 里:

- name: Run Codex uses: openai/codex-action@v1 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} prompt: | CI failed for this commit. Run the smallest relevant test command to reproduce the failure. Identify the minimal cause. Implement only the minimal fix. Run the failing test again. Do not refactor unrelated files. Do not skip tests. Do not weaken assertions. Do not increase timeouts unless you prove the failure is infrastructure-related.

然后保存 patch:

git add -N . git diff --binary HEAD > codex.patch

如果 patch 非空,就上传 artifact。

另一个 job 再下载 patch、git apply --index codex.patch、创建 PR。

这个模式的好处是:

  • Codex 不直接污染原 PR 分支
  • 人类可以 Review 它生成的补丁
  • 写权限不和 API key 放在同一个 job
  • 失败修复有独立提交和 PR 记录

十一、不要让 Codex 为了测试通过而破坏逻辑

这是本篇最重要的部分。

你要明确告诉 Codex:哪些修法是禁止的。

禁止 1:降低断言强度

危险修法:

- expect(response.status).toBe(401) + expect(response.status).toBeGreaterThanOrEqual(400)

这确实可能让测试通过。

但它把“必须返回 401”的业务约束变成了“随便一个 4xx 都行”。

这是测试变弱。

正确做法是先判断:

  • 业务是否真的要求 401?
  • 如果新行为应该是 403,需求依据是什么?
  • 是否需要更新文档或调用方?

禁止 2:跳过测试

危险修法:

- it("returns 401 when session expired", async () => { + it.skip("returns 401 when session expired", async () => {

除非团队明确接受临时 skip,并有 issue 跟踪,否则这不叫修复。

禁止 3:吞掉异常

危险修法:

try { return await doSomething() } catch { return null }

如果异常本来应该暴露给上层处理,吞掉它只会让 bug 更隐蔽。

禁止 4:扩大 timeout 掩盖异步问题

危险修法:

- await waitFor(...) + await waitFor(..., { timeout: 30000 })

如果问题是没有等待正确条件,扩大 timeout 只是让测试慢一点、偶尔继续失败。

禁止 5:删除关键测试

危险修法:

- test("expired session returns 401", ...)

删除失败测试通常不是修复,除非这个测试已经被更好的测试替代。

推荐写入 Prompt 的红线

修复时禁止: 1. skip / only / todo 失败测试。 2. 删除关键测试。 3. 降低断言强度。 4. 吞掉异常。 5. 盲目加 retry。 6. 扩大 timeout 掩盖异步问题。 7. 修改无关文件或做顺手重构。 如果你认为必须做其中任何一项,先停止并解释原因,等我确认。

这段很建议放进 AGENTS.md。

十二、把测试规则写进 AGENTS.md

前面几篇讲过 AGENTS.md。在 CI / 测试修复场景里,它特别有用。

你可以在仓库根目录加:

## Test and CI guidance - Always reproduce a failing test before changing code when feasible. - Prefer the smallest relevant test command first. - After fixing, run the failing test and the nearest related suite. - Do not skip tests, weaken assertions, swallow errors, or increase timeouts unless explicitly approved. - If a test is flaky, diagnose whether the cause is time, async waiting, global state, network, randomness, or CI environment before patching. - Summarize commands run, results, skipped checks, and remaining risks. ## Common commands - Install: `pnpm install` - Unit tests: `pnpm test` - Focused tests: `pnpm vitest run <file>` - Lint: `pnpm run lint` - Typecheck: `pnpm run typecheck`

这样以后你不用每次重复告诉 Codex:

不要 skip 测试,不要降低断言,先复现再修。

它会在项目上下文里自动读到这些规则。

十三、实战案例:expired session CI 失败

假设 CI 日志里有这样一段:

FAIL tests/auth/callback.test.ts expired session expected status 401 received status 500 TypeError: Cannot read properties of null (reading 'user') at src/auth/callback.ts:42:25

错误 Prompt

测试挂了,帮我修到通过。

这太危险。

正确 Prompt 第 1 轮:读日志

请分析这段 CI 失败日志。 先不要修改文件。 请输出: 1. 首个真正失败的错误。 2. 失败测试和断言差异。 3. 最可能根因。 4. 最小复现命令。 5. 需要查看哪些文件。

理想分析结果应该是:

根因可能是 callback.ts 在 session 为 null 时读取 session.user。 测试期望 expired session 返回 401,但实际抛异常导致 500。 最小复现命令:pnpm vitest run tests/auth/callback.test.ts -t "expired session" 需要查看:src/auth/callback.ts、tests/auth/callback.test.ts

正确 Prompt 第 2 轮:最小修复

请修复 expired session 返回 500 的问题。 要求: 1. 保持 API response shape 不变。 2. session 为 null 时返回原本约定的 401。 3. 保留并补充 expired session 回归测试。 4. 不重构 auth 模块。 5. 修复后运行最小复现命令和 lint。

正确 Prompt 第 3 轮:验收

请汇总本次修复: 1. 根因。 2. 改动文件。 3. 运行命令和结果。 4. 为什么测试没有被削弱。 5. 剩余风险。

这样 Codex 的输出才适合放进 PR:

根因: - callback 在 session 为 null 时读取 session.user,导致 expired session 分支抛异常。 改动: - src/auth/callback.ts:在读取 user 前处理 null session。 - tests/auth/callback.test.ts:保留 expired session 401 断言。 验证: - pnpm vitest run tests/auth/callback.test.ts -t "expired session":通过 - pnpm run lint:通过 测试有效性: - 没有放宽状态码断言,仍要求 401。

十四、提交前用 Review 面板或 /review 做最后一遍检查

CI 修复最怕“改着改着改多了”。

官方文档里提到,Codex App 的 Review 面板显示的是 Git 仓库状态,不只是 Codex 改的内容。它可以按文件、hunk stage 或 revert。

所以提交前看一眼:

git status --short git diff --stat git diff

如果你在 CLI,可以跑:

/review

或者用自定义 Prompt:

请审查当前未提交改动。 重点确认: 1. 是否只修复 CI 失败根因。 2. 是否没有削弱测试断言。 3. 是否没有 skip 或删除关键测试。 4. 是否没有吞掉异常。 5. 是否没有扩大 timeout 掩盖问题。 6. 是否包含必要回归测试。 不要修改文件,只输出高价值发现。

如果 Review 发现 Codex 改了无关文件,直接让它收回:

这次 diff 范围过大。 请撤回与 CI 失败无关的改动,只保留: 1. 根因修复。 2. 必要回归测试。 3. 必要配置修复。

十五、CSDN 读者可以直接照抄的完整流程

如果你只想记一个流程,就记这个:

1. 下载或打开失败日志

gh run view <run-id> --log > ci-failure.log

2. 让 Codex 先分析

请读取 ci-failure.log。 先不要修改文件。 请找出首个真正失败的错误、失败测试、断言差异、可能根因和最小复现命令。

3. 让 Codex 最小复现

请运行最小复现命令。 如果无法复现,说明环境差异。 复现前不要修改文件。

4. 让 Codex 最小修复

请基于复现结果做最小修复。 禁止 skip 测试、降低断言、吞异常、扩大 timeout 或做无关重构。 修复后运行最小测试和相关检查。

5. 让 Codex 汇总验证

请输出根因、改动文件、运行命令、结果、未运行检查、剩余风险。

6. 人类看 diff

git diff --stat git diff

7. 跑 Review

/review

或者让 Codex 审查当前 diff 是否削弱测试。

8. 提交 PR

git add <files> git commit git push

9. 等 CI 全量验证

本地最小测试通过,只说明修复方向对。

最终还要看远端 CI。

十六、总结

Codex 修 CI / 测试失败,最重要的不是“让它更快改代码”,而是让它慢半拍:

先读日志 先找首个错误 先最小复现 先解释根因 再最小修复 再跑验证 最后 Review diff

你可以记住 6 条红线:

  1. 没复现,不急着改。
  2. 首个错误比最后汇总更重要。
  3. 测试变绿不是目标,真实逻辑正确才是目标。
  4. 不 skip、不删测试、不降低断言。
  5. Flaky test 先诊断来源,再考虑 retry。
  6. 自动修复建议走 patch PR,让人类最后 Review。

只要这套流程固定下来,Codex 就可以在 CI 修复里承担很大一部分苦活:读日志、缩小范围、跑命令、总结结果、生成补丁。但最终你仍然要守住工程质量底线:让测试变绿,同时不能让测试变弱。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐