> 断言"它拒绝了"不能用非零退出码——任何东西都会非零退出
一个项目里有一组"必须失败"的门禁:某工具在没有后端时必须拒绝跑,画图脚本在没有结果时 必须拒绝编数据。它们的断言写成了 exit != 0。
结论先行¶
一个项目里有一组"必须失败"的门禁:某工具在没有后端时必须拒绝跑,画图脚本在没有结果时
必须拒绝编数据。它们的断言写成了 exit != 0。
在这个项目自己的工作站上,画图那条门禁在一个 ModuleNotFoundError 上绿了好几周。
因为非零退出码是个大杂烩:缺一个 Python 模块、崩溃、命令打错字,全都非零。 每一种都能让一条名叫"拒绝编造数据"的门禁通过,而那段拒绝逻辑从来没有运行过。
修法是让门禁去匹配拒绝的原话:
refused() {
# refused <exit code> <captured output> <phrase>
[ "$1" -ne 0 ] && printf '%s' "$2" | tr -s '[:space:]' ' ' | grep -qF "$3"
}
GATE_OUT=$(./titans_llm_experiment --backend vllm --port 59999 2>&1)
check "refuses to run with no backend" \
refused "$?" "$GATE_OUT" "will not fall back to a stand-in"
tr -s '[:space:]' ' ' 把空白压平,这样把提示信息重新折行不会让门禁变红——
同一个项目的 CI 曾经因为相反的原因红过:它逐行 grep 一条被折行的信息。
为什么¶
这件事有三层,每层都踩过:
一、退出码的语义空间不能让"工具坏了"和"工具正确拒绝"撞车。
同一个项目里 generate_figures.py 缺 seaborn 的退出,和它按设计拒绝的退出,
对调用方完全不可区分。所以第三种状态是必要的:
if printf '%s' "$GATE_OUT" | grep -q 'ModuleNotFoundError'; then
# 跑不起来,所以无法判断它会不会拒绝。报告出来,
# 既不算通过也不算失败——在这里打印 "ok" 正是这段重写要去掉的缺陷。
printf ' skip %s -- cannot verify on this host: %s\n' ...
else
check "refuses to fabricate data" ...
fi
二、成功的退出码可以就是那个失败。同一个工具在"发布一个决策"模式下必须违反
新鲜度 SLO 并以 5 退出;它要是退出 0,意味着那个契约不再被执行了。
所以脚本断言的是 test $? -eq 5,不是 -eq 0,也不是 -ne 0。
三、"点数太少无法判断"必须是拒绝,不能是通过。这两者从退出码 0 看过去一模一样,
所以也单独断言成 -eq 2。
把退出码设计成语义的(0 跑了 / 1 数据错 / 2 用法错 / 3 协议违反 / 5 SLO 破了 / 6 回退), 门禁才有东西可断言。代价是:改任何一个都会弄坏构建——这正是你想要的那种代价。
怎么用¶
给每条"必须失败"的断言问三个问题:
- 这条断言能被一次崩溃满足吗? 能,就把它改成匹配拒绝的原话。
- 拒绝的原话和"工具坏了"的输出能撞车吗? 能,就加第三种状态(skip), 并且明确记账为"未验证",不要记成通过。
- 这个工具正确工作时应该以几号退出? 写进文档表格,并让脚本断言那个具体的数,
不是
!= 0。
匹配原话时把空白压平(tr -s '[:space:]' ' ' + grep -qF),否则重排版就会误报。
一条判断"这类门禁是否已经坏掉"的捷径:永不触发的门禁和永远触发的门禁, 在绿色构建上看起来完全一样。见 测不出失败的门禁比没有门禁更糟——它在制造信心。
出处¶
~/Engineering/High-Performance-Hybrid-Trading-System/scripts/reproduce.sh第 151–185 行("Honesty gates (these MUST fail)" 一节)与第 366–378 行;refused()的实现、skip 分支的注释、以及-eq 5/-eq 2/-eq 6三处具体断言- 退出码的语义表:同项目
PROJECT_ENGINEERING_GUIDE.md§15.1(10 个工具 × 7 列) - 同一类失效的另一面:手写的自检清单永远是全绿的——它只包含你刚修过的东西、自审的失效模式不是"没发现",是"发现了没执行"