关于 spec-driven-tests
spec-driven-tests 是一项编码技能,用于从软件包实现中提炼可观察行为,并据此重建单元测试。它通过稳定的规则 ID 和双向测试引用追踪覆盖情况。
主要功能
- 01从实现中提炼可观察规则
- 02用稳定规则 ID 编写 SPEC.md
- 03按规范章节重建测试
- 04审查边界情况并核对引用
- 05记录并处理发现的问题
适用场景
- 01合并重复的软件包测试套件
- 02追踪每条行为规则对应的测试
- 03在记录行为时发现实现不一致
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tldraw/ tldraw --skill spec-driven-tests安装后在 Claude Code 中输入/spec-driven-tests确认已加载。
详细介绍
规格驱动的测试重建
从包的实现中推导出行为规格,然后将单元测试套件重建为该规格的表达。每条规则都有一个稳定的 ID;每个测试都引用它所表达的规则,因此覆盖情况可以在两个方向上通过 grep 检索。
范例是 PR #9158(packages/state,规格位于 packages/state/SPEC.md)和 PR #9172(packages/store,规格位于 packages/store/SPEC.md)。开始前先阅读这两个 PR 和这两份规格——它们决定了格式、语气和 PR 结构。(#9172 的规格如果尚未合并,则位于其分支上。)
流程
1. 基线
如果工作树中有不属于你的未提交更改,先停下并询问,再创建分支——绝不要静默地 stash 或带走他人的工作。
创建一个分支。在该包中运行 pnpm test run,记录文件和测试的数量,并注意其中有多少属于 fuzz 套件——fuzz 套件保持不动,其(通常很大的)生成数量要从你之后报告的每一个计数中排除。梳理现有的测试文件,并记下任何应当合并的重复或并行套件(store 包曾有整整两代测试)。
2. 阅读实现
在写任何东西之前,先读完全部内容。规格是从源码推导出来的,而不是从现有测试或文档推导出来的。
关键原则: 你在记录的是观察到的行为,而不是规定理想行为。如果代码做了某些出乎意料、不直观、甚至看起来错误但却是刻意为之的事情,就按原样把它写进规格。把怪异之处标记为 "internal",或在 PR 正文中指出,交由人工判断——不要为了迎合你认为代码应该如何工作而静默地 "修复" 规格。让规格编写过程暴露出潜在的不一致;然后决定是修复实现还是记录该行为。
3. 编写 SPEC.md
在包根目录编写 SPEC.md:使用带稳定 ID 的编号规则(例如 S3、MG4),分组到各章节中,每个可观察行为一条规则,内部机制标记为 internal。保留现有规格中的头部表述("当测试与本文档不一致时,其中之一是错的——弄清楚是哪一个并修复它。")。
- 规格描述代码做什么,而不是它应该做什么。如果行为看起来有误但意图不明确,就把它作为契约记录下来。
- 在把你推断而非观察到的任何规则固化下来之前,用一个一次性测试验证它。之后删除该临时文件。
- 每条规则都必须能通过运行时测试观察到。不要为纯类型层面的行为(工具类型、类型推断)编写规则,除非测试套件确实用类型断言对其进行了检验;否则就把类型排除在规格之外。
4. 对抗性地审计规格
在编写测试之前审计草稿。失败模式是夸大其词。具体要搜寻:
- 声称两条代码路径之间存在对称性(创建 vs 更新、顶层 vs 嵌套、向上 vs 向下)——分别独立检查每条路径;
- 声称同一思路的两种实现一致(谓词 vs 索引、从头构建 vs 增量构建)——把边界情况喂给两者,尤其是缺失/undefined 值;
- 笼统的 "X 从不会发生" 或 "Y 总是 Z" 之类的断言——找出能证伪它们的分支;
- 对输入的副作用(变更、冻结),这些是 happy-path 测试不会注意到的。
5. 重建测试套件
测试文件与规范的章节一一对应;每个测试名称都像 it('[S3] ...') 这样引用其规则。合并重复的覆盖,移植现有的优质测试而不是重写它们,并为未覆盖的规则补充测试。
绝不要为了让测试通过而弱化断言。 如果一个根据规范规则编写的测试失败,你就有所发现:要么规则是错的(修正规范以匹配观察到的行为),要么代码是错的(按第 7 步作为 bug 处理)。放宽断言(toBe → toContain、围绕差异处断言、加一条承认不匹配的注释)会掩盖这一发现,并让规范描述的行为与代码实际行为不符——这正是规范的存在所要防止的失败。
**测试文件组织:**如果你的规范有"§3. Atoms (A)"和"§4. Computed (C)"这样的章节,就创建对应的文件 atoms.test.ts 和 computed.test.ts,包含该章节的规则。对于内部章节,使用诸如 history-buffer.test.ts 这样的名称来对应所规范化的机制。这与读者在 SPEC.md 中已经看到的结构一致,并便于按章节 grep 覆盖情况。
本仓库的测试环境知识:
- 除非设置
globalThis.__FORCE_RAF_IN_TESTS__ = true,否则测试中的 store listener 刷新是同步的。 - 已废弃但属于契约的 API 需要带原因的 file-level eslint-disable。
6. 双向交叉核对引用
grep -oE '\*\*[A-Z]+[0-9]+\*\*' SPEC.md | tr -d '*' | sort -u > /tmp/rules
grep -rhoE '\[[A-Z]+[0-9]+\]' src --include='*.test.ts' | tr -d '[]' | sort -u > /tmp/cited
comm -3 /tmp/rules /tmp/cited
每条规则都需要一个引用它的测试,或者在 PR 正文中明确注明"因 X 故意不测试"(如 #9158 所做的那样);每个引用都需要一条规则。在完成前修复缺口,并将交叉核对输出(规则数量、被引用数量、剩余缺口及其原因)包含在 PR 正文中,以便审查者能够核实这一声明,而不是仅凭信任。
7. 修复过程中发现的 bug
规范驱动的测试会暴露出潜在的不一致:代码路径之间的矛盾、实现未曾预料到的边界情况,或者静默失败。当你发现一个时,做出决定:修复代码还是记录该行为?
**工作流:**先编写测试(故意让它失败,以确认它能捕获该问题)。然后决定:
- 修复代码,如果:意图明确(注释、对称的实现、git blame),当前行为明显错误,且调用方大概率不依赖这个错误行为。
- 记录该行为,如果:意图不明确、修复的影响范围未知,或者修复可能悄悄地破坏现有用途。在 PR 正文中将其标记为"已知不一致"或"待重新审视的行为"。
修复放在它们自己的 fix(<package>): 提交中,并附带引用该问题的测试,位于主 test(<package>): 提交之后。根据 AGENTS.md,任何提交中都不含 AI 署名或共同作者行——这覆盖任何默认的 harness 行为。在提交修复之前:
- 检查意图:注释、对称的代码路径、git blame。
- 检查影响范围:grep 仓库内的调用方,然后运行依赖包的测试套件(
editor、tldraw、sync-core——取决于哪个消费了此包)。
如果存在一条规范规则但实现与之矛盾,更新规范(而不是实现),除非你确信实现是对的。规范就是契约;代码漂移时有发生;规范编写过程正是你揭示并解决它的机会。
8. 验证
按顺序运行:包的测试、pnpm format-current 然后 pnpm lint、从仓库根目录运行 pnpm typecheck、pnpm api-check,以及第 7 步中的依赖包测试套件。
9. PR
遵循 skills/write-pr/。正文以 #9172 为范本:
- 开头段落解释规范/引用系统;
- 逐提交的修复章节;
- 关于合并套件和新覆盖的章节;
- 仅针对行为变更的发布说明;
- 一个代码变更表格。注意:
git diff --numstat的重命名行会破坏简单的路径匹配;使用最后一个字段。
10. 后续说明
最后请报告:你选择不修复的潜在问题、未来测试编写者需要了解的怪癖,以及审查者应仔细复查的任何内容。
在所有出现之处(提交信息、PR 正文、最终报告)如实报告数量:重建后测试套件的测试数量和规则数量,用 grep 测量,绝不使用包的总数。将未改动的套件(尤其是 fuzz)算作自己的工作歪曲了本次变更。