你的检查是绿的,因为没人问过它能失败的问题
一个检查通过了,你会松一口气。但"通过"这个词其实有歧义:它可能是"我确认这件事没问题",也可能是"我从没问过它能不能失败"。这两件事在仪表盘上长得一模一样。
一个真实逃逸的校验
配置门禁要做的事很普通:确认每个 key 都在、每个 value 不是空的。
{
"service": "orders",
"region": "cn-north-4",
"replicas": 3,
"owner": "platform"
}
有人把 "replicas": 3 改成 "replicas": 0。服务从此起不来。门禁放行了。
原因不复杂:门禁检查的是「key 存在」和「value 非空」。0 既存在,又不为空。它测的所有结构属性依然成立。
这类缺口能活下来,恰恰因为它只测形状。任何只看结构的东西,都看不见它。
把"找缺口"变成可重复的动作
靠人眼找这种缺口不现实。但有一件事机器做得很好:变异。
把配置逐行改坏——删一行、清空一个值、把数字改成 0——每改一次就跑一遍你的门禁,看它响不响。
mutants : 12
caught 11 / 12
mutants the gate let through (1):
* blank-value:config.json:4:"replicas":
12 个变异,11 个被拦住,1 个溜过去。溜过去的那个,精确指向一条没有任何东西在测的规则。
同一条规则,用在数字上
门禁之外,同一个问题适用于所有指标。
你有一个健康度分数,它一直是 1.0。它在告诉你系统完美吗?不是。它在告诉你:这个数字从来没被观测到会因不同输入取不同的值。
一个数字,在被观测到会因不同输入取不同值之前,都不是测量。
固定不变的 1.0 不是"满分",是"没测"。一个从没被问过能失败的问题的仪表盘,是另一种形式的静默——而工具存在的意义,是让这种静默变得能被听见。
第二个漏洞:检查是什么时候写的
就算一个检查真的能失败,还有第二个口子:它是什么时候写出来的。
如果检查是等工作做完才写的,写它的人已经知道结果长什么样,就很容易把检查调成"刚好通过"。这不是恶意,是自然。
解法是把检查冻结在运行之前:先写验收条件,hash 锁定,再去干活。干完再改检查?那之后的所有结论作废——可证明地作废,不是靠约定。
还有一层更狠的自检:检查通过之后,把被检查的产物改坏,再跑一遍。如果一个检查在坏输入上依然通过,它从来就不是证据。
$ precheck register
froze 1 commitment(s) at seq=1 sha256=e2d4561aa9f0
$ precheck settle
PASS the deployment config is valid and safe to ship
$ precheck audit
ran 4 mutation(s) across the declared artefacts
2 check/mutation pair(s) survived -- these prove nothing yet
真实数字
把它指向一个 25 条规则的门禁:175 个变异,78 个溜过,其中 4 个是真缺陷。
4 个里最值得看的一个不是模拟出来的,是设计缺陷:一条规则的触发条件由被检查方自己提供——“如果你声明自己是非交互的,就必须注册”。于是删掉那句声明,要求就消失了。
一个只在你承认有罪时才生效的检查,不是检查。
一个反直觉的忠告
这类工具最容易被误用的地方,是被当成判决器。
跑一遍,输出一堆"存活变异",然后宣布发现了 19 个缺陷——这是撒谎。存活变异是候选,不是缺陷。上面那次跑里,大多数存活变异是无害的(删掉一个 title、删掉一个可选字段)。
一个会喊"19 个缺陷!"的工具,在你第二次核对时就会被你丢掉。所以它该给的是清单,不是判决。
怎么落地
- 把你的门禁命令接进来。任何"非零即拒绝"的命令都行:linter、schema 校验、CI 步骤、你自己的
validate.py。 - 让它在 CI 里逐行变异、重跑、报告活下来的变异。
- 对指标做同一件事:找一个你已经在采集的分数,问它是否曾经取过不同的值。没有,就不是指标。
- 把"检查必须写在运行前、且 hash 锁定"变成流程,而不是靠自觉。
零依赖、退出码 0/1,可以直接当它自己的 CI 步骤。
绿是结果,不是证据。真正的问题是:这个绿,有没有可能变成红?
- 点赞
- 收藏
- 关注作者
评论(0)