我让 AI 搭一个 Markdown 网站的自动发布流程。第一版 CI 全绿,15 项测试通过,MkDocs 也构建成功,但我仍没有让它上线。PR 审查发现:需求只允许发布专题目录的直接子文件 ,准备脚本却递归收集 Markdown,连产物审计也放行更深的路径。若将来出现 专题/草稿/未审核.md,它可能跟着构建产物进入公开网站。
这不是一个"测试没跑"的故事。代码运行了,测试也跑了;问题是它们共同验证了一个比需求更宽的范围。修正边界并补上反向用例后,17 项测试通过,但线上发布仍待单独验收。这个案例想回答的是:AI Coding 交付时,怎样判断它实现的是需求,而不只是实现了一套自洽的代码和测试?
任务很简单,边界不能含糊
这条流水线的路径是:
text
GitHub Markdown → MkDocs → GitHub Actions → 网站
源仓库是私有的,网站却可以公开访问。需求允许四个指定的根目录页面,以及 记录/*.md、专题/*.md。这里的 * 只代表目录的第一层文件;专题/草稿/未审核.md 不属于白名单。仓库里"存在"一篇文章,与它"获准公开",是两回事。
AI 交付了内容准备脚本、产物审计、构建工作流和测试。第一轮 Actions 的构建审计任务成功,15 项测试全部通过,MkDocs 严格构建也成功。单看绿色状态,这份交付很像已经满足要求。
为什么全绿仍然错了?
把需求、实现和测试放在一起,偏差就很清楚:
text
需求:专题/*.md
只允许专题目录第一层 Markdown
初版实现:递归遍历专题目录
专题/草稿/未审核.md 也会被收集
初版测试:覆盖允许文件、目录外文件和符号链接等情况
没有"专题下的嵌套 Markdown 必须被排除"这一反例
第一版准备脚本用的是 os.walk(base, followlinks=False),会继续走进子目录。审计脚本则用 source.startswith("专题/") 加 .md 后缀判断来源。两处都没有表达"路径恰好只有两层"这个要求。于是 专题/草稿/未审核.md 在准备阶段能被收集,在审计阶段也能被接受。
问题有三层。第一,自然语言里的"专题目录下的 Markdown",若不明确写成 专题/*.md 且注明不递归,很容易丢掉目录深度这条边界。第二,递归遍历是常见写法,但在本任务中扩大了可发布范围;至于 AI 为什么选它,现有记录不能证明其内部原因。第三,初版交付的测试验证了"该进来的能进来",却没验证"看似属于专题、实际越界的不能进来"。代码和测试因此可以一起通过,而需求仍未满足。
这里的"草稿"文件是反向用例,不是已经被公开的真实文章。PR 自审在部署前发现了问题,没有证据表明未审核稿曾被发布。
修改代码,更要修改验收方式
修正后的准备脚本只看目录的直接子项:
python
for source in base.iterdir():
if source.is_symlink():
relative = source.relative_to(root).as_posix()
raise SourceBoundaryError(f"symlink input is not allowed: {relative}")
if source.is_dir() or not source.name.lower().endswith(".md"):
continue
relative = source.relative_to(root).as_posix()
found.append((_resolve_regular_file(root, relative), relative))
iterdir() 遍历第一层,子目录被跳过。这样 专题/草稿/未审核.md 在内容准备阶段就不会进入站点输入。
但只修准备脚本还不够。以后若代码回退或另一个入口写错,产物审计仍应独立拒绝越界路径。修正后的审计条件是:
python
direct_child_markdown = (
len(path.parts) == 2
and path.parts[0] in {"记录", "专题"}
and path.suffix.lower() == ".md"
)
专题/文章.md 是两段,可以通过;专题/草稿/未审核.md 是三段,必须拒绝。相比只检查 startswith("专题/"),这段代码把需求中的目录深度落实为可检查的条件。
随后补了两处反向测试:一处在临时仓库创建嵌套稿,确认准备脚本没有复制 它;另一处故意把同一路径写进产物清单,确认审计拒绝接受它。关键断言是:
python
copied = stage_sources(self.root, output, manifest)
self.assertNotIn("专题/草稿/未审核.md", copied)
正向测试证明允许的内容能进去;反向测试证明不允许的内容进不去。两者缺一,白名单就没有被完整验收。修正后的 PR 构建运行了整套 17 项测试,并通过 MkDocs 严格构建、SHA 审计和静态产物上传。17 项是全部测试数,并非 17 项白名单测试。
我从这次交付提炼的四层验收顺序
这个顺序来自本次案例,用于检查类似的 AI Coding 交付;它不是一套已在所有项目验证过的标准。
- 需求边界 :把允许和禁止的输入都写成样例,例如
专题/文章.md可以发布,专题/草稿/未审核.md不可以。 - 反向测试:专门构造"差一点就合法"的输入,确认它被拒绝,而不只测试正常路径。
- 产物审计:检查最终清单和生成物是否仍满足边界。准备脚本正确,不等于后续环节永远正确。
- 真实环境验收:构建成功后,核对部署版本、提交 SHA 和网站实际内容,才能说线上完成。
本例目前通过的是前三层对应的 PR 阶段检查。部署步骤在 PR 运行中被跳过,PR 尚未合并,新版本也未完成线上验收。因此结论只能是:越界实现已修正,构建产物在 PR 阶段通过检查;自动发布到网站尚未验证完成。
AI Coding 最大的问题未必是代码写错,而是它可能完整地实现了一个"理解错的需求"。所以 AI 交付后,我现在最先补的不是更多正向测试,而是需求边界、反向用例,以及对最终产物的独立审计。