退出码 127,command not found。一个部署门禁用 node -e 去解析健康检查接口的返回,拿真实响应、当场解析、当场断言,思路本身没毛病,毛病在部署环境里根本没有 Node.js。于是连续六次部署死在 127 上,而门禁这一步在审查界面上始终是绿的。同一个脚本,跑检查的地方有 node,真正部署的地方没有。检查永远查不出一个不存在于自己世界里的故障。
这不是孤例。做这次盘点的人翻了自己的三个仓库,CI 里跑的、钩子里挂的、定时任务里巡的,全算上,204 个自动化检查。其中带阴性对照探针的,22 个。阴性对照说的是这么个东西:往检查里喂一个已知该被拒收的坏样本,看它拒不拒。204 个里只有 22 个干过这件事,约 11%;剩下那 89%,从出生到现在,一个坏输入都没见过。
11% 这个数字我想了好几天。写一个坏样本探针是全天下最便宜的测试,所以这不是能力问题;什么 CI 都跑得动它,也不是工具问题。它是个来路问题:我们的测试文化从头就没把“检查”当测量仪器,自然没人觉得仪器需要校准。要讲清这个来路,得往回挖,一直挖到 Dijkstra 那句话。
1970 年前后,Dijkstra 在《结构化程序设计札记》里写下一句后来被引用到起茧的话:测试能证明缺陷存在,永远不能证明缺陷不存在。这句话常被读反——人们以为他在贬低测试,他其实在划边界:绿色从来不是“没问题”,只是“没找到问题”。这个边界在 1970 年是常识;五十五年后,它被做进徽章、做进退出码、做进“合并按钮亮了就能点”的肌肉记忆,语义膨胀成“这个改动没问题”。没人开会决定过这次膨胀。它是长出来的。
1973 年前后,管道在贝尔实验室落地,grep 大概也是那两年从 ed 的 g/re/p 里长出来的;到 1979 年的第七版,“程序是过滤器、过滤器接管道、管道末端报告退出码”这套语法已经成型。先说语境:一个坐在终端前的人,肉眼看着命令跑完。退出码是给那双眼睛的速记;管道只把最后一个命令的退出码传下去,是因为反正有人在读前面的输出,人自己会察觉不对劲。这些决策在 1979 年全都成立、全都漂亮,前提是屏幕前有人。
如果当年 shell 把管道里每个环节的退出码都收拢起来,后面五十年的脚本会少摔多少跟头?我认真想过这个问题,然后把它扔了。反事实历史是廉价的,何况那个取舍在交互语境下几乎是对的。真正的问题不在 1979 年的设计,在我们后来把这套给活人用的速记,原封不动焊进了无人值守的机器。代价从这里开始:脚本没人盯了以后,那双眼睛的职责没有继承人。
九十年代末,另一条线接了上来。XP 那拨人把持续集成和自测试构建从边缘实践推成主流纪律;Fowler 2000 年那篇讲持续集成的文章后来成了事实上的教义,单一命令构建、构建自测试、保持绿色;2001 年 CruiseControl 出来;2005 年 Hudson 落地,2011 年改叫 Jenkins,也是那一年,Travis 把“每个 push 自动跑”做成了服务;2019 年 GitHub Actions 把这整套焊进了托管平台本身的 UI。时间线摆出来,“绿色”的语义一路升值:退出码 0,构建通过,测试通过,改动可接受,可以合并。每一级升值都有正当理由,每一级也都在离 Dijkstra 那个边界更远一步。这是绿色的登基史,一步一个合理的决定。
这条时间线上掉队的是人工审查者。原文有个提法我直接借用:结论承载型检查,读你的代码、配置或系统状态,然后下断言,这类检查坐的其实是审查者的判断席。以前合并一个改动,最后一道闸门是个活人。活人有种不值钱但可靠的直觉:日志里有股怪味、这个绿得有点太顺、这步凭什么两秒就跑完。这种直觉是没写进任何退出码的粗糙阴性对照,但它在。自动检查接管闸门的时候,接走的是权限,没接走直觉。
校准这件事,别的行当一百多年前就解决了。往仪器里塞已知样本来验仪器,往前能追到科赫那代人验证病原的规程,往后到 Fisher 在 1935 年把对照做进实验设计的方法论。软件行业把实验室的自动化搬了个干净——流水线、并行、报告、归档——唯独没进口对照组这个概念。这次盘点里那 22 个探针用的标记叫 KONTROLLE,喂已知坏样本、确认被拒收。这命名一股实验室味,我猜是有意的;这段是推测,原文没解释词源。一个人在 2026 年用手工业的方式,给自己补一件本来该随机器一起到货的零件。覆盖面,11%。
剩下两个实例值得并排看,因为它们朝相反的方向撒谎。一个采集任务只认退出码:跑成功,写进去 7 条数据,然后撞上一条非致命警告、非零退出,整次运行被判失败、排队重试——仪器把“活干完了但抱怨了一句”报成“没干完”。另一个是错误分类器,拿 '50[024]' 这个模式去扫输出文本,把配额提示里的 500 当成服务器错误,把一次成功判成故障,仪器对着信号而不是产物下判断。加上开头那个 node 缺席的例子,三个故障没有一个是“检查发现了问题”,全是“检查不知道自己在看什么”。
所以原则就一条:验产物本身,别验信号。退出码、摘要、完成状态、徽章,都是信号;文件、diff、响应体、数据库里那几行,才是产物。信号是别人对产物的转述,转述链条上任何一环撒谎,你拿到的就是那句经典的、危险的零结果。绿色零结果的危险在输出端分不开两种世界:“没找到问题”和“找不到问题”在日志里长得一模一样。阴性对照是唯一的区分手段,喂一个铁定该报错的样本,看它报不报。这不是什么高深方法论,这是仪器附带的说明书。
盘点里最好的部分,是作者自己栽的两跤,就栽在写它的那个星期里。一次是等价测试的缺陷长在测试代码自己身上,坏仪器检查自己,得出的当然是健康。另一次更妙:管道里 npm test 的输出接了个 grep,grep 的退出码盖住了真实的测试失败,测试挂了,管道绿了。这不是 2026 年的新坑,是 1973 年“只报最后一个命令退出码”那个决策,五十多年后照原价收租。当年给终端前那双眼睛留的余地,如今站在无人值守的 runner 脚本里:眼睛没有了,余地还在。这不是设计缺陷,是遗产的运费。
按老规矩交代材料成色:一名开发者、三个仓库、一个星期,计数靠命名标记扫出来,有浮动空间,性质是个案报告,外推不成“行业的 11%”。但说句偏心的话,我从头到尾没觉得这个数字偏乐观——以我自己经手过的流水线而论,从没喂过坏样本的检查是常态,喂过的才是异类。那 11% 大概率还是幸存者里筛出来的优等生。
肯定有人要说了:阴性对照自己也会腐化。会,我承认。产物变了、断言松了、坏样本过时了,探针都会悄悄失效。但它和普通检查的区别在死法上。普通检查腐化以后继续绿,绿到你不知道它瞎了;阴性对照一旦失效,会在下一次检查变更时炸出一个看得见的失败。会大声死的仪器,比会安静绿的仪器可靠。这一条区别,就是写它们的全部理由。
原文最后落到 AI 上,我也觉得该落在这儿:AI 把开发者的主要工作从写代码换成验证 AI 的产出,于是你自己搭的那些检查,第一次成了主战武器。结构上是老剧本重演,上一回大规模许诺“人只管验收、产出自动来”的是 4GL 和 CASE 那拨工具,我之前讲过那条路是怎么死的:验收的人一旦真信了自己只剩按按钮,按出来的东西就没人兜底。这次不同的地方在量:代码生产的通量上去了,验证通量没上去的部分全部转嫁给自动化检查,而自动化检查里 89% 从没证明过自己看得见问题。拿没校准过的仪器去接翻了倍的测量量,这间实验室产得再快,数据都不能信。
所以回到今天。绿色不是 Dijkstra 意义上的任何承诺,它是 1979 年的退出码、2000 年的 CI 教义、2019 年的徽章 UI 三代决策叠出来的信号,每一代都默认上一代已经把判断做完了。往自己的检查里喂一个坏样本,是全天下最便宜的测试,买的不是覆盖率,是知道自己这台仪器到底瞎不瞎。下次看到一条一路绿到底的流水线,先别急着羡慕,问一句:它拒绝过什么东西吗?一个从没拒绝过任何东西的门卫,你分不清它是尽职,还是睡着了。
