当Agent能同时修改测试与代码时,CI通过只是颜色而非质量信号;提出oracle字节对比、属性种子、flake freeze三段验证。
一个通过的 job 不是合并决策的依据,它只是一种颜色。当一个 Agent 能够在与生产代码同一个树中重写测试时,这种颜色可以通过删除本该失败的断言来购买。
根据 Agent 无法控制的三个事实来给补丁打分:oracle 字节与合并基准的对比、满足最小成对距离的种子属性命中,以及一个标明了原因和过期时间的 flake freeze。如果这三个中有任何一个缺失,就不要让补丁进入 default 分支。
弱测试是人类的问题。Agent 补丁引入了第二个问题:检查和变更是一起提交的。
当人类写 assert True 时,套套逻辑很明显。但当 Agent 在同一个 commit 中放宽了一个数值边界、删除了一行 fixture 数据,或者将一个 property 标记为 xfail(同时声称在修复一个 bug)时,这就更隐蔽了。套件保持绿色。行为没有移动。合并日志看起来仍然是干净的。
不要从覆盖率开始。覆盖率在 Agent 添加它同时也在测试的文件时会上升。要从 oracle 所有权开始。然后衡量独立检查是否真的被执行了。
将独立检查保持在 oracles/ 目录下。那不是一个风格偏好,而是一个合并不变量。
如果 oracles/ 与合并基准不同,门禁应当拒绝该补丁。协议变更属于人类撰写的 PR,先更新 oracle。Agent 补丁消费这些 oracle,但不去编辑它们。
将属性检查、golden fixture、seed 语料库和 freeze 元数据移到 oracles/ 下。
添加一个 CI 步骤对该路径与合并基准进行 diff,并失败关闭。
当 diff 非空时阻止合并,即使 pytest 是绿色的。
在没有 Agent 参与的情况下审查 oracle PR。
git fetch origin main
BASE=$(git merge-base origin/main HEAD)
git diff --exit-code "$BASE" HEAD -- oracles/
如果该命令退出非零值,其余的评分表就是参考性的。它不是一个合并信号。
将 oracle runner 锁定在 Agent 也无法编辑的 lockfile 上。重写的 oracles/requirements.txt 与重写的断言是同一类失败。
python -m pip install -r oracles/requirements.txt
python oracles/gate.py --parent /tmp/parent --patch /tmp/patch
将生产代码旁边的应用测试视为表征测试即可。它们可以记录意图,但不能授予合并信用。
一个在父分支上从不失败、在补丁上也从不失败的属性不是证据。一个在 Agent 记忆的某个 seed 上失败的属性也不是证据。
将种子语料库锁定在 oracles/seeds.json 中。在父分支和补丁上重放相同的属性。只对以下种子给补丁计分:在父分支上失败、在补丁上通过、且彼此之间至少相隔 D bits。距离是"Agent 没有对单个 blob 过拟合"的一个廉价代理。它不是语义多样性。把它当作一个过滤器,而不是证明。
提议的语料库结构(示例数据,非实测套件):
{
"property": "parse_roundtrip",
"seeds": [
{"id": "s1", "payload": {"raw": "a,b", "n": 2}},
{"id": "s2", "payload": {"raw": "", "n": 0}},
{"id": "s3", "payload": {"raw": "a,,c", "n": 3}}
]
}
重放 helper 必须是 payload 加上 tree 的纯函数。无 clock、无网络、无 oracles/ 和被测应用包之外隐藏文件。
# oracles/replay.py — proposed runner, not production telemetry
from __future__ import annotations
import json
import sys
from typing import Any, Callable
def parse_roundtrip(payload: dict[str, Any]) -> None:
raw = payload["raw"]
n = payload["n"]
parts = raw.split(",") if raw != "" else []
if len(parts) != n:
raise AssertionError(f"expected {n} parts, got {parts!r}")
if ",".join(parts) != raw:
raise AssertionError("join did not recover raw")
PROPS: dict[str, Callable[[dict[str, Any]], None]] = {
"parse_roundtrip": parse_roundtrip,
}
def main() -> int:
prop = sys.argv[1]
payload = json.load(sys.stdin)
try:
PROPS[prop](payload)
except Exception:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
提议的门禁(示例评分器,非测量生产结果):
# oracles/gate.py — example scorer, not measured production results
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import sys
from datetime import date, datetime
from pathlib import Path
import yaml
def hamming(a: bytes, b: bytes) -> int:
n = max(len(a), len(b))
a, b = a.ljust(n, b"\0"), b.ljust(n, b"\0")
return sum(bin(x ^ y).count("1") for x, y in zip(a, b))
def seed_bytes(payload: dict) -> bytes:
blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(blob).digest()
def run_property(tree: Path, prop: str, payload: dict) -> int:
proc = subprocess.run(
[sys.executable, str(tree / "oracles" / "replay.py"), prop],
input=json.dumps(payload),
text=True,
cwd=tree,
capture_output=True,
)
return proc.returncode
def freeze_debt(root: Path, today: date) -> list[str]:
doc = yaml.safe_load((root / "oracles" / "flake_freeze.yml").read_text())
bad: list[str] = []
for row in doc.get("entries", []):
if not row.get("cause") or not row.get("ticket"):
bad.append(f"{row.get('id')}: missing cause or ticket")
continue
exp = datetime.strptime(row["expires"], "%Y-%m-%d").date()
if exp < today:
bad.append(f"{row['id']}: expired {row['expires']}")
return bad
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--parent", type=Path, required=True)
p.add_argument("--patch", type=Path, required=True)
p.add_argument("--today", default=date.today().isoformat())
args = p.parse_args()
policy = yaml.safe_load((args.patch / "oracles" / "policy.yml").read_text())
corpus = json.loads((args.patch / "oracles" / "seeds.json").read_text())
hits = []
for seed in corpus["seeds"]:
parent_rc = run_property(args.parent, corpus["property"], seed["payload"])
patch_rc = run_property(args.patch, corpus["property"], seed["payload"])
if parent_rc != 0 and patch_rc == 0:
hits.append(seed)
kept = []
for seed in sorted(hits, key=lambda s: s["id"]):
sb = seed_bytes(seed["payload"])
if all(
hamming(sb, seed_bytes(k["payload"])) >= policy["min_seed_distance"]
for k in kept
):
kept.append(seed)
debt = freeze_debt(args.patch, date.fromisoformat(args.today))
card = {
"oracle_hits": len(kept),
"raw_fail_to_pass": len(hits),
"min_seed_distance_policy": policy["min_seed_distance"],
"hit_ids": [s["id"] for s in kept],
"freeze_debt": debt,
"merge": len(kept) >= policy["min_oracle_hits"] and not debt,
}
print(json.dumps(card, indent=2))
return 0 if card["merge"] else 2
if __name__ == "__main__":
raise SystemExit(main())
输出的对象才是合并输入。15 个通过不是。如果 oracle_hits 为 0,补丁没有移动一个独立检查。绿色的 pytest job 不能覆盖那个零。
Worktree 只用于让父分支和补丁重放变得平淡。它们不是第二个产品。在两个树中从同一个 lockfile 安装,这样 runner 不会漂移。
git worktree add /tmp/parent "$BASE"
git worktree add /tmp/patch HEAD
python oracles/gate.py --parent /tmp/parent --patch /tmp/patch
Flakes 破坏 oracle 信用。团队随后会 freeze 测试直到套件安静下来。安静不是稳定。
允许有 freeze 文件。不允许匿名 freeze。每行需要有一个测试 id、一个原因类、一个 ticket 和一个过期时间。过期后,这个 freeze 就是合并债务。下一个 Agent 补丁在该行仍然存在时不会获得绿色通道。
值得编码的原因类:
order — 集合顺序依赖
time — clock、sleep 或 wall timeout
rng — 未播种的随机性
shared — 泄露的进程状态
如果原因未知,不要 freeze。将测试从阻塞通道中隔离出来,并记录 oracle 集合缩小了。缩小 oracle 是可见的。安静的 skip 不是。
# oracles/flake_freeze.yml — example policy, not a live ticket dump
version: 1
entries:
- id: tests/test_cache.py::test_ttl_under_load
cause: time
ticket: FLAKE-184
expires: "2026-09-27"
note: "wall-clock assertion; rewrite on a logical clock in a human PR"
# oracles/policy.yml
min_oracle_hits: 3
min_seed_distance: 8
oracle_paths:
- oracles/
过期时间是一个日期,不是一句注释。门禁应该解析它。没有 cause 或 ticket 的 freeze 也是债务。YAML 是 oracle 树的一部分,所以 Agent 不能在不触发 Layer 1 的情况下扩展它。
在 Agent 打开补丁后运行此流程。不要用它来替代 review。
确认 oracles/ 与合并基准字节相同,包括 freeze、seeds 和重放 lockfile。
创建两个 worktree。不要复用 Agent 的本地 virtualenv 或其 editable install。
在两个树上重放 oracles/seeds.json。只保留 fail-to-pass 种子。
丢弃距离保留命中在 D 之内的任何命中。保留仍然满足 H 的集合。
加载 oracles/flake_freeze.yml。如果任何 freeze 已过期或缺少 cause,则拒绝补丁。
输出评分表 JSON。只有当 hits ≥ H、距离过滤器仍然保留这些命中、freeze debt 为空且 oracle diff 为空时,才能合并。
H 和 D 是本地策略。在 oracles/policy.yml 中发布它们。不要埋在 Agent 可以重写的 chat prompt 中。
将评分表读为三个数字和一个列表:oracle_hits、raw_fail_to_pass、min_seed_distance_policy 和 freeze_debt。如果 raw_fail_to_pass 很大而 oracle_hits 很小,说明语料库是共线的。在人类 PR 中拓宽 seeds。不要在 Agent 补丁中降低 D。
Seed 距离是句法层面的。两个 JSON blob 在 Hamming 空间中可以相隔很远,但仍然只执行一个分支。大型 fixture 仍然可能是共线的。门禁不能替代能够阅读补丁的 reviewer。
Freeze 分类法是不完整的。带有动画计时的浏览器测试不会干净地适合 cause: time。YAML 枚举不是 flake debugger。
父分支/补丁重放假设 oracle 在父分支上是确定性的。如果父分支本身已经 flake,你就没有 oracle。你有一枚硬币。在人类 PR 用 cause freeze 它或修复它之前,停止对该检查评分 Agent 补丁。
门禁也没有说明 Agent 在应用代码旁边添加的测试。这些测试作为表征仍然可能有用。它们不是 oracle 命中。在别处计数,或者不计数。
如果你的唯一套件是一个远程浏览器网格,具有不可约的 flake 率和无可播种输入,不要安装这个门禁。你会 freeze 一切,然后在空的 oracle 上合并。
不要用它作为跳过阅读生成代码的理由。独立 oracle 捕获 Agent 没有隐藏的行为回归。它们不能捕获一个 leak、一个 license 文件或一个没有任何 property 提到的被删除的 auth 检查。
不要"就这一次"让 Agent 指向 oracles/。这就是不变量死亡的方式。如果协议改变了,先 landing oracle 更新,有人类 reviewer,然后让 Agent 针对新的检查实现。
如果你的 seeds 已经由领域专家枚举且少到可以手动检查,跳过 Hamming 过滤器。距离是大型语料库的crowd-control 工具。对于一个十二行的表,它是开销。
相对于评分,生成候选补丁是廉价的。具有免费模型访问和免费服务器选项的 coding agent 可以对应用代码提出 diff,而 CI 拥有 oracle 路径。
披露:本文是 MonkeyCode 产品推广的一部分。
如果你已经在免费服务器上生成这些 diff,请将两个 worktree 指向那个 checkout,并在 pytest 颜色之前读取评分表。有趣的输出仍然是 oracle_hits 和 freeze_debt,不是提出 diff 的模型。