PR-witness:用基分支测试跑 PR 代码,揪出被绿灯掩盖的回归
Show HN: PR-witness – run the base branch's tests against the PR's code
PR-witness 通过运行基分支的测试来检验 PR 代码,捕捉 CI 绿灯下被隐藏的回归。该工具已在 PyPI(pip install pr-witness)、GitHub Marketplace 上线,并可作为 Claude Code 插件安装。
检查一个拉取请求的测试是否仍然意味着它们之前所意味的东西。
测试套件是一种声明:这些行为成立。一次变更可以让套件保持绿色,同时悄悄撤回该声明——通过缩小被收集的范围、通过重写期望值以匹配新的 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。
如何消除误报
- 阅读该发现的“↳”行——每一行都会说明它在什么情况下是无害的。
- 如果行为变更是有意为之,请在拉取请求中说明并
添加
test-change-approved。在require-ack模式下这就足够了。 - 如果 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