CI 全绿,但你的质量门禁可能已经悄悄少了一条规则
一、CI 绿灯,说明不了什么
先看一个很常见的组合:
- 你的项目里有
schema.json、有一份 lint 配置、有一个validate.py、有一道 LLM guardrail; - 流水线里挂着一条命令,坏输入进来它就返回非零,构建变红;
- 这条命令一直是绿的。
“一直是绿的"通常被读成"规则在生效”。但它其实只说明最近没有人往里面送坏输入。
这两件事的差别,在规则悄悄失效的时候才会显形——而你恰恰不会知道。规则失效的典型原因不是有人删掉了它,而是它从来就没被验证过:加规则的时候顺手复制了上一行、条件写反了、字段改名之后引用断了、某条分支永远进不去。代码没报错,CI 没变红,你也没有理由去查。
于是你带着一份自以为在保护你的规则集上线了。
二、代码覆盖率为什么盖不住这个
覆盖率回答的问题是:哪些代码被执行过。
它不回答:哪些规则真的在起作用。
一条写错的规则,会被覆盖率当成"已覆盖"——因为它所在的函数确实被调用了,只是它在任何输入下都不返回拒绝。覆盖率给的是执行痕迹,不是判定能力。
真正能回答第二个问题的,是变异测试(mutation testing)的思路:
故意把被测对象改坏。如果测试还能过,说明测试没测到这块。
传统变异测试的靶子是代码。但规则文件、策略配置、schema、guardrail——这些东西同样是"逻辑",同样会失效,却几乎没有人对它们做变异。
三、做法:把变异打在规则文件上
思路很直白:
- 给你一份规则文件(JSON Schema / CI 检查 / lint 配置 / 策略文件 / guardrail);
- 给你那条"返回非零即拒绝"的命令;
- 工具一行一行地删改规则文件,每改一次就重跑一次那条命令;
- 报告哪些改动没有被拦下来。
任何一个"活下来"的变异,都对应一条没有任何东西在测试的规则。
举例:如果删掉 "required": ["name"] 之后,校验命令依然返回 0(通过),那说明"name 必填"这条规则,在你的流水线里其实是空转的——它拦不住任何东西。
四、30 秒复现
它只用 Python 标准库,没有依赖、不需要凭据、不启动任何服务:
git clone https://github.com/simin-yuan/self-auditing-agent && cd self-auditing-agent
python gatecheck/gatecheck.py \
--gate "python examples/gate.py {target}" \
--target examples/service-config
两个参数的含义:
--gate:那条本该拒绝坏输入的命令。任何"返回非零即拒绝"的东西都能当门禁——linter、CI 步骤、schema 校验器、你自己写的validate.py;--target:规则和样本所在的目录。
有一个使用上的坑要点明:门禁命令要写在 --target 外面。 否则工具会把变异打在门禁自己身上,测出来的东西就不是你要的了。
退出码是 0/1,所以它能直接当作流水线里的一个步骤——包括华为云 CodeArts 流水线或任何 CI。零依赖意味着你不需要为了加它而引入一套测试框架。
五、把它对准自己的门禁:175 / 78 / 4
工具好不好用,看它能不能撞出真东西。我拿它对准了我自己那套 25 条规则的校验门禁:
| 指标 | 数字 |
|---|---|
| 生成的变异总数 | 175 |
| 没被拦住的变异(漏过) | 78 |
| 逐条复查后确认的真实缺陷 | 4 类 |
78 个漏过不等于 78 个缺陷。 这一点必须说清楚:里面大部分是无害的——删掉一个 title 字段、删掉一个可选属性,本来就不该影响判定。工具的价值在于把候选缩小到你要人工看的范围,不在于替你下结论。
真正值得看的是那 4 类。
六、最值得看的那一条
四条里最狠的,是一个设计层面的缺陷,不是写错的语法:
有一条规则的触发条件是——“如果你声明自己是非交互功能,你就必须登记”。
问题在于:这个"声明",是被检方自己提供的。
于是攻击路径变得极其简单:把声明删掉,那条要求就消失了。
这条规则在正常路径上工作得很好,文档里读起来也完全合理。它失效的方式是结构性的:
一个只在你自认有罪时才生效的检查,等于没有检查。
(同源的坑在很多地方都能见到:自评表、自查清单、“如涉及 X 请勾选”、由被评估方自己划定评估范围。判断一个检查有没有效,要先问一句——它生效的前提,是谁提供的?)
七、两条设计原则
这套东西里我认为最通用的,不是那个工具,是两条原则:
原则一:一个不能输出"否定"的判据,不是判据。
只会报"通过"的校验器比没有校验器更糟——它给了你信心,但没给你保护。
原则二:只会报"失败"的判据,同样没用。
因为那时你分不清"这是一个严格的检查"还是"这个检查坏掉了"。
所以正确的做法是双向断言:既证明它在坏输入上会拒绝,也证明它在好输入上会通过。任何一边塌了,这个判据就作废。
我的仓库里就是这么做的——两条命令、每次推送都在干净机器上重跑一遍,任一条不成立徽章就变红。主张必须能被复现,否则它只是主张。
八、这套东西的边界(说在前面)
诚实一点,gatecheck 目前的限制是明确的:
- 它只给清单,不给判决。 活下来的变异是候选,不是缺陷。谁要是喊"发现 19 个缺陷",等你第二次核对发现大部分是无害的,你就不会再信它了——工具的可信度是这么被消耗掉的;
- 变异算子只有 7 个,覆盖面有限:目前只做行级的删除/损坏,没有做值级别、类型级别、跨文件的语义变异。所以"没发现漏过"≠“规则没问题”;
- 它测的是规则文件的判定能力,不测规则本身是否符合你的业务意图。一条被完整测试的错规则,依然是错规则。
九、怎么用在你自己的项目上
如果你的项目里有下面任何一样东西,都值得跑一遍:
- 一份 JSON Schema 或 OpenAPI 定义;
- 一道 CI 里的策略检查 / 准入检查;
- 一份 lint 或安全策略配置;
- 一条 LLM 的输出 guardrail。
上手顺序建议:先跑一次拿到清单 → 挑里面你以为是硬约束的那几条 → 手工确认哪些是真缺陷 → 先修"触发条件由被检方提供"这一类结构性问题,再修字段级的。
最后留一个问题,比工具本身更值得带走:
你现在的门禁里,有哪几条规则,是从来没有人试过把它弄坏的?
参考:仓库 github.com/simin-yuan/self-auditing-agent(MIT 许可,Python ≥3.9,零依赖);未修复缺陷的完整诊断见仓库内 docs/BLIND-SPOTS.md。
- 点赞
- 收藏
- 关注作者
评论(0)