用二分定位智能体补丁引发的测试污染
文章指出,智能体新增测试文件可能改变执行顺序,让缓存、环境变量或会话级夹具残留触发失败。建议先确认目标测试单独通过,再缩减导致失败的前置测试集合,区分顺序依赖与真正的随机故障。
AI 深度解读
Agent 补丁让测试挂了,单独重跑却通过,问题就一定是「偶发失败」吗?作者提出,很多被贴上 flaky 标签的测试,其实受执行顺序影响:前面的测试留下共享状态,后面的测试才失败。新增文件改变了顺序,原有问题就暴露出来。
文章给出的办法很具体:先确认目标测试能独立通过,再缩小让它失败的前置测试集合,最后决定修 fixture 还是暂时隔离。这套思路值得借鉴,但配套脚本还不能直接当成可靠的 CI 裁判。
新增一个测试文件,为什么会弄坏旧测试
作者讨论的是 Agent 提交补丁后的测试流水线。测试运行器通常按固定顺序收集文件,例如按路径字母排序。新增 tests/test_auth_refresh.py,可能改变它与已有 tests/test_z_session.py 之间的执行安排,即使旧测试一行没改。
问题的载体往往是会话级 fixture,也就是在整轮测试中被重复使用的准备环境。缓存、临时数据库、环境变量、模块级 client,都可能把一个测试的影响带到下一个测试。
例如,前面的测试在共享 client 上创建了 session,却没有清理;后面的 test_rotate 原本假设 client 是干净的,于是失败。它在 head 上整套运行失败,在 base 上通过,单独重跑也通过,看起来很像随机故障。
单独通过只能说明当前执行条件下通过,不能证明整套运行时的失败具有随机性。
作者据此区分两种处理方向:真正随机的失败可以考虑临时冻结,顺序污染应该修 fixture。把后者长期塞进隔离名单,只会让共享状态问题继续存在,等下一次文件顺序变化时再出现。
先证明能独立通过,再寻找污染它的测试
第一步是在 head 上反复单独执行目标测试。原文要求连续通过 5 次,并关闭相关插件:
TARGET="tests/test_z_session.py::test_rotate"
BASE_CMD=(python -m pytest -q -p no:randomly -p no:cacheprovider)
for i in 1 2 3 4 5; do
"${BASE_CMD[@]}" "$TARGET" >/dev/null 2>&1 || {
echo "fails alone: order dependence is not the primary hypothesis"; exit 1; }
done
echo "passes alone 5/5 — continue to bisection"
要留意 BASE_CMD:-p no:randomly 禁用随机排序插件,-p no:cacheprovider 禁用 pytest 缓存插件。后续污染实验也要采用一致配置,否则比较的就不是同一组条件。
接着比较 base 与 head。head 单独失败、base 单独通过,应按补丁回归处理并阻止补丁进入;两边单独都失败,属于已有问题,另开工单;两边单独都通过,才进入顺序污染排查。上面的代码只检查 head,base 仍需另行验证。
第二步是缩小前置集合:找出哪些测试按原有相对顺序先执行,仍能让目标失败。作者使用 delta debugging,也就是反复删减候选集合,再运行实验,保留仍能触发故障的部分。400 个前置测试很难审查,一个污染测试就容易追到具体资源。
脚本的使用方式是:
python orderbisect.py "tests/test_z_session.py::test_rotate" tests/
原文给出的示例结果只剩 tests/test_auth_refresh.py::test_first_login_creates_session。这是说明性输出,没有附上真实仓库和完整实验记录。
找到组合后,还可以交换顺序再运行。如果两种顺序都失败,作者认为应转向共享全局状态排查,而不只盯着单向依赖。不过,这仍是线索,具体原因要靠断言和清理逻辑确认。
缩小复现之后,才有资格讨论隔离
作者要求先分类,再决定是否冻结测试,并强调应把责任人、关联工单和解除条件写清楚。但输入原文在这里缺了分类表,只留下「仅第 3 行可以冻结,第 1、2 行应修测试」的描述,无法还原每一行的完整判定条件。
原文随后展示了一个带原因的 @pytest.mark.skip(reason="order-dependent pollution, see #412"):注释说明,test_first_login_creates_session 在共享 client 上留下了活跃 session,跟踪工单是 #412,等 client fixture 改为函数级作用域后复查。
这段示例强调了可追踪性,却也与前面「顺序污染应修 fixture」的主张存在张力。可以借鉴它记录原因与退出条件的方式,不能据此认定发现污染后就应该加 skip。
执行成本也有边界。作者称 ddmin 最坏需要 O(n log n) 次运行;如果整套测试一次要二十分钟,应先缩到文件级,再细化到 node ID。使用 -n auto 并行执行时,worker 调度会破坏固定顺序前提,因此定位过程需要串行。
分片 CI、每测试独立容器、worker 隔离等安排,可以减少部分共享状态,但仍要看隔离粒度。这个流程也不擅长定位线程竞争:依赖两个线程特定交错的故障,未必能缩成一组前置测试。
我的判断
我认同作者把问题从「它是不是 flaky」改成「谁先执行会弄坏它」。前者容易得到一个标签,后者更接近可复现、可审查的修复证据。随机排序适合发现隐藏依赖,固定顺序适合定位依赖,两者可以分工使用。
但脚本的结果需要打问号。它的 pollutes() 只判断整个 pytest 进程是否非零退出,没有确认失败的确实是目标测试。候选测试自己报错、收集失败,也可能被算成污染。
候选集合也不是严格的「目标之前的前缀」:代码收集整个目录后,只排除了目标,再把目标追加到最后,因此原本排在目标后面的测试也会参与实验。
还有两处实现与承诺不完全对应。collect() 使用 PYTEST[:3],丢掉了运行阶段的插件禁用参数;ddmin 只检查拆出的子集合,没有检查补集,因此不能保证注释承诺的「1-minimal」,即再删掉任意一个成员就无法复现。原文给出的复杂度也应结合具体缩减算法判断。
这篇文章最有价值的是排查方法,脚本适合作为起点,不能直接把输出当成结论。
作者提到用 MonkeyCode 免费服务器批量定位,再让免费模型阅读缩小后的测试集合,检查 fixture、环境变量、缓存和临时路径。文章明确披露了 MonkeyCode 产品推广合作,且没有提供成本或准确率对照数据;资源选择属于作者经验。让模型提出污染假设,再由人核对两个测试,这个分工更值得保留。
能拿来做什么
- 遇到 AI 补丁导致「整套失败、单测通过」时,先比较 base 与 head 的独立运行结果,再决定查回归还是查顺序污染,不要仅凭重跑成功就放行。
- 给前端或 Node 测试建立同样的复现路径:固定执行顺序,记录目标之前运行过哪些测试,再检查共享缓存、环境变量和模块级 client 是否恢复干净。原文脚本依赖 pytest,不能直接跨框架使用。
- 自动缩减前,先让判定逻辑确认目标断言确实失败,并保留失败输出。否则缩得越小,也可能只是越准确地定位到了另一个无关错误。
- 在审查评论里附上最小复现组合、执行顺序和相关 fixture。确需临时隔离时,同时写明工单与解除条件,让下一位维护者知道何时恢复测试。