10 月 4 日下午,我在一个仓库里多加了一行重复的 import。
本地跑全量:3254 通过,6 个基线失败,3 skipped。全绿。
推到 PR,CI 的 ubuntu 3.11 和 3.13 两个作业直接红了:
javascript
misakanet/profile.py:20:8: F811 [*] Redefinition of unused `sys` from line 19
F811 就是「重定义」。没错,第 19 行本来就有 import sys,我又加了一行。这是个不需要解释的 bug。
值得解释的不是这行 import,是另一件事:我在本地看到 3254 个测试跑完、看到一行 summary、看到全是绿的那两秒钟里,手上握着一个和真通过逐字相同的信号。
一个「绿」有三种含义
那行 F811 是我自己写的,这个错误毫无价值。真正的问题是:是谁替我决定那 3254 个测试可以不算数的?
顺着这个问题查下去,我在一个仓库里翻出了三种不同的假绿。它们在 CI 界面上的样子完全一样 ------ 绿的 checkbox、绿的 badge、绿的 ✅。
| 类型 | 表面现象 | 实际发生了什么 |
|---|---|---|
| A 跑绿了,但抓不到 | ✅ 通过 | 门禁物理上不可能变红 |
| B 根本没跑 | ✅ 通过 | 依赖缺失被 skip 吞掉 |
| D 跑的是另一棵树 | ✅ 通过 | 检查在别的 ref 上跑,或绿了没有接收端 |
三类的共同点是:它们和真通过的输出不可区分。 这才是难的地方 ------ 如果假绿长得不一样,一眼就能看出来。
下面每一条我都打开原文读过,不是转述。
A 类:跑绿了,但抓不到
这类门禁的共性是:它的判据指向一个永远为真的条件。不是判据写错了,是判据被接在了一个恒真的那端。
一次性找出 4 处
2026 年 9 月 21 日,一天之内,仓库里找到 4 处「看起来是门禁、其实永远不会红」:
| issue | 现象 |
|---|---|
| #2006 / #2008 | 跨平台矩阵 8 条腿一条测试都没跑,仍然打印 ✅ |
| #1822 | 工具计数徽章 grep 的正是它自己守护的那个文件 |
| #1999 | 26 条测试里有 23 条在生产代码回滚后依然通过 |
| #2002 | 断言 PATH=/usr/bin:/bin(在 CI runner 上恒真)的测试 |
第 4 条最直白:断言「系统 PATH 就是 /usr/bin:/bin」。在 CI runner 上这个断言永远成立,因为 runner 的 PATH 本来就是这个。所以这条测试绿了 100%,验证的信息量是 0。
第 3 条更有意思:26 条测试,生产代码整个回滚掉之后,其中 23 条照样通过。也就是说这 23 条测的根本不是那段代码。
然后逐个把它们记成 commit
有意思的是,这批缺陷不是靠 review 抓的,是靠后来专门造的一台机器抓的 ------ 我翻了 6 个 commit,每一个标题都是一个「无法失败」的独立案例:
b61d1ed2 #2306(09-26) ------ 这条最干净。测试声称在管「draft 课程会不会漏进搜索结果」,代码这么算:
python
draft_count = sum(1 for r in results if r.get("status") == "draft")
但它跑的是 compact(默认)搜索 ,而 status 字段压根不在 compact 的投影里,只在 detail=full 才有。于是 r.get("status") 对每一条结果都是 None,draft_count 恒为 0,断言无条件通过。
讽刺的是,draft 真的会被搜出来 ------ probe lesson 就是个 draft,搜索能命中它。这条 guard 声称在防的事正在发生,而它报绿。
a602ed8e #2270(09-28) ------ 直接把那条空转的 draft-status 检查删了。不是修,是认。
0fff7ee6 #2815(10-04) ------ DSH-home 守卫。它在所有 agent-assisted checkout 里都在 /tmp worktree 下跑,但测试传进去的是仓库根目录,所以那个「拒绝临时目录之外的 home」的断言在真跑的时候从不生效。
3761785(09-21) ------ commit message 里就写着 "a green run that verified nothing"。
c675619b #2201(09-25) ------ D1 备份守卫在 macOS 上根本没有 verdict 。用 stat -c%s 拿文件大小,macOS 的 BSD stat 不认 -c,直接 illegal option。这条腿上的红不是「判错」,是没判。
23a9ee55 #1921(09-20) ------ 最诚实的一条:加一个报警,「PR 门禁从没跑过的时候要报警」。承认了「静默跳过」本身就是一类需要被抓的事故。
这类缺陷不能靠 review 抓,所以造了一台机器
方法叫门禁变异抽查 (docs/maintainer/gate-mutation-audit.md,#2045,09-22 落地):主动把门禁守护的那个东西故意改坏,然后看门禁红不红。
判读方式只有两种:
- 红 = 门禁还能失败,符合预期(这才是「通过」)
- 绿 = 门禁无法失败 → 脚本打印
cannot fail并非零退出
也就是说在这台机器上,「通过」的判定被整个反过来了。2026-09-22 那次实测输出:
scss
2 gate(s), 6 mutation(s) --- 5 red (as required), 1 skipped (network),
0 that did not prove the gate can fail
它还有一个刻意的设计:这套变异审计不挂在 PR 上,只在每周一 06:43 UTC 定时跑。理由是它回答的是「这个门禁还有能力失败吗」,尺度是月,不是提交。
外部对照:slim-m #1849
GitHub 上有人在 10-04 报了一个同形状的 case,标题是 "the release asset watchdog can't fail, because piping into tee hides the exit code"。
原文:
the step's exit code is tee's and the checker's exit 1 for an incomplete release gets thrown away.
steps.scan.outcomeis then alwayssuccess,ADVISORY_STATUSis alwaysclean, norelease-incompleteissue opens and the "fail the run" step never fires.
中文:门禁的退出码被管道吞了,于是那一步永远成功,告警状态永远干净,开 issue 那步永远不触发。
最狠的一句在 notes 里:
The workflow's only test (
test_the_script_is_the_one_the_workflow_runs) just checks the script name appears in the yaml, so nothing exercises the exit code path.
中文:这个工作流唯一的测试只检查脚本名出现在 yaml 里,所以没有任何东西走一遍退出码那条路。
这就是 A 类的完整形状:门禁不可能红,而它自己的测试只检查门禁"存在",从不检查门禁"能红"。 存在性测试给了存在性的安心感,没给有效性的任何保证。
B 类:根本没跑
回到我自己的那行 F811。
本地为什么看不见?因为仓库里管 F821/F811 的门禁是这个文件 ------ tests/test_python_undefined_names.py,3 个测试。它的第一件事是探测 ruff 能不能跑:
python
def ruff_available() -> None:
probe = ruff("--version")
if probe.returncode != 0:
pytest.skip("ruff is not installed in this interpreter, so the gate cannot run. ...")
我的环境里没装 ruff。于是这 3 个测试全部 skip ,F821/F811 这道门禁在我本地等于不存在。那句「3254 通过」听起来是满的,实际上是 3254 passed + 6 failed + 3 skipped ------ 后两个数字被折进了「全绿」里。
而它在 CI 上是两个作业(ubuntu 3.11 / 3.13)真跑真红的。
handoff 里留了一句操作要点,我到现在还在用:
看到 "3 passed" 才是真的在跑;"3 skipped" 等于没跑
这句话的重点在「3」这个数上 ------ 你得同时知道「有几个测试」和「过了几个」。只报 passed 数的输出会把 3 skipped 显示成一行安静的空。
更隐蔽的一层:一个测试都没跑
同一份 handoff 里还有另一段,比 skip 更难发现。
pytest tests/ 在收集阶段就中断了:
makefile
Interrupted: 3 errors during collection
三个测试文件 import 就失败了,一个测试都没开始跑。 那三个是 test_intake_rate_limit_key.py、test_intake_spam_guard.py、test_mcp_http_server.py。
原文对这个的判断:
这比红更危险,因为它长得像"环境坏了"而不是"你什么都没测"。
这是本篇我最想让你记住的一句。skip 的输出里至少有 skipped 这个词,你会看见。本地 pytest 在收集期崩掉,输出长得完全像"我电脑环境坏了,重装一下就好" ------ 而它真正的含义是「这一轮 0 个测试被执行」,比红更糟,因为红至少还告诉你某个断言没成立。
B 类的判别方法很简单,而且只要 5 秒:
你跑的那条命令,报的是 3 passed 还是 3 skipped?采集期有没有 error?
外部对照:exarchos #2107
lvlup-sw/exarchos 10-05 开了个 issue,标题是「Install tests pass when the self-test exclusions, the negation rule, the VCS preamble, the canonical key order or the retired-reference sweep is broken」。
For each item, a probe broke the property that the test names, and the test file passed.
中文:每一项里,一个探针破坏了那个测试名字所声称的性质,而那个测试文件还是过了。
里面最干净的一个,VcsPreamble_PresentInAffectedSkills:
the test reads
content/<name>/SKILL.md, and each skill is atcontent/<domain>/skills/<name>/. None of the six paths exists, so the loop skips each skill.
中文:测试读的路径是 content/<name>/SKILL.md,而实际每个 skill 在 content/<domain>/skills/<name>/。那六个路径一个都不存在,于是循环把每个 skill 都跳过了。
issue 的收尾那句总结得比什么都狠:
The VCS preamble test reads no skill at all.
中文:这个 VCS 前言测试,一个 skill 都没读。
「6 个路径不存在 → 循环体一次没执行 → 14 个测试全绿」。这一类假绿不需要任何巧合,只要路径写错一个字符,就凭空多出 14 个绿色 checkbox。
D 类:跑的是另一棵树
最后一类最难发现,因为它需要你跳出「测试」这个视角,去看这次测试到底是在哪棵树上、给谁看的。
我自己的仓库里有一个 docs/maintainer/ 交接文档目录,专门放给下一轮维护者看的交接。这里有个三层同时坏掉的结构。
第一层:收据本身不在读者所在的地方
10-04 那份 33 KB 的 handoff 当时写在 /home/eric_jia/misakanet-handoff-2026-10-04.md,从没进仓库,抬头还明写「本地交接文档,不在仓库里」。
后果不是「风格不统一」。仓库里有一条门禁专门保证「代码引用的收据必须存在」,而一份没人能读到的 handoff 就是不存在的收据。下一轮维护者(也就是我)花了几分钟才在 WSL 的 home 目录里翻到它。
第二层:检查收据的那条门禁,自己带着一张假收据上线
#2827(4a8e5749,10-04 08:55Z 开、10:06Z 合)加了上面那条门禁,并当场修掉了 5 处悬空引用中的 4 处。第 5 处漏了:
bash
E AssertionError: code cites a handoff that was never committed, so the measurement
it stands on cannot be checked by anyone:
E .github/workflows/registry-drift-watch.yml cites docs/maintainer/handoff-2026-09-24.md,
which does not exist
handoff-2026-09-24.md 从来没存在过 ------ 序列是 09-16 → 09-30,中间没有 09-24。那条 handoff 里的「~1 request in 4」这个数字在仓库里也查不到出处。
于是这条门禁在九个 test 作业上全红,release PR #2825(2.41.2)直接 BLOCKED。
一层讽刺:这条门禁的全部职责就是「不让假收据进仓库」,而它自己带着一张假收据进了仓库。
第三层:main 上根本不跑矩阵
这才是三层里最该先记的,因为它解释了上面两层为什么没被早点发现。
shell
$ gh run list --commit 4a8e5749 --limit 25 # 空
$ gh run list --limit 12 # main 上只有 Update Badge Counts / Site build watch /
# PR Quality Gate(skipped) / deploy-worker
空的。 tests/ 矩阵只在 PR 上跑。推 main 的时候 PR Quality Gate 是 skipped,没有任何 test (...) 作业。
所以 #2827 让 main 变红这件事,在 main 上完全不可见 ------ 它只在下一个 PR 上来的时候才显形。
这次是靠「2.41.2 发布 PR 恰好存在、恰好被挡住」才浮出来的。handoff 原文那句判断我直接抄:
这不是门禁在工作,这是运气。
规则集 23826057(名字叫 main: the deterministic gates)的必需检查只有 4 条:
vbnet
DCO / Signed-off-by
test (ubuntu-latest, 3.11)
gate
audit
其余全部是可选的。 判断「能不能合」的时候,别把可选的腿当门禁。
(顺带补一句我这次复核的:那次被挡之后,#2848 修掉了最后一处引用,#2825 随后合入,v2.41.2 于 10-05T16:50:16Z 发布。所以「2.41.2 没发出去」这个说法当时只在几分钟内为真。但要点不在发布结果上 ------ 要点是它当时能被看见,是因为发布 PR 恰好存在,不是因为 main 上的矩阵在跑。)
外部对照:perl-lsp-swarm #17354
这是 D 类里「绿了但没有接收端」的另一种形态。
标题:Nightly benchmark regressions never reach a human (no alert path)。
No nightly benchmark regression --- in any category --- reaches a human. The scheduled-run signal is a step summary nobody watches plus artifacts ("manual investigation").
中文:任何类别的夜间基准回归都到不了人跟前。 定时任务的信号是一个没人看的 step summary,加上 artifacts 上写的一句「manual investigation」。
具体配置是:compare 和 alert 两个步骤都是 continue-on-error: true,工作流权限只有 contents: read,PR 评论被移除(#6028),退出码被吞掉,trend 存储禁用。
所以这一类的形状是:矩阵每天按时跑,绿灯按时亮,但结论的接收端不存在。 跟 A 类的区别是 A 的判据恒真、这一类的判据可能是真的 ------ 判据没问题,是没人接线。
收尾:五个能自己问的问题
这三类假绿的共同点是:输出和真通过不可区分。所以只能靠换问题来区分,不能靠读输出。
我把自己的做法压缩成五问,你拿去问自己那个仓库:
一、A 类:这个门禁的红,是什么让它红的? 把它守护的那个东西改坏一次,看它真的红吗 ------ 别靠推理,靠跑一次。如果改坏了它还是绿,那这个 ✅ 是空的。
二、B 类:我跑的那条命令,报的是「3 passed」还是「3 skipped」? 两个数都要知道。采集期有没有 error?pytest tests/ 会不会在收集阶段就中断 ------ 那不是环境坏了,那是一个都没跑。
三、D 类:这个检查在哪个 ref 上跑? gh run list --commit <sha> 返回空吗?返回空就意味着这次 push 上什么都没跑。规则集里 required_status_checks 只有几条?剩下的都是可选的腿,别当门禁。
四、这份「收据」,读者能打开吗? 它在仓库里吗?它引用的每一个文件都存在吗?写在自己 home 目录里的东西,对下一轮维护者就是不存在的。
五、这个门禁的结论,最后落在哪儿?谁会看? 一个 step summary 没人打开的话,它和一个不存在的门禁没有区别。
这五问都不是新东西,但把它们放在一起有个好处:它们覆盖了三类假绿的三个位置 ------ 判据、触发、接收端。 一个门禁要真的有效,三个位置都得是实的。少任何一个,它给你的绿就只是一个形状。
至于我自己那行多余的 import sys ------ 那个 bug 一分钟就修好了。真正花时间的是搞清楚:为什么我本地的 3254 个绿,和 CI 的两个红,能在同一个 commit 上同时为真。
答案是:因为那次本地运行是 3254 passed + 6 failed + 3 skipped,而那 3 个 skipped 空跑这件事,在输出里长得和真通过一模一样。
附:本文引用的仓库与编号
自家:Ikalus1988/MisakaNet。每个 commit / issue 我都用 git log 或 gh 核过。
外部三条,2026-10-04 ~ 10-05 公开:
- Slim-m-org/slim-m#1849 --- 10-04 23:10 UTC
- lvlup-sw/exarchos#2107 --- 10-05 07:12 UTC
- EffortlessMetrics/perl-lsp-swarm#17354 --- 10-05 16:28 UTC
自家 A 类那批修 commit:
b61d1ed2#2306(09-26)·a602ed8e#2270(09-28)·0fff7ee6#2815(10-04)3761785(09-21)·c675619b#2201(09-25)·23a9ee55#1921(09-20)- 审计文档
docs/maintainer/gate-mutation-audit.md(#2045,09-22 落地)
B 类证据:docs/maintainer/handoff-2026-10-04.md:100-128、docs/maintainer/handoff-2026-10-05.md:257。 D 类证据:docs/maintainer/handoff-2026-10-05.md §2.1--2.3。