test-suite-doctor 是做什么的?
将臃肿的测试套件(例如 2000+ 个测试,其中大量是 AI 生成的垃圾代码) 精简为一个小而精的高质量套件,同时保留可配置比例的真实覆盖率—— 基于实际测量,而非猜测。
硬性规则
- 先测后判,数据说话。 在
collect和minimize产生计划之前,永远不要提议删除任何测试。仅凭阅读测试代码就判断其“无用”不是证据——一个看起来微不足道的测试可能是某个分支的唯一覆盖。 - 计划只是提案,不是最终判决。 覆盖率无法感知 API 契约、有文档记录的回归用例或属性不变性。每个删除候选都需要经过审核(步骤 3)才能删除。
- 在分支上操作。 在删除任何内容之前,确保工作树是干净的,并创建一个分支(例如
test-doctor/minimize)。切勿在脏工作树或默认分支上删除测试。 - 验证或回滚。 只有
verify退出码为 0 才算任务完成。如果验证失败,则为报告出的缺口重新生成测试并重新验证——或者恢复已删除的测试。
先决条件检查(请先执行此操作)
- Node.js ≥ 22,并且
package.json中使用本地安装的 Vitest 或 Jest(node <skill-root>/dist/cli.mjs collect --help)。 - 必须安装覆盖率提供程序(Vitest:
@vitest/coverage-v8; Jest 内置一个)。如果缺少,主动提议将其安装为 devDependency。 - 变异测试验证额外需要目标仓库中已配置好
@stryker-mutator/core——这是可选功能,未经用户同意请勿安装。 - 开始前测试套件应是绿色的。如果基准运行报告有失败,告知用户并就如何处理它们达成一致后再进行最小化(失败的测试会提供不可靠的覆盖率)。
以下所有命令都在目标仓库根目录下运行;$SKILL 是该技能的目录。
产物会存放在 .test-doctor/ 目录下(建议将其加入 .gitignore)。
工作流程
1. 收集(脚本——始终首先执行)
node "$SKILL/dist/cli.mjs" collect --cwd .
检测测试运行器,先完整运行一次整个套件作为覆盖率基线,然后
单独重新运行每个测试文件,记录覆盖的行数/分支和执行时间。
写入 .test-doctor/report.json。
- 默认粒度是每个测试文件(按测试隔离是精确的,但会
单独运行每个测试——仅对小型或已精简的套件,或使用
--filter限定范围时,才提供--granularity test选项)。 - 对于 2000 个测试的套件,这需要一段时间;请在后台运行并报告进度。
认真对待报告中的
collectionErrors——这些单元默认会阻止最小化。 如果用户明确选择--keep-unmeasured,则每个受影响的单元都是强制保留的,并且计划会被标记为不可信。 --filter会创建一个有意限定了范围的基线:收集、最小化 和验证仅覆盖匹配的测试文件。在每个建议中说明这个范围; 它不能保证整个仓库的覆盖。
2. 最小化(脚本)
node "$SKILL/dist/cli.mjs" minimize --coverage-floor 0.97 --target-count 200
贪心加权选择:重复保留每毫秒运行时覆盖最多新行/分支的单元,直到达到覆盖率下限。写入 .test-doctor/plan.json 和人类可读的 .test-doctor/plan.md,其中包含每个保留和删除的单元的理由。
- 如果用户未指定,请询问他们的阈值;默认值是 0.97 的行覆盖率下限,
无分支覆盖率下限,无目标计数。
--help列出所有参数。 - 预先强制保留已知的契约/回归测试:
--keep 'contract' --keep 'regression'(基于单元 ID 的正则表达式)。 - 注意计划的
warnings数组——例如无法达到下限,或 需要的单元数超过目标计数。将警告逐字传达给用户。
3. 审核(判断——由你完成)
阅读 references/slop-patterns.md,然后审核 plan.md 中的删除列表:
- 拯救任何验证覆盖率无法感知的行为的测试:公共 API
契约、引用问题/错误 ID 的回归测试、消费者依赖的错误消息措辞、
属性/不变性检查。使用额外的
--keep模式重新运行minimize,而不是手动编辑计划。 - 确认其余的都是安全的删除项;按垃圾模式归类它们,这样用户就能看到为什么每个测试被删除(模式文件中有分类目录)。
然后对照 references/quality-rules.md 审核保留列表:
保留的测试中,看起来有问题的(mock 嵌套 mock、同义反复、空洞快照)会在原地重写——
它们赢得了覆盖率,但应该以干净的方式赢得。
仅在完成此审核后,才在分支上删除已丢弃的测试文件/用例。 当某个文件中的所有单元都被删除时,删除整个文件;当只有文件中的部分测试被删除时,编辑文件(在测试粒度下)。
4. 重新生成(判断——由你完成)
先运行 verify 一次(预期可能会失败)以获取覆盖率缺口报告,然后
为 verify 列出的受影响最严重的源文件编写新的测试,遵循
references/quality-rules.md:AAA 结构、行为驱动命名、每个测试一个行为、
仅在外部边界使用 mock。一个覆盖真实用户可见行为的强测试优于三个浅层测试。
5. 验证(脚本——作为完成的关卡)
node "$SKILL/dist/cli.mjs" verify --coverage-floor 0.97
# 可选,仅对关键模块启用(较慢):
node "$SKILL/dist/cli.mjs" verify --mutation --mutate "src/billing/**/*.ts" --mutation-floor 80
重新运行套件,要求为绿色,并要求与基线相比的行保留率满足下限。 退出码 0 = 完成;退出码 1 = 修复并重新运行(重新生成更多测试,或恢复删除); 退出码 2 = 环境问题,停止并报告。变异测试是可选的,因其较慢—— 仅在用户认为关键的模块上提供。
版本 2 会为已覆盖的源文件、测试运行器配置、锁文件、运行器
和覆盖率提供程序生成指纹并与基线比对。如果 verify 报告来源偏移,
则重新收集基线。仅在有明确用户同意的情况下使用 --allow-provenance-drift,
并将结果判定报告为不可信。旧的 v1 基线同样需要显式的 --allow-legacy-baseline 逃生舱。
报告
为用户总结:单元数(精简前 → 精简后)、行/分支保留率、运行时间(精简前 → 精简后)、发现的垃圾模式(含计数)、从删除列表中拯救的测试及原因、重新生成的测试以及最终的验证结果。包含 plan.md 的路径以提供完整的审计跟踪。
失败模式
- 未检测到运行器 / 不明确 → CLI 退出码为 2 并附带指导信息;传入
--runner或--cwd(对于 monorepo:在每个包中单独运行)。 - 收集不完整 → 修复孤立的失败。不要仅仅为了生成一个通过的计划就将它们过滤掉;过滤器会改变声明的基线范围。
--keep-unmeasured是保守的逃生舱。 - 重新生成后验证仍然失败 → 在大约 3 次尝试后停止循环; 按文件呈现剩余的缺口,让用户决定是降低下限还是保留更多测试。
- 开始前套件是红色的 → 不要默默地“修复”无关的失败; 先报告它们并就范围达成一致。