文章主张将测试输入锁、属性判定器和不稳定测试豁免记录设为 Agent 不可修改的证据,避免补丁通过削弱检查掩盖缺陷。证据变更应由人工在不含产品修复的后续提交中完成,远端通过结果只能作为已有证据的补充。
只有证据路径保持不变,Agent 补丁才具备可审查性。fixture 锁、property oracle 和不稳定测试的冻结记录,是审查者判断失败是否真实的依据。如果同一份 diff 可以修改这些文件,生成补丁的 Agent 就可能直接删掉失败,而不是修复产品。
这就是准入门槛。产品代码可以改,证据不能改;唯一的例外,是由人类在后续提交中单独修改证据,而且该提交不能包含产品修复。
生成补丁是成本低的那一步。真正仍然需要明确流程的,是生成补丁的 Agent 可以写入哪些路径,以及写入之后,哪些结果仍然有效。
本文关注的失败模式,是通过修改证据让测试套件全部通过:断言被改了,fixture 中的某个字节被改了,或者冻结记录中新增了一个名字。产品 bug 却还留在原处。
要评估三个对象,不要只看一个通过标记。
fixture 锁,是测试可能读取的输入内容的哈希。
property oracle,是必须仍能拒绝已知错误输入的检查。
冻结台账,是唯一可以将某个不稳定测试排除在判定之外的记录,而且必须附有一份并非由当前补丁写入的引用证据。
远端通过结果不是第四个对象。它只是附在已有存储证据上的一条引用。第二个运行环境可以确认失败类别,但不能消除本地 fixture 不匹配的问题,也不能在 Agent diff 中新建冻结记录。
先看文件名。声称“测试未改动”的摘要不能算证据。只要改了 tests/fixtures/order.json 中的一个字段,就足以让这次运行失去效力。
git fetch origin main
git diff --name-status origin/main...HEAD > /tmp/patch_status.txt
find tests/fixtures -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/fixture.lock
sha256sum -c /tmp/fixture.lock
先读状态列,再读补丁正文。这个示例解析的是 A、M 和 D 这三类记录。重命名记录通常是 R100,包含两个路径,必须手动分类,或交给扩展后的解析器处理。
将每个解析出的路径归入一个类别。这些前缀是仓库约定,不是通用标准。
只要有任何路径落入 fixture、oracle 或 freeze_ledger,就停止。不要再花一轮模型调用来解释这次修改。证据路径已经被改动,因此这个补丁超出了允许范围。
other 表示暂缓,不是通过。一个 workflow 文件即使没有改动产品代码,也可能通过延长超时时间来掩盖测试的不稳定性。在仓库公布允许列表之前,把这些 diff 留给人类处理。
删除 fixture 与重写 fixture 属于同一类修改。证据前缀下的路径即使状态是 D,也仍然要拒绝。
property 应当描述补丁声称要保留的行为,而且应当已经存在于基准版本中。在同一份 diff 中新增的 oracle,不能算作 property,只能算作未经审查的主张。
即使检查通过,也要存储一个样本。没有保留输入的通过结果,不如审查者能够重新计算的结果可靠。
def shipping_total(weight_g: int, zone: int) -> int:
base = 200 + (weight_g // 100) * 40
return base + {1: 0, 2: 150, 3: 400}[zone]
def zone_monotonic(weight_g: int) -> tuple[bool, dict]:
totals = [shipping_total(weight_g, z) for z in (1, 2, 3)]
sample = {"weight_g": weight_g, "totals": totals}
return totals[0] <= totals[1] <= totals[2], sample
这两个函数只是未经执行的示例,既不是生产环境的运费价目表,也不是 benchmark。重量为 250 时,总费用是 [280, 430, 680]:基础费用为 200 + (250 // 100) * 40 = 280,然后分别加上各区域的附加费用 0、150 和 400。
把这个样本写入 tests/properties/samples/zone_monotonic.json,并将该文件的哈希纳入 fixture 锁。Agent 补丁可以读取这个样本,但不能重写它。如果后续某个函数返回了不同的三个值,保留的样本就已经过时,锁校验步骤应当失败。这项比较不需要模型参与。
只遍历一个有界的整数范围,范围大小以你愿意存储的数据量为限。如果出现失败,保留第一个失败样本。失败样本属于证据路径,因此 Agent diff 仍然不能为了让测试通过而修改它。
每个补丁都使用相同的判定行。真正有用的是拒绝条件集合,而不是顺利通过的情况。
有两条解读必须固定下来,因为它们很容易被“优化”掉:新增测试不能继承旧的冻结记录;远端通过不能消除本地 fixture 失败或本地 property 失败。这张表把这两条解读与路径规则放在一起,让它们始终属于同一个判定过程。
暂缓不是一种宽松的通过。处于暂缓状态的补丁一律不能合并。补丁必须等待,直到证据恢复干净,或者由人类另开一个只修改证据路径的提交。
下面的模块是一份提案。本文没有在真实仓库中执行它。接入 CI 之前,请先审查它输出的判定字符串。重命名记录被刻意保留为未解决的情况:脚本会返回暂缓,而不是猜测。
import argparse
import sys
from pathlib import Path
PREFIX = {
"fixture": ("tests/fixtures/",),
"oracle": ("tests/properties/", "tests/oracles/"),
}
def classify(path: str, status: str) -> str:
if path == "flake_freeze.json" or path.startswith("tests/freezes/"):
return "freeze_ledger"
for label, prefixes in PREFIX.items():
if any(path.startswith(p) for p in prefixes):
return label
if status == "A" and path.startswith("tests/"):
return "added_test"
if path.startswith("src/"):
return "product"
return "other"
def gate(rows, fixture_match: bool, local_property: str, remote: str) -> str:
if not rows:
return "hold: empty diff"
classes = {classify(path, status) for status, path in rows}
if classes & {"fixture", "oracle", "freeze_ledger"}:
return "reject: evidence path edited"
if "other" in classes:
return "hold: unclassified path"
if not fixture_match:
return "reject: local fixture lock failed"
if local_property == "fail" and remote == "pass":
return "reject: remote pass cannot clear a local property fail"
if "added_test" in classes:
return "hold: added tests need a separate review"
if local_property == "fail" and remote == "same_class":
return "hold: cite the pre-existing test later, do not freeze it here"
if local_property == "pass" and classes <= {"product"}:
return "review: product diff only"
return "hold: incomplete evidence"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--status", required=True)
parser.add_argument("--fixture-match", choices=("yes", "no"), required=True)
parser.add_argument("--local-property", choices=("pass", "fail"), required=True)
parser.add_argument("--remote", choices=("not_run", "pass", "same_class"), required=True)
args = parser.parse_args()
rows = []
for line in Path(args.status).read_text().splitlines():
if not line.strip():
continue
parts = line.split("\t")
if len(parts) != 2 or parts[0] not in {"A", "M", "D"}:
print("hold: rename or unparsed status; classify both paths")
return 2
rows.append((parts[0], parts[1]))
decision = gate(
rows,
fixture_match=args.fixture_match == "yes",
local_property=args.local_property,
remote=args.remote,
)
print(decision)
return 0 if decision.startswith("review:") else 1
if __name__ == "__main__":
sys.exit(main())
首先检查路径类别。哈希匹配不能挽救被改动的 oracle,远端通过也不能挽救本地 property 失败。模型输出为空或发生传输错误时,仍然保持 remote=not_run,不要强行将其转换为 pass。
python evidence_gate.py \
--status /tmp/patch_status.txt \
--fixture-match yes \
--local-property pass \
--remote not_run
只接受字符串 review: product diff only。任何其他字符串都必须阻止合并。把判定结果与 base SHA、head SHA 一起记录下来。后续引用证据应当指向这份日志,而不是聊天记录。
下面这样一份紧凑的记录结构,就足以供审查使用。其中使用的是占位符,并不声称实际执行过:
{
"base": "<base sha>",
"head": "<head sha>",
"classes": ["product"],
"fixture_lock": "<sha256 of tests/fixtures tree>",
"sample": "tests/properties/samples/zone_monotonic.json",
"local_property": "pass",
"remote": "not_run",
"freeze_in_diff": false,
"decision": "review: product diff only"
}
披露:本文是 MonkeyCode 产品推广活动的一部分。
这里提到 MonkeyCode,是因为运营方将它描述为一个开源项目,提供免费模型访问和免费服务器选项。这些关于可用性的说法来自运营方。本文不补充 token 配额、模型名称、硬件规格、使用时长或 benchmark。这些条款会变化。开始任务时,请阅读项目的最新文档,不要从旧文章中照搬数字。
使用免费模型访问时,只让模型起草产品 diff。在任务中明确可写范围:src/ 可以修改;tests/fixtures/、tests/properties/、tests/oracles/ 和 tests/freezes/ 不可以修改。草稿生成后,执行步骤 1。如果草稿碰了证据路径,就丢弃这份草稿。
不要让模型在同一个补丁中恢复这些文件。恢复操作应当用 git checkout 取回基准版本,然后重新分类。如果补丁中记录了修改,那么第二份声称“把测试改回去了”的草稿,仍然属于证据修改。
只有在本地 property 已经失败、并且保存了样本之后,才把免费服务器用作第二个运行环境。重放这个样本。如果失败类别一致,就可以为已有测试提供一条引用证据。但这仍然不意味着可以在 Agent diff 中添加冻结记录。如果服务器通过,而本地锁校验失败,判定表已经明确要求拒绝。
前缀规则发现不了运行时篡改。产品代码仍然可能在导入后修改 oracle。如果辅助代码位于 src/ 下,就要审查导入图,或者从产品包无法写入的路径加载 oracle。
哈希锁覆盖不了运行时获取的输入。应当计算已记录响应正文的哈希,并将这个哈希存储在证据路径上。否则,锁校验的可能只是测试根本没有读取的文件。
两个运行环境可能共享同一个 bug,并输出相同的失败类别。一致的引用证据不等于测试的不稳定发生率。如果发布决策关注的是测试失败的频率,就不要使用这套准入检查。它回答的是:这份 diff 是否修改了那些可以掩盖失败的文件。
状态解析器不理解复制或重命名记录。在解析器支持这种情况之前,任何重命名到证据前缀下的操作都应当拒绝。对于包含 R 或 C 记录的 diff,这个示例脚本即使以成功状态退出,也没有意义。
由人类修复 oracle,要走另一条流程。如果 property 本身有误,应当由人类在一个不包含产品修改的提交中修正它。把 oracle 修复与产品修复混在一起,正是这套工作流要拒绝的情况。
如果代码树没有 fixture、pull request 本身就是为了修改 oracle,或者所有测试文件都是新增的,就不要采用这套方法。新增测试需要单独审查,不能借用旧的冻结记录,把它们包装成可信的测试覆盖。
让证据路径对 Agent 补丁保持只读。对状态和路径分类,锁定 fixture 字节,保留 property 样本,并拒绝 diff 内的冻结记录。免费模型可以起草产品修改;免费服务器可以在本地失败后,补充第二条引用证据。
两者都不能修改那些决定引用证据是否有效的文件。如果你已经遵守这种职责划分,MonkeyCode 的免费模型访问和免费服务器选项,就足以让你在临时分支上演练这套流程。先检查当前条款,再确保草稿不涉及证据路径。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。