用三臂证明和回归检查验收 AI 补丁

受 Trail of Bits patching-bugs / regression-hunt 插件启发,用一个金额解析函数演示:两个候选补丁都能修好报告的 bug 并通过三臂证明(原代码失败、补丁通过、撤回后失败),但只有加上既有行为的回归检查,才能拦下顺手破坏负数解析的那个补丁。

已验证 Python 3.12.15 耗时 不到 0.1s 运行于 2026-10-21 22:38 UTC
结论

只看 bug 是否修好会接受补丁 A;加上回归检查后只接受补丁 B

检查 3/3 通过
  • 两个补丁都通过三臂证明
  • 补丁 A 被回归检查拦下(负数解析被破坏)
  • 补丁 B 没有回归

以上内容从实际运行输出中提取,完整输出见下方。

python main.py
== 补丁 A ==
  原代码: 复现检查失败(期望失败)✓
  打补丁: 复现检查通过(期望通过)✓
  撤回补丁: 复现检查失败(期望失败)✓
  三臂证明:成立
  回归:1 个,原本支持、现在失败的输入:['-3']
  结论:拒绝

== 补丁 B ==
  原代码: 复现检查失败(期望失败)✓
  打补丁: 复现检查通过(期望通过)✓
  撤回补丁: 复现检查失败(期望失败)✓
  三臂证明:成立
  回归:0 个
  结论:可接受

[PASS] 两个补丁都通过三臂证明
[PASS] 补丁 A 被回归检查拦下(负数解析被破坏)
[PASS] 补丁 B 没有回归

RESULT: 只看 bug 是否修好会接受补丁 A;加上回归检查后只接受补丁 B

目的

Trail of Bits 在 Your AI agent fixed the bug, but what else did it change? 中发布了 patching-bugs 和 regression-hunt 两个插件,核心是两条验收标准:

  • 三臂证明:在原代码上复现检查应失败 → 打补丁后应通过 → 撤回补丁后应再次失败。它证明「是这个补丁修好了 bug」,而不是检查本身写错或碰巧通过
  • 回归检查:补丁不应破坏原本正常、受支持的行为

本实验用一个极小的例子演示:为什么只有三臂证明还不够。

注意:这里的函数、补丁和测试都是本实验自行编写的简化示例,不是 Trail of Bits 插件的实现。

步骤

  1. parse_amount() 原版不支持千位分隔符,bug 报告是 "1,234.50" 解析失败
  2. 两个候选补丁: - 补丁 A:用正则校验格式,修好了千位分隔符,但正则漏了负号 - 补丁 B:同样的思路,保留可选负号
  3. 对每个补丁执行三臂证明,再用 5 个既有用例(含 "-3")做回归检查:原版通过、补丁后失败的即为回归
  4. 断言:两个补丁都通过三臂证明;A 有且只有 -3 这一个回归;B 没有回归

运行:

python main.py

程序最后输出 RESULT: ...,任一检查失败时以非零状态退出。

预期结果

  • 补丁 A:三臂证明成立,回归 1 个(-3),结论「拒绝」
  • 补丁 B:三臂证明成立,回归 0 个,结论「可接受」

局限与可以继续尝试

  • 回归检查只覆盖已有用例,用例没写到的行为照样可能被破坏;Trail of Bits 的做法是追踪改动影响到的真实接口,并在运行中的系统上复现
  • 可以把 ORIGINAL / PATCH_* 换成你自己项目里 Agent 提交的补丁,把 EXISTING 换成现有测试,用同样的方式做验收

内网复现

  • 无需任何第三方依赖,只用 Python 3.8+ 标准库,无需联网,可直接在内网运行
  • 把 main.py 拷贝到内网机器执行 python main.py 即可
  • 若要在内网安装 Trail of Bits 插件本身,需要能访问 GitHub 上的 trailofbits/skills 仓库,或先在外网克隆后同步到内网 Git 服务,再按其 README 用本地目录添加插件市场

代码

main.py118 行
"""用「三臂证明 + 回归检查」验收一个补丁。

受 Trail of Bits patching-bugs / regression-hunt 插件启发:
  三臂证明:原代码上复现检查应失败 → 打补丁后应通过 → 撤回补丁后应再次失败
  回归检查:原来通过的既有行为,打补丁后仍应通过

样例:parse_amount() 解析金额字符串,原版不支持千位分隔符(bug 报告:'1,234.50' 报错)。
两个候选补丁都能修好 bug,但其中一个顺手破坏了负数解析。
只用 Python 标准库,所有代码都在本文件中。
"""
from __future__ import annotations

import sys

ORIGINAL = '''
def parse_amount(s):
    return float(s.strip())
'''

# 候选补丁 A:用正则校验格式,修好了千位分隔符,但正则漏掉了负号
PATCH_A = '''
import re
_PAT = re.compile(r"^\\d{1,3}(,\\d{3})*(\\.\\d+)?$|^\\d+(\\.\\d+)?$")
def parse_amount(s):
    s = s.strip()
    if not _PAT.match(s):
        raise ValueError("bad amount: " + s)
    return float(s.replace(",", ""))
'''

# 候选补丁 B:同样校验格式,保留可选负号
PATCH_B = '''
import re
_PAT = re.compile(r"^-?(\\d{1,3}(,\\d{3})*|\\d+)(\\.\\d+)?$")
def parse_amount(s):
    s = s.strip()
    if not _PAT.match(s):
        raise ValueError("bad amount: " + s)
    return float(s.replace(",", ""))
'''

# bug 报告对应的复现检查
BUG_CHECK = ("1,234.50", 1234.5)

# 既有的、原本就支持的行为(相当于已有的测试集)
EXISTING = [
    ("12.5", 12.5),
    (" 7 ", 7.0),
    ("-3", -3.0),
    ("0.25", 0.25),
    ("1000", 1000.0),
]


def load(src: str):
    ns: dict = {}
    exec(src, ns)  # noqa: S102 —— 只执行本文件内的固定代码
    return ns["parse_amount"]


def passes(fn, case) -> bool:
    inp, want = case
    try:
        return abs(fn(inp) - want) < 1e-9
    except Exception:  # noqa: BLE001
        return False


def three_arm(patch: str) -> tuple[bool, list[str]]:
    arms = [
        ("原代码", ORIGINAL, False),
        ("打补丁", patch, True),
        ("撤回补丁", ORIGINAL, False),
    ]
    log, ok = [], True
    for name, src, want in arms:
        got = passes(load(src), BUG_CHECK)
        good = got == want
        ok &= good
        log.append(f"{name}: 复现检查{'通过' if got else '失败'}(期望{'通过' if want else '失败'}){'✓' if good else '✗'}")
    return ok, log


def regressions(patch: str) -> list[str]:
    before, after = load(ORIGINAL), load(patch)
    return [inp for inp, want in EXISTING if passes(before, (inp, want)) and not passes(after, (inp, want))]


def main() -> int:
    results = {}
    for name, patch in (("补丁 A", PATCH_A), ("补丁 B", PATCH_B)):
        ok, log = three_arm(patch)
        regs = regressions(patch)
        results[name] = (ok, regs)
        print(f"== {name} ==")
        for line in log:
            print("  " + line)
        print(f"  三臂证明:{'成立' if ok else '不成立'}")
        print(f"  回归:{len(regs)} 个" + (f",原本支持、现在失败的输入:{regs!r}" if regs else ""))
        verdict = "可接受" if ok and not regs else "拒绝"
        print(f"  结论:{verdict}\n")

    a_ok, a_regs = results["补丁 A"]
    b_ok, b_regs = results["补丁 B"]
    checks = [
        ("两个补丁都通过三臂证明", a_ok and b_ok),
        ("补丁 A 被回归检查拦下(负数解析被破坏)", a_regs == ["-3"]),
        ("补丁 B 没有回归", b_regs == []),
    ]
    for desc, good in checks:
        print(f"[{'PASS' if good else 'FAIL'}] {desc}")
    all_ok = all(g for _, g in checks)
    print("\nRESULT:", "只看 bug 是否修好会接受补丁 A;加上回归检查后只接受补丁 B" if all_ok else "检查未全部通过")
    return 0 if all_ok else 1


if __name__ == "__main__":
    sys.exit(main())
requirements.txt1 行
# 无第三方依赖,只使用 Python 标准库
Esc
↑ ↓ 选择Enter 打开

键盘快捷键

j / k
下一条 / 上一条
o
展开或收起当前条目
Enter
打开当前条目详情
c
复制当前条目链接
s
收藏 / 取消收藏当前条目
/ 或 ⌘K
搜索
1 2 3
切换密度:标题 / 摘要 / 完整
t
切换主题
?
显示本帮助