编码 Agent 常修改测试用例迁就自身 bug 导致 CI 变绿;作者构建了检测工具,在测试被修改时返回 INCONCLUSIVE 而非假阳性通过。
Coding Agent 写出的代码看起来很流畅。最昂贵的失败不是那些明显坏掉的代码,而是那些看起来已经验证过的补丁:
测试"通过"了,因为它根本没跑起来(import 报错在修复前后都是非零退出,naive 的前后对比会把这种情况读成"没有变化");
覆盖率数字很好看,因为那个数字是默认值,从来没人真正测量过;
Agent 引入了一个 bug,然后把断言改成了认同这个 bug。补丁自己的测试通过了。CI 是绿的。
我写了 AdversaryGate 来专门捕获这类问题。它不会去问模型"这段代码看起来对不对",而是直接跑你的测试,然后给出三种答案之一:
第三种答案才是关键。一份没人真正产出过的测量结果,不可能用来"清零"。
一个 Agent 把 add 函数改成了返回 a + b + 1,然后把测试改成期望 6 而不是 5。跑补丁自己的测试:通过了。
AdversaryGate 注意到补丁修改了一个基线版本中已有的测试。它拷贝补丁的代码树,把测试文件(连同 helpers)换回基线版本中的副本,然后用新代码去跑这些测试:
{
"decision": "block",
"claims": [{
"test_id": "test_ops",
"classification": "regression",
"oracle": "baseline"
}],
"reason": "judged by the baseline's version of test_calc.py and its test files (the patch rewrote test_calc.py): test passes on baseline and fails on patch"
}
实际跑的是 Agent 动手之前就存在的那个测试。修改它没有意义。
两边跑同样的测试。可以通过名字指定,也可以让 coverage.py 的 per-test 上下文来挑选实际执行了被改行的那几个测试。只有真正的测试失败才算证据;import 报错、测试缺失、超时、以及被资源限制杀掉的运行都是"未验证",不是"失败"。
从 artifact 计算 diff 覆盖率。通过统一 diff 加上 coverage.py 的 JSON 报告计算得出,测试文件不计入。两份输入都以 SHA-256 形式记录,任何人都可以重新算出这个数字。
对补丁改过的行做变异测试。每条被改的行只产生一个变异体,且每行只能变异一次。覆盖率底线读取的是 80% Wilson 区间下界,不是原始比例:
一次变异体被杀只是得到一个比例,不是证据。
一个通过的 claim 也会说明是怎么通过的:如果是补丁前后测试从失败变成通过(修复被证明),记为 fixed;如果两次都通过(没有回归),记为 no_regression。
我在自己的 gate 里发现了一个 bug:补丁可以携带一个 .pytest.ini,其中 addopts = -p plugin,再附加一个 plugin 来在源码包含 bug 确切字节的时候才报告"passed"。变异会改变那些字节,所以每个变异体都真实地被杀死了。结果:MERGE,exit 0,strength 1.0,而此时 add(2, 3) == 6。
现在代码树中所有测试相关的配置文件(pytest 9 会读取的五种配置文件名、conftest.py、sitecustomize.py、*.pth...)都必须与基线逐字节一致,逐树比较,而不是从 diff 报告的路径去读取。
这个发现以及其他 31 个 bug 都记录在公开台账里,每条都说明了如何复现以及是否仍在开放状态。错误 MERGE 是这个工具存在的意义,所以它自身的 bug 被当作安全问题处理。
从 2.8 开始它带了一个 MCP server,所以 Agent 在说"完成了"之前可以调用这个 gate:
pip install "adversary-gate[mcp]"
adversary-gate-mcp
设计原则:Agent 说要评判什么;谁来跑 Agent 谁就说要有多严格、基于什么。工具调用只接收 repository、claims 和测试路径。所有决定答案的东西都来自 server 的环境:
ADVERSARY_GATE_POLICY:底线、sandbox。一个能传 --coverage-floor 0 的 Agent 是在给自己打分。
ADVERSARY_GATE_BASE_REF:基线是 oracle。Agent 可以提交一个改写过的测试并把那个 commit 命名为 base。
ADVERSARY_GATE_PYTHON:解释器的 site-packages 是 harness 的一部分,安装在那里的 plugin 跑在 pytest 内部。
Hermes Agent 有一个现成的 skill(adversary-gate-mcp --print-hermes-skill),并且兼容任何 MCP client。
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt pytest coverage
- uses: Sanflow10/adversary-gate@v2
with:
base-sha: ${{ github.event.pull_request.base.sha }}
有了 base-sha,Action 自己构建基线、diff 和 per-test 覆盖率报告。
测量 Python 以外的语言。C++ 或 SQL 的改动会得到 INCONCLUSIVE,永远不会 false MERGE。
捕获知道自己在被测试的代码。通过 sys.modules 中的 pytest 来检测测试状态,或者从进程内部篡改 pytest 的代码,能骗过任何基于测试的 gate。这一点有文档记录,不是隐藏的。
自己下决定。MERGE 意味着证据已经清零了。这只是给人类一个去看一眼的许可,不是让你直接发货的指令。
Site: https://sanflow10.github.io/adversary-gate/ Repo: https://github.com/Sanflow10/adversary-gate pip install adversary-gate · MIT
欢迎来告诉我它在哪些 case 上给错了答案。