跳到正文
Hacker News· vent911·· 6 天前AI 评分37

PR-witness:用基分支测试跑 PR 代码,揪出被绿灯掩盖的回归

Show HN: PR-witness – run the base branch's tests against the PR's code

AI 导读

PR-witness 通过运行基分支的测试来检验 PR 代码,捕捉 CI 绿灯下被隐藏的回归。该工具已在 PyPI(pip install pr-witness)、GitHub Marketplace 上线,并可作为 Claude Code 插件安装。

正文 · AI 翻译

检查一个拉取请求的测试是否仍然意味着它们之前所意味的东西。

测试套件是一种声明:这些行为成立。一次变更可以让套件保持绿色,同时悄悄撤回该声明——通过缩小被收集的范围、通过重写期望值以匹配新的 bug、通过跳过那个守护它所破坏之物的测试。CI 两种情况都报告绿色,因为 CI 只会用新代码运行新测试。

pr-witness 运行了没人运行的组合:基分支的测试针对拉取请求的代码。一个之前通过、现在失败的测试,就是拉取请求隐藏的回归,无论那个绿色对勾怎么说。

状态

阶段 0–6 已完成:语料库、跨运行、差异完整性(通过 checkwash)、声明检查、GitHub Action、签名证据和 Claude Code 插件。已在 PyPI(pip install pr-witness)、GitHub Marketplace 上发布,并可作为 Claude Code 插件安装。

关于数字。评估语料库是 17 个合成案例,由编写检测器的同一个人编写,按三种判定评分:作弊必须被标记,重写的期望必须返回为 review,干净的变更必须不被触碰。该工具全部 17 个都正确——eval/witness_results.md。那是一个回归门禁,不是准确性声明:它说明该工具没有变差,而不是它在从未见过的拉取请求上表现如何。

第一批真实案例正在循环中,来自两个来源:

  • corpus/real/swebench——一个前沿智能体在 SWE-bench Verified 上的七次测试编辑(从 1,500 个补丁中检查得出),根据维护者自己的测试补丁进行标注。六个端到端运行;六个的判定都匹配(一个 clean,五个 review)。
  • corpus/real/swechat——来自真实仓库(SALT-NLP/SWE-chat)上真实 Claude Code 会话的提交:挖掘了 3,355 次智能体测试编辑,目前有七个端到端运行,由人工标注。七个中有六个匹配。那个未命中已在那里记录;那个在获得自己的发现之前一直是未命中的案例也已记录——一个被删除的测试被另一个不同的测试替换,任何运行都看不到,而 coverage_replaced 从源代码中读取。

那些案例是凭肉眼挑选的,而且几乎每一个之后工具都被修改过,所以它们的分数对它有所美化。没有美化的数字是:batch 1——十七个 SWE-chat 提交,由一条书面规则选出,在运行前标注,在工具冻结的情况下运行。十二个产生了证据;判定在十二个中的七个上匹配。所有五个未命中都是工具的问题,其中四个是在一个诚实提交上返回 flag。review 每次使用时都是对的;flag 每次都是错的。该批次中没有提交是不诚实的,因此在真实数据上捕获一个仍未测量。这四个原因已在 0.3.1 中修复;这十二个现在一致了,但这不能衡量任何东西——新的数字需要工具从未见过的提交。

这个数字由什么构成。属于这个工具自身的部分——用新代码运行基础分支的测试并展示变化——在这批中一次都没错。在跨运行完整跑完的十一个案例中,它报告的内容每次都和标签一致,包括六个提交,它正确地展示了被重写或删除的测试以供审查;在第十二个中,pytest 提前终止了运行。四个误报没有一个来自它:三个是原样传递的 checkwash 评级,一个是跳过检查,没有询问被跳过的测试是否是新的。在这四个中的两个里,跨运行正确的 review 已经在报告里了,就在标记下面。

所以:运行的比较在它从未见过的真实提交上经受住了考验;决定用多大声说出来的那一层没有,并且被改了。在那一层通过它自己的冻结批次之前,以 report 模式运行,并在用 flag 把关合并之前阅读标记为审查的内容。

真实运行比合成语料库更多地改变了这个工具——review 判定、greened_by_test_edit(一个失败的测试套件通过编辑测试变绿,跨运行看不到)、base_broken、bun 运行器——每一个都来自一个具体的提交,列在两个 README 中。

数字见 eval/results.md,案例如何构建见 corpus/README.md。

信号

信号 它捕获什么
crossrun 一个 PR 没有 触碰的基础分支测试,现在在 PR 的代码上失败——一个隐藏的回归(高)
expectation_changed 一个 PR 重写 的测试,其旧断言在新代码上失败;报告显示旧断言和新断言(中 → review)
greened_by_test_edit 一个在基础分支上为红、现在为绿的测试,其旧版本仍然在新代码上失败——是测试编辑让它通过的,不是代码(中 → review)
tests_removed_still_passing 一个 PR 删除的测试,它仍然能针对 PR 的代码通过——行为保留了,覆盖率没了(中 → review)
coverage_replaced 替换:被修改的测试过去调用的一个函数仍然存在,且没有测试再真正调用它——运行看不到的那一处测试编辑,通过阅读源码发现(Python;中 → review)
base_broken / base_not_green 每个基础测试都失败不是证据,不是干净;一些失败是上下文
denominator 实际运行的测试比以前少——4966/4966 ALL PASSED 把戏
skip_inflation 收集数量稳定,而跳过数量攀升
added_tests_skipped PR 添加但不运行的测试——报告,但不标记:之前运行的没有任何东西停止运行(低)
claim_false 描述断言了运行所反驳的东西
claim_unverifiable CI 中没有任何东西能确认或否认的声明

作为 GitHub Action

# .github/workflows/pr-witness.yml
name: pr-witness
on: pull_request
permissions:
  contents: read
  pull-requests: write      # for the comment

jobs:
  witness:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v7
        with: { fetch-depth: 0 }     # both branches must be present
      - uses: actions/setup-python@v7
        with: { python-version: "3.12" }
      - run: pip install -e . pytest  # whatever your own test job installs
      - uses: talhayme/pr-witness@v0.3
        with:
          test-command: pytest        # or vitest, jest, go, django (Django's own runtests.py)
          mode: report                # report | require-ack | strict

它每个拉取请求发布一条评论,并在每次推送时编辑它,因此讨论串不会塞满过时的报告。同一份报告进入作业摘要,evidence.json 作为输出暴露给任何下游。

模式 行为
report 从不使作业失败。默认。
require-ack 在标记上失败,直到维护者添加 test-change-approved 标签
strict 在标记上失败;标签记录意图但不改变事实

一个 标记 是一个被反驳的声明或一个高严重性发现。一个 审查 是一个被重写的测试,其旧断言在新代码上失败:诚实的规范变更还是测试被弯曲以适应 bug,运行无法分辨,所以它显示两个断言,require-ack/strict 像对待标记一样为人类保留作业。其他中等发现和误导性声明会被报告,但在任何模式下都不会使作业失败。

在本地,同样的事情:

pip install pr-witness
pr-witness --base origin/main                   # text report
pr-witness --base origin/main --format markdown --pr-body pr.md
pr-witness --base origin/main --mode require-ack --approved
pr-witness --base origin/main --changed-only      # only the test files the change touches
pr-witness --base origin/main --paths tests/api   # or a subset you name; large suites in seconds

签名证据

使用 sign: "true"(默认值)时,该操作会用 GitHub Artifact Attestations 对 evidence.json 进行签名, 评论中会链接到该证明。这正是这份报告比 agent 随手敲出的评论更有价值的原因:它证明该文件是在 CI 中由这个工作流生成的,且此后未被篡改。

gh attestation verify evidence.json --repo you/your-repo \
  --predicate-type https://github.com/talhayme/pr-witness/evidence/v1

该任务需要 id-token: write、attestations: write 和 artifact-metadata: write。签名在所有 GitHub 套餐的公开仓库中均可使用;私有仓库需要 Enterprise Cloud。当无法运行时,报告仍会发布,并标记为“Unsigned”并附上原因。

证据格式为 schema/evidence.v1.json。

如何消除误报

  1. 阅读该发现的“↳”行——每一行都会说明它在什么情况下是无害的。
  2. 如果行为变更是有意为之,请在拉取请求中说明并 添加 test-change-approved。在 require-ack 模式下这就足够了。
  3. 如果 pr-witness 判断有误,请提交一个 issue并附上 diff;它会 成为语料库中的一个 clean 案例,这样下个版本就不会出现回归。

在 Claude Code 中

claude plugin marketplace add talhayme/pr-witness
claude plugin install pr-witness

三件小事,均为建议性质:

  • 在测试文件变更前暂停。编辑、写入或对测试文件执行 rm/mv/sed 操作时会请求确认,理由为:“如果行为本就该改变,请说明;如果测试碍事,请修改代码——无论如何 CI 都会用新代码运行旧测试。”PR_WITNESS_GUARD=deny 表示坚决拒绝,PR_WITNESS_GUARD=off 表示不再提示。
  • 当 agent 声称测试通过时进行检查。当最终消息做出可核查的断言——“所有测试通过”、“12/12 全绿”、“为 X 添加了测试”——该钩子会在工作树上针对基础分支运行 pr-witness, 并在会话中展示任何与运行结果相矛盾的断言。它从不阻止停止:一个拒绝让 agent 在数字匹配前结束的钩子,会教会它不再给出数字。
  • /pr-witness:verify-before-done——一份检查清单:运行整个测试套件, 引用运行器自己的摘要行,报告分母,列出每个变更的测试及其原因。

该插件是一种礼貌,而非控制。agent 在自己的环境中运行,可以绕过任何钩子。真正的证明是来自 CI 的签名报告,那是 agent 无法触及的地方。

与 checkwash 的关系

checkwash 检测 diff 层面的 篡改——弱化的断言、放宽的容差、被禁用的测试、被改动的 CI——并且做得很好,有自己的公开基准。pr-witness 不是它的 替代品,也不会重新测试那些检测器。它覆盖的是 diff 无法 展示的内容:代码实际运行时发生了什么。

两者都运行。

复现基线

python -m venv venv && ./venv/bin/pip install checkwash pytest
(cd .tsrunner && npm install)

python corpus/build.py         # generate the 16 cases
python corpus/verify.py        # measure the Python cases
python corpus/verify_ts.py     # measure the TypeScript cases
python eval/run_eval.py --tool checkwash
python eval/run_eval.py --tool crossrun

许可证

Apache-2.0。

来源:Hacker News · github.com