AI 写完代码再写测试,如何避免自证正确
文章提出将补丁作者与测试判定逻辑作者分离,降低同一上下文导致的自我确认。再把新增测试放到修复前的代码上运行,通过其是否仍然通过,识别无法捕获目标缺陷的测试。
AI 深度解读
AI 写完补丁,再顺手补测试,测试全绿,能证明问题修好了吗?作者指出,这两份产物可能只是共享了同一个错误理解:代码按这个理解实现,测试再按这个理解验收,需求有没有被满足,反而没人检查。
作者提出把补丁和测试分给两个独立会话,再让新测试分别运行在修复前后的代码上。最值得借走的不是多开一个 Agent,而是一个追问:这条测试,能不能识别尚未修复的问题?
测试全绿,为什么还不能证明修复有效
原文里的 oracle,可以理解为「判断行为对不对的测试断言」。它不仅要能通过,还得有机会被错误实现推翻。
作者列出三种容易混过绿色流水线的情况。
第一种是同一上下文里的自我确认。Agent 知道自己改了哪一行,于是专门断言这一行现在返回的新结果。这能当冒烟检查,却未必对应需求。
第二种是测试数据吸收了变化。补丁同时修改实现和 fixture,断言拿新代码对照新预期,自然容易一致,但这个预期是否合理仍然没有得到验证。
第三种是缺少需求的空断言。测试作者只看见 diff,没有拿到 issue,最后写出 result is not None。返回了东西,并不代表返回得正确。
对于声称验证本次修复的新增测试,修复前失败、修复后通过,比单独一次通过更有说服力。
这里有个范围限制:修复前也通过的测试,不一定没有价值。它可能在保护已有行为,只是不能拿来证明这次补丁解决了问题。
两个会话之间,应该隔开什么
作者的方案分三步,关键是控制测试作者拿到的信息。
先生成一份封闭规格,也就是 sealed spec:包含 issue 文本,以及基线提交中非测试模块的公开接口,例如函数签名、文档字符串和导出名称;隐藏 diff 和新实现。测试作者应该从需求推导预期行为,而不是照着实现倒推断言。
接着在一个全新会话里生成 pytest 测试,不共享补丁会话的历史。要求每条测试只验证一种行为,不访问网络、不 sleep、不导入以下划线开头的私有 helper,只检查外部可观察的结果。
最后,把同一份测试放到两棵代码树上执行。一棵是修复前的 base,一棵是应用补丁后的代码。作者通过 git worktree add --detach 创建基线工作目录,完成后用 git worktree remove --force 清理。
注意,是同一份测试跑两遍。两边各生成一套测试,就失去了比较依据。
原文使用的执行形式是 python -m pytest,参数包括 -v、--tb=no 和 -p no:cacheprovider。脚本通过输出中的 PASSED、FAILED、ERROR、SKIPPED 解析结果,再进行分类。
这种隔离不要求换模型。重点是需求来源和上下文独立,而不是两个 Agent 用了不同名字。
真正该追踪的是多少测试「修复前就能过」
作者把主要指标叫作 base-pass rate:
base_pass / (base_pass + fail_to_pass)
其中,base_pass 表示测试在修复前后都通过;fail_to_pass 表示修复前失败、修复后通过。这个比例回答的是:新增测试里,有多少条其实不需要这次修复就能满足?
原文给出下面的报告样例,用来展示账本记录什么:
{
"oracle_id": "3f9a1c0d2b7e4a51",
"counts": {"fail_to_pass": 4, "base_pass": 2, "fails_always": 1},
"blocking_findings": {"test_parser_returns_list": "base_pass",
"test_retry_counts_attempts": "base_pass"}
}
要看的是 counts 这一行:4 条测试从失败变成通过,2 条原本就通过,另有 1 条始终失败。按公式,样例的 base-pass rate 是 2 / (2 + 4),约为 33.3%。原文明确声明,这只是报告格式,不是任何仓库的实测结果。
作者还要求记录 fails_always 和 unknown。前者表示修复后依然失败,后者用于标记无法可靠分类的情况,例如测试收集错误。它们不进入主指标分母,因此不能只展示一个漂亮的比例,还要一起查看这些异常数量。
测试生成调用通过 ORACLE_ENDPOINT 指向兼容 Chat Completions 的端点,使用 ORACLE_MODEL 和 ORACLE_KEY,请求设置 temperature: 0,从 choices[0].message.content 读取生成文件。模型调用超时为 240 秒,子进程默认超时为 1800 秒。这些是脚本参数,不是效果保证。
原文提供的入口命令是 python oracle_split.py . origin/main issue.md tests/oracle_ledger.jsonl。不过,贴出的 Python 代码在 live.write_text(tests 处中断,缺少后续分类、账本写入和命令行入口,不能直接照贴运行。
作者还提出让新增测试先进入观察期,第一次出现时不能决定合并,并建议先影子运行一周。后文提到前三次出现都不阻断,但原文没有给出完整状态机,也没有说明三次如何计数。
我的判断
我认同它把「测试通过」拆成了两个问题:补丁是否满足断言,以及断言是否有能力识别原来的错误。对 AI 编程尤其如此,同一会话连续生成实现和测试,很容易让未经确认的假设同时进入两边。
但双会话只减少一种关联错误。issue 如果含糊,两个会话照样可能误解需求;公开接口如果只有一个巨大的 run(config),测试作者也很难凭空写出可靠预期。和人工先写验收测试、再实现补丁相比,这套协议主要是在补救测试与实现同时生成的流程。
脚本对「封闭规格」的落实也弱于描述。它用 git show 读取完整源码,再通过 ast.parse 提取信息,因此注释里的「不读取函数体」并不准确。准确地说,是发给模型的规格不包含函数体。另外,ast.walk 会遍历嵌套定义,代码没有过滤私有名称,也没有完整保留参数默认值和类型注解,提取结果并不等于严谨的公开 API 清单。
不联网、不 sleep、不导入私有 helper 等要求,目前主要写在提示词里。一次 pytest 子进程执行也不能自动保证这些约束得到遵守。
还有一个需要打问号的地方:作者建议,当测试开始在 base 上通过时就将其退役。这里必须区分固定的修复前提交和持续前进的主分支。如果主分支已经合入修复,测试通过完全正常,不能据此删除回归保护。原文没有把这个生命周期讲清楚。
作者没有提供对照实验,也没有给出误合并率或维护成本的实测数据。文章披露了 MonkeyCode 产品推广背景,关于免费模型访问和服务器运行的建议,应与测试协议本身分开评估。
能拿来做什么
- 给现有 AI 修复流程增加一次基线回放,先记录
base_pass、fail_to_pass、fails_always和unknown,按作者建议影子运行一周,再判断是否适合进入合并检查。 - 为测试会话单独整理 issue、明确的输入输出示例和公开接口,不传补丁 diff,也不复用实现会话的聊天历史;需求说不清时,先补需求。
- 在 Node 或前端工具库里迁移这套比较方法时,让同一份行为测试运行于固定的修复前提交和修复后提交,并确保失败来自目标行为,而非依赖安装、模块导入或环境差异。
- 单独审查 fixture 和快照更新。两棵代码树回放不能替代预期结果审核,也不要仅凭测试在最新主分支通过,就自动移除回归测试。