三个不同代码版本分别通过 8/8、10/10、12/12 顶层测试;独立审查发现四类缺陷,最终四个额外探针均被拒绝,结论仅限离线。
三个代码版本的测试结果与后续独立审查。绿色不代表生产放行。

现在写代码,很多时候已经不是古法手搓,而是当下流行的 Vibe Coding。许愿、抽卡,LLM 很快就能吐出一大堆格式工整、看起来很像回事的代码。那一刻很容易觉得自己是运筹帷幄的指挥官:手下突然来了几个脑子灵、出活快的实习生,需求刚说完,代码就交上来了。可这些交付你又不敢完全放心。等到 Review 跨文件 diff,指挥官立刻变回“古法手工核对员”,一行行确认改了什么、有没有漏掉边界。活儿是做快了,最后签字放行的人反而更累。

真正卡住的不是代码产量,而是验收能力没跟着产量一起增长。模型解释得再顺、测试全绿,甚至另一个模型给出 LGTM,也不能单独证明实现能安全进生产。

我想解决的就是这个落差:不靠把每行 diff 都从头读完,也不把“模型说没问题”当证据,能不能建立一条可复验的验收线?先说清代码必须满足什么,再用反例和执行结果去戳那些最容易被忽略的边界。

这次复盘的是我为这个博客程序的动态终端 SVG 编写的构建前校验器。脚本要检查三件事:SVG 是否符合限定的 XML/CSS 规则,资源清单(manifest)中的路径和 SHA-256 是否对应真实文件,以及八种语言的文章是否真的引用了可见的演示图片。canonical SVG 是一份固定的合法样本,用来确认新增检查没有把正确输入也拒绝掉。四类问题都在合并前的隔离测试中发现,范围只覆盖这个校验器的离线检查。

原有测试 8/8 通过,独立审查仍找到两种错误接受;第一轮修复后 10/10 通过,第二轮又找到另外两种。最终固定版本的主测试套件 12/12 顶层测试通过;独立审查者随后又单独执行四个攻击探针,全部被拒绝。另有三个 JavaScript 文件通过了 node --check 语法解析。

我会把这条验收线拆成五步:

  1. 固定范围和不变量。 记录目标 commit、改动范围,以及不能被破坏的行为。
  2. 为关键不变量构造最小反例。 尽量只改变一个条件,同时保留合法输入、文件摘要和其他外围校验。
  3. 把反例固化成回归测试。 如果实际在修复前运行并观察到目标断言失败,才能记录为 RED;没有这份运行记录,就不搞“先射箭后画靶”,更不事后补写一张漂亮的成绩单。
  4. 在精确版本上独立复验。 重新核对 commit,重跑正例和负例,再看完整相关测试,而不是只相信开发者报告的 GREEN。
  5. 按风险决定是否放行。 逻辑测试不能替代构建、目标平台或运行态检查。关键边界仍未验证,就不能把局部通过说成整体生产就绪;正确性有了证据之后,再检查重复逻辑和不必要的复杂度。

不必每个小 PR 都填一份长表。跨模块改动,或涉及安全、权限、文件系统、数据迁移等高风险边界时,可以把关键证据记成一张小卡片:

变更范围 / 风险:
Base commit -> Head commit:
必须成立的不变量:
合法正例:
最小反例 / 预期拒绝:
实际命令、输出与运行环境:
仍未验证的关键边界:
结论:已验证范围内放行 / 需修复 / 阻断

低风险改动可以裁剪字段,但至少要能回答:改了什么、用什么证明它按预期工作、还有什么没验证。本文重点是方法和可复制的记录卡;校验器是博客仓库中的构建前组件,并非独立打包的通用 CLI。目前也没有测量这套方法能节省多少 Review 时间。下面的案例展示的是它如何帮助发现错误假设。

公开仓库只包含脱敏后的合成反例与可运行教学测试,不包含这个博客程序的生产校验器、生产修复或内部架构。文中的 8/8 → 10/10 → 12/12 是原工程离线验收的结果;公开仓库只复现四类缺陷模式,不复刻原有生产实现。

脱敏合成示例与可复验测试

8/8 通过,为什么还会接受错误输入

初始测试覆盖了合法样本和一批恶意输入,但没有覆盖所有条件组合。测试全绿,只能说明当前测试集满足了当前断言;它不能自动证明所有不该通过的输入都会被拒绝。

第一个反例是一行混合了合法图片和未获准的跨文章资源引用。旧逻辑只要在这一行发现一个允许的 <img>,就像听到一句“来都来了”,直接给整行放行;后面还夹带了什么引用,它没再查。

独立审查在隔离的临时 checkout 中,先创建了一个真实存在的、属于另一个文章 slug 的 SVG 文件,再把指向它的 Markdown 引用追加到八种语言文章里原本合法的 <img> 同一行。创建目标文件,是为了排除“引用文件不存在,所以检查才失败”这种干扰。旧实现仍返回成功,报告 posts_checked=8。

示意输入如下,省略了实际摘要值和部分属性:

<img src="/assets/terminal-demo.svg"
     alt="Terminal demo" width="356" height="137" /> ![foreign](../foreign/terminal-demo.svg)

前半段图片合法,不代表后半段的另一条引用也合法。修复后的逻辑验证唯一允许的图片后,还会检查剩余内容中的引用,而不是因为同一行出现了合法标签就放过整行。

第二个反例来自文件系统:检查过的路径,不一定还是最后打开的那个文件。

独立测试在真正的打开文件边界模拟父目录替换:仓库内原路径保留错误字节,目录外则放着内容和预期 SHA-256 完全相符的 canonical SVG。父目录被替换成指向外部目录的符号链接后,旧实现读到了目录外的正确文件并错误接受。

这个细节很重要:被绕过的是“文件必须来自可信 checkout 内”的来源约束,不是内容摘要校验。 外部文件的内容本来就是正确的,因此只验证哈希抓不住它。

最终实现的 Linux 分支先打开可信 checkout 根目录,再逐层从已打开的父目录文件描述符定位下一层。中间组件要求是目录并使用 O_NOFOLLOW,最终文件也拒绝符号链接;随后用 fstat 检查实际打开的文件类型、链接数和大小,并在读取前后比较设备号、inode 和大小,最后另行验证文件摘要。这样,路径名即使被换成指向外部目录的符号链接,打开操作仍相对于已经固定的父目录句柄进行。

为什么没有直接调用 openat2?当前实现只使用 Node 内置的 fs API,fs.openSync 没有暴露 Linux openat2 的 RESOLVE_* 路径解析参数。直接调用需要额外的 native binding 或系统调用封装;这份实现选择了 Linux 的 /proc/self/fd 目录句柄方案,并在没有可用 procfs 时失败关闭。这是实现取舍,不是说它在所有威胁模型下都优于 openat2。已验证的范围依赖可信 checkout 根目录和可用 procfs,不涵盖任意同 UID 或主机级对手。Linux openat2(2) 文档解释了 O_NOFOLLOW 只影响最后一个路径组件,而 RESOLVE_NO_SYMLINKS 可以约束整条路径的符号链接解析。

这类并发写入防护也不该机械复制到每个构建脚本里。若构建环境能保证 checkout 不可变、校验期间没有并发写入者,可以把这项威胁排除在适用范围之外;关键是明确并落实前提,而不是默认假设成立。

第二轮:哈希没错,内容也可能不可见

第一轮修复后,测试增加到 10/10 通过。独立审查继续追问另外两个问题。

第三个反例针对减少动态效果时的最终画面。这个 SVG 用多个内容帧逐步呈现终端输出;用户开启 reduced motion 后,动画可以停止,但最终结果仍然必须可见。

基础规则让所有帧默认透明:.f{opacity:0;...}。正常动画负责逐步揭示它们;如果 reduced-motion 规则只停掉动画,却没有显示最终帧,结果就会空白。下面是省略无关动画声明后的示意:

/* 基础规则:所有 .f 帧默认透明 */
.f { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  #f0 { animation: none; opacity: 1; } /* 空的首帧被显示 */
  #f3 { animation: none; }            /* 仍继承 opacity: 0,最终帧隐藏 */
}

测试不只是改 CSS 后看哈希检查会不会报警。它还重新计算 SVG 的完整 SHA-256、字节数、内容寻址文件名和 canonical manifest,确保外围校验仍然合法,真正被测的是校验器有没有理解“最终结果必须看得见”这条契约。

MDN 对 prefers-reduced-motion 的说明是检测用户是否偏好减少非必要动态效果。用户只是说“别晃了”,不是让你关灯;停止动画不能顺手把最终内容也藏起来。

第四个反例来自 Markdown。校验器需要确认文章里有一张可见的终端演示图片,但旧实现用正则计数 <img> 标签,没有排除 HTML 注释。把唯一图片包进注释:

<!-- <img src="/assets/terminal-demo.svg"
     alt="Terminal demo" width="356" height="137" /> -->

标签文字还在,页面却不会显示图片;其他合法 marker 和 caption 仍然可以保留。旧逻辑仍把它计为一张图。CommonMark 0.31.2 的 HTML block 规则明确规定,注释块里的标签不会成为可见图片。问题在于校验器把“源文本出现标签”误当成“页面存在可见元素”。

首轮修复后的 10/10 测试通过后,独立审查仍发现 HTML 注释内的图片被误计为可见图片;最终 12/12 通过,四个独立探针被拒绝。
测试全绿也会漏掉反例:注释里的图片标签不等于页面可见图片。

把测试结果绑定到精确版本

把几个版本放在一起,才看得清每个数字证明了什么。

代码版本主测试套件结果随后的独立审查
原始版本8/8 通过SEC-01、SEC-02 两类错误接受
第一轮修复版10/10 通过SEC-03、SEC-04 两类错误接受
最终离线验收版12/12 顶层测试通过四个独立攻击探针另行执行,全部拒绝

最终版本的 12 个顶层测试中,包含 SEC-01 至 SEC-04 对应的回归测试组。独立审查者还使用另一份审查脚本,额外执行了四个攻击探针;这四次独立复验不计入 12/12。它们是两组互相补充的证据,不应相加写成 16/16,也不能把四个独立探针说成主测试套件的全部覆盖。

另有三个 JavaScript 文件(校验器模块、测试文件和命令行入口)通过了 node --check。这是语法解析检查,不能替代功能测试或安全探针。

还有一个过程细节值得保留:后两类回归测试的提交早于源码修复,但完整测试套件没有在那个“只有新测试、还没改源码”的提交上运行。因此,没有真实的 10/12 RED 运行回执。可以核对的是:独立审查在旧版本的完整校验器中复现了错误接受,最终固定版本的主测试套件和额外的独立攻击探针都通过了。

这也是为什么独立 Review 不能只看开发者贴出的绿色结果。审查者需要重新核对目标 commit 和测试文件,自己重跑关键正例与负例;如果代码版本变了,旧版本的结论不能自动继承到新版本。

正确性有了足够证据后,才轮到复杂度审查。逐个追踪 helper、分支、配置和调用点,确认它们保护什么不变量、是否重复实现、有没有实际消费者,再判断能否安全简化。代码短不等于简单;删掉一个看似啰嗦的路径保护,也可能把已经关闭的竞态重新带回来。

交互式证据面板

选择一个合成反例,查看输入、不变量、复现的失败与证据边界。

SEC-01 · 存在不等于排他

输入条件
允许图片与未授权跨文章资源引用同时出现。
应守住的不变量
每个相关引用都必须满足规则;找到一个合法引用不代表整行安全。
合成示例复现的失败
旧逻辑发现允许图片后就放行整行,漏掉额外引用。
预期正确行为
逐一检查所有引用;任何越界引用都应使输入被拒绝。
原工程发现轮次
第一轮独立审查发现
可迁移的审查经验
构造合法与非法内容并存的最小反例,确认规则检查完整输入。

SEC-02 · 检查与使用竞态

输入条件
路径通过检查后,在打开前替换父目录或符号链接。
应守住的不变量
被验证对象必须与实际访问对象一致,并保持在预期文件系统边界内。
合成示例复现的失败
检查路径后再按路径打开,可能让目标在两步之间被替换。
预期正确行为
验证与访问必须守住边界;合成例不代表已实现生产修复。
原工程发现轮次
第一轮独立审查发现
可迁移的审查经验
检查验证与使用之间是否存在可被并发改变的间隙。

SEC-03 · 减少动态效果仍须显示终帧

输入条件
SVG 帧默认透明;启用减少动态效果后动画停止。
应守住的不变量
关闭动画不能让关键最终内容消失。
合成示例复现的失败
只停用动画却不显示终帧,结果可能空白。
预期正确行为
减少动态效果模式也应直接呈现完整可读的最终状态。
原工程发现轮次
第二轮独立审查发现
可迁移的审查经验
将无障碍偏好视为独立输入,检查内容是否仍可见。

SEC-04 · 注释不是可见内容

输入条件
HTML 注释中包含看似有效的图片标签。
应守住的不变量
源码字符串与浏览器实际渲染的可见元素不同。
合成示例复现的失败
简单统计标签可能把注释里的图片误算为可见图片。
预期正确行为
解析结构并排除注释节点,只判断真正渲染的元素。
原工程发现轮次
第二轮独立审查发现
可迁移的审查经验
把伪造标签放进注释,验证检查器理解结构而不只是文本。

公开合成教学证据

公开仓库只复现合成缺陷模式;测试成功代表复现成立,不代表生产修复通过。

查看固定版本的合成教学仓库

原工程离线摘要

原工程记录:8/8 → 10/10 → 12/12。四个独立探针单独执行,不计入 12/12。

验证边界

范围仅限 Node.js v22.16.0 / Linux x64 离线校验。macOS 恶意并发文件系统写入和全站运行态仍未验证。

站内另外三篇文章分别讨论了相邻问题:Mutation Testing 检查测试能否抓住人为注入的行为变化;OpenQodex 的 pre-push 审阅失败边界 分析扫描、独立审阅与推送门禁的区别;Diffle 的评论锚点 讨论代码变化后审阅意见是否仍指向正确位置。本文的重点则是:实现本身究竟接受了什么、拒绝了什么。

从测试通过到生产放行,还差什么

最终独立复核覆盖的是 Node.js v22.16.0、Linux x64 下的离线校验脚本:主测试套件 12/12 通过,额外四个独立攻击探针全部被拒绝,合法 SVG、manifest 和八语言绑定正例通过;三个 JavaScript 文件也都通过了 node --check。

这足以支持一个有边界的结论:在上述环境和固定校验规则下,四类已知错误接受得到了修复。它仍不能单独证明整个博客可以发布到生产:原生 macOS 上的恶意并发文件系统写入尚未验证,整个站点的构建和最终页面运行验收也不在这次离线测试范围内。

真实项目的放行线应随改动风险分层:单元测试检查被覆盖的行为,静态检查寻找特定风险模式,构建和集成测试确认模块能否正确组合,目标平台与运行态验收则检查部署环境中的关键边界。只要一个关键风险边界仍未验证,就不能把局部通过写成整体生产就绪。

我真正想保留的结论很简单:先用契约和反例找出最值得怀疑的行为,再让精确版本上的测试和独立证据决定下一步该深读哪里。 这样做不是取消人工判断,而是让 Review 的结论从“看起来没问题”变成有证据、有范围、能复核的放行决定。