test-suite-curation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<objective>
Test A and Test B cover the exact same lines, so a tired engineer deletes B — and three weeks later a production defect slips through because B was the only test that asserted the rounding was correct. Coverage equality is not redundancy. This skill audits an entire regression suite as a corpus (the redundancy analysis no human does by hand), prunes it on evidence rather than vibes, and treats every deletion as a destructive change that requires a quarantine grace period, human sign-off, and a record you can defend to an auditor.
</objective>
<objective>
测试A和测试B覆盖完全相同的代码行,于是疲惫的工程师删除了测试B——但三周后生产环境出现了缺陷,因为测试B是唯一断言四舍五入逻辑正确的测试。覆盖率相等并不等同于冗余。本技能将整个回归测试套件作为一个整体进行审计(这种冗余分析无法由人工手动完成),基于证据而非直觉精简套件,并将每一次删除视为破坏性变更,要求设置隔离宽限期、人工签字确认,并保留可供审计的记录。
</objective>
Quick Route
快速导航
| You want to... | Go to |
|---|---|
| Find which lines/branches each test uniquely covers | Coverage Fingerprinting |
| Decide if "same coverage" means "delete one" | The Coverage-Equality Trap |
| Surface copy-pasted near-duplicate tests | Near-Duplicate Clustering |
| Find never-failing and always-flaky tests | Mining CI History |
| Decide redundant vs obsolete vs low-value vs keep | Prune Decision Rules |
| Split a flat suite into smoke/core/extended | Tiering |
| Safely delete the tests you flagged | Destructive Safety |
| Produce the "what we deleted and why" record | The Audit Record |
Discovery Questions
探索问题
First, check in the project root and skip anything it already answers. Then clarify:
.agents/qa-project-context.md- Which language/runner? pytest+coverage.py, Jest/Vitest+Istanbul, Go, JUnit — the per-test context mechanism differs per stack (and so does the mutation tool).
- Is there CI test-result history, and how far back? No JUnit/Datadog/Trunk history means you cannot mine never-failing or flaky signals — you only have coverage and clustering.
- What is the suite size and current wall-clock time? This sizes the tiering target (e.g. smoke under 5 min) and whether per-test coverage is feasible in one run or must be sharded.
- What is the business risk map / critical paths? Tiering and the "keep" disposition both depend on it. If absent, run first.
risk-based-testing - Who signs off on deletions, and is there a CODEOWNERS file? Deletion is destructive; you need a named approver before this skill removes anything.
- What is the acceptable observation window? How long the team will run quarantined tests as skipped before permanent removal (default: 2 sprints / 2 releases).
首先检查项目根目录下的,跳过已有的答案。然后明确以下内容:
.agents/qa-project-context.md- 使用哪种语言/测试运行器? pytest+coverage.py、Jest/Vitest+Istanbul、Go、JUnit——不同技术栈的单测试上下文机制不同(突变测试工具也不同)。
- 是否有CI测试结果历史,时长多久? 若无JUnit/Datadog/Trunk历史,则无法挖掘永不失败或不稳定的信号——仅能使用覆盖率和聚类分析。
- 测试套件规模和当前执行耗时? 这决定了分层目标(例如冒烟测试需在5分钟内完成),以及单测试覆盖率分析是否可一次完成,还是必须分片执行。
- 业务风险地图/关键路径是什么? 分层和“保留”判定均依赖于此。若不存在,请先运行技能。
risk-based-testing - 谁负责审批删除操作,是否有CODEOWNERS文件? 删除是破坏性操作,在本技能执行删除前,需指定审批人。
- 可接受的观察窗口时长? 团队将隔离测试标记为跳过,直到永久删除前的运行时长(默认:2个迭代周期 / 2个版本发布周期)。
Core Principles
核心原则
-
Coverage equality is not redundancy. Two tests hitting the same lines can assert completely different things — different oracles, inputs, edge cases. Line/branch coverage tells you what code ran, never what was checked. The only evidence that one test subsumes another is that the survivor catches the same faults, which you prove with mutation testing, not a coverage diff.
-
The agent's edge is whole-corpus analysis, not deletion authority. An agent can fingerprint 4,000 tests, cluster near-duplicates, and cross-reference CI history in minutes — work no human does by hand. That is the entire value. But the agent proposes; a human approves. Never let the corpus-scale analysis become corpus-scale auto-deletion.
-
Deletion is destructive and must be reversible in practice. "It's in git history" is not a recovery plan. Quarantine first (skip/xfail, or move to a deprecated suite), observe for a defined window, watch for escaped defects, then delete with sign-off. The grace period is the safety net, not the commit log.
-
Every disposition is differentiated. Redundant, obsolete, and low-value are three different states with three different actions. Collapsing them into one "delete" bucket is how you lose real coverage. A test that never failed is not the same as a test that cannot fail.
-
Evidence over intuition, recorded per test. Each removal carries its own row: category, the test that supersedes it, the coverage delta, who approved, and how to restore. If you cannot fill the row, you cannot delete the test.
-
覆盖率相等并不等同于冗余。 两个测试覆盖相同代码行,可能断言完全不同的内容——不同的验证逻辑、输入、边缘场景。代码行/分支覆盖率仅能告诉你哪些代码被执行,无法告诉你验证了什么内容。证明一个测试可替代另一个的唯一证据是,保留的测试能捕获相同的缺陷,这需要通过突变测试来证明,而非覆盖率差异对比。
-
智能体的优势在于全套件分析,而非删除权限。 智能体可在数分钟内为4000个测试生成指纹、聚类近似重复测试、交叉引用CI历史——这些工作人工无法手动完成。这就是其核心价值。但智能体仅负责提出建议;人工负责审批。绝不能让全套件规模的分析变成全套件自动删除。
-
删除是破坏性操作,必须具备实际可回滚能力。 “代码在git历史中”并非有效的恢复方案。应先隔离(标记为skip/xfail,或移至废弃套件),在设定的窗口内观察,监控是否有缺陷漏网,然后经签字确认后再删除。宽限期才是安全保障,而非提交日志。
-
每个测试的处置方式需区分对待。 冗余、过时、低价值是三种不同状态,对应三种不同操作。将它们归为“删除”一类会导致真实覆盖率丢失。永不失败的测试与不可能失败的测试并非同一概念。
-
基于证据而非直觉,为每个测试保留记录。 每一次删除都需单独记录:类别、替代它的测试、覆盖率变化、审批人、恢复方式。若无法填写完整记录,则不能删除该测试。
1. Coverage Fingerprinting (per-test)
1. 覆盖率指纹识别(单测试层面)
The wrong answer is a single combined report that tells you per-file percentages. That cannot tell you which test covered which line, so it cannot tell you which tests overlap. You need per-test (dynamic) contexts.
--covpytest / coverage.py — record which test hit each line with dynamic contexts, and turn on branch coverage:
bash
pytest --cov=src --cov-branch --cov-context=test--cov-context=testswitch_context()--cov-branchifelse.coveragecontextline_bitsarcThen read the contexts table out of the SQLite DB to build a per-test fingerprint: for each test, the exact set of and pairs it covered. From those sets you compute:
.coverage(file, line)(file, branch-arc)- Uniquely covered lines — lines/branches that only one test covers. Lose that test and you lose that coverage outright. These tests are pulling their weight; protect them.
- Subsumption — test A's covered set is a superset of test B's. A candidate for redundancy (but see §2 — it is not proof).
See for the SQL to pull contexts from , the Python that builds per-test line/branch sets and computes uniquely-covered and subsumption relations, and the JS/Vitest+Istanbul equivalent ( with per-test reporters).
references/coverage-fingerprinting.md.coverage--coveragecoverage-final.jsonDo not rank tests by line count per test file, and do not delete tests merely for having a low overall coverage percentage — a one-line test can be the only thing guarding a critical branch.
错误的做法是生成单一的报告,仅显示每个文件的覆盖率百分比。这种报告无法告诉你哪个测试覆盖了哪一行代码,因此无法找出测试间的重叠。你需要单测试(动态)上下文。
--covpytest / coverage.py —— 使用动态上下文记录每个测试覆盖的代码行,并开启分支覆盖率:
bash
pytest --cov=src --cov-branch --cov-context=test--cov-context=testswitch_context()--cov-branchifelse.coveragecontextline_bitsarc然后从**.coverage SQLite数据库中读取contexts表**,构建单测试指纹:对于每个测试,记录其覆盖的精确和对集合。通过这些集合可计算:
(文件, 行号)(文件, 分支弧)- 唯一覆盖代码行 —— 仅被一个测试覆盖的代码行/分支。丢失该测试就会直接丢失这部分覆盖率。这些测试发挥着实际作用,需加以保护。
- 包含关系 —— 测试A的覆盖集合是测试B的超集。这是冗余的候选情况(但请参见第2节——这并非证据)。
请查看,了解从中提取上下文的SQL语句、构建单测试代码行/分支集合并计算唯一覆盖和包含关系的Python代码,以及JS/Vitest+Istanbul的等效配置(带单测试报告的)。
references/coverage-fingerprinting.md.coverage--coveragecoverage-final.json请勿按每个测试文件的代码行数对测试排序,也勿仅因测试的整体覆盖率低就删除它——一行代码的测试可能是保护关键分支的唯一防线。
2. The Coverage-Equality Trap (the load-bearing rule)
2. 覆盖率相等陷阱(承重规则)
This is the single most important rule in the skill. When per-test data shows Test A and Test B cover exactly the same lines, the naive conclusion is "redundant, delete one." That is wrong, and here is the gotcha:
Coverage equality does not prove the tests have the same assertions, the same inputs, or the same oracle. Two tests can cover identical lines while one asserts the HTTP status and the other asserts the response body, or while they pass different edge-case inputs. One covers same lines but asserts different values; the other covers same lines but checks different state. Coverage measures execution, not verification.
To find out whether B is actually redundant — whether A truly subsumes B's fault-detection — run mutation testing:
- mutmut (3.6.0+) or cosmic-ray for Python, StrykerJS (9.x) for JS/TS, PIT for Java/JVM, cargo-mutants for Rust.
- Mutation testing injects faults (mutants) into the covered code. A test "kills" a mutant if it fails on the mutated code. If A kills every mutant that B kills, A genuinely subsumes B's fault detection and B is a defensible delete candidate. If B kills a mutant A misses, B catches a defect class A does not — keep B, even though coverage was identical.
Decision: identical coverage → flag as a candidate → confirm with mutation testing → only then propose deletion. When assertions differ and mutation results differ, retain both, do not delete. See for the mutmut/Stryker config that scopes mutation runs to the suspect lines and the kill-set comparison.
references/mutation-confirmation.md这是本技能中最重要的规则。当单测试数据显示测试A和测试B覆盖完全相同的代码行时,直观结论是“冗余,删除其中一个”。这是错误的,陷阱在于:
覆盖率相等并不证明测试具有相同的断言、相同的输入或相同的验证逻辑。 两个测试可覆盖相同代码行,其中一个断言HTTP状态码,另一个断言响应体;或者它们传入不同的边缘场景输入。一个测试覆盖相同代码行但断言不同值;另一个覆盖相同代码行但检查不同状态。覆盖率衡量的是执行情况,而非验证情况。
要确定测试B是否真正冗余——即测试A是否真正覆盖了测试B的缺陷检测能力——需运行突变测试:
- Python使用mutmut(3.6.0+)或cosmic-ray,JS/TS使用StrykerJS(9.x),Java/JVM使用PIT,Rust使用cargo-mutants。
- 突变测试会在被覆盖的代码中注入缺陷(突变体)。若测试在突变代码上运行失败,则说明该测试“杀死”了突变体。若测试A能杀死测试B能杀死的所有突变体,则测试A真正覆盖了测试B的缺陷检测能力,测试B可作为删除候选。若测试B能杀死测试A未覆盖的突变体,则测试B能捕获测试A无法覆盖的缺陷类别——保留测试B,即使覆盖率完全相同。
决策流程:相同覆盖率 → 标记为候选 → 通过突变测试确认 → 仅在此之后提出删除建议。当断言不同且突变测试结果不同时,保留两个测试,勿删除。请查看,了解针对可疑代码行的mutmut/Stryker配置,以及用于确认包含关系的杀死集合对比方法。
references/mutation-confirmation.md3. Near-Duplicate Clustering
3. 近似重复聚类
Goal: surface copy-pasted tests without flagging every test in the same file. Grouping tests by filename is not clustering — it tells you nothing about similarity. Two defensible signals, combined:
-
AST (abstract syntax tree) similarity. Parse each test into an AST, normalize away identifier names and literals, then compare structure. Use a token/tree similarity metric (Jaccard over normalized token shingles, cosine over AST n-grams, or tree edit distance). AST-based comparison ignores formatting and variable-name noise that defeats exact string matching or raw. Never use raw line numbers as a similarity signal.
diff -
Coverage-profile signature. From §1, each test already has a covered-line/branch set — its execution profile. Tests with near-identical coverage signatures and near-identical ASTs are strong near-duplicate candidates; either signal alone is weak.
Cluster with a tunable similarity threshold (e.g. agglomerative/hierarchical clustering, cut at a configurable cutoff — start ~0.85, tune to your false-positive tolerance). Output clusters, never deletions.
Every cluster is routed to human review. The agent does not delete a whole cluster automatically — copy-paste tests frequently diverge in one assertion that matters. Present each cluster with its members, the pairwise similarity, and the coverage-profile overlap, and let a human confirm which (if any) collapse.
See for the AST normalization, the shingle/Jaccard and tree-edit similarity functions, and the agglomerative clustering with the tunable threshold.
references/clustering.md目标:找出复制粘贴的测试,同时避免标记同一文件中的所有测试。按文件名分组并非聚类——这无法体现测试的相似性。需结合两个可靠信号:
-
AST(抽象语法树)相似度。 将每个测试解析为AST,标准化标识符名称和字面量,然后对比结构。使用令牌/树相似度指标(标准化令牌分片的Jaccard系数、AST n元语法的余弦相似度或树编辑距离)。基于AST的对比可忽略格式和变量名噪声,这些噪声会导致精确字符串匹配或原始失效。切勿使用原始行号作为相似度信号。
diff -
覆盖率配置文件特征。 从第1节可知,每个测试已有覆盖代码行/分支集合——即其执行配置文件。覆盖率特征近似相同且AST近似相同的测试,是强近似重复候选;仅单个信号的情况下可信度较低。
使用可调相似度阈值进行聚类(例如层次聚类,设置可配置的截断值——初始值约0.85,可根据误报容忍度调整)。输出聚类结果,而非直接删除。
每个聚类都需提交人工审核。 智能体不会自动删除整个聚类——复制粘贴的测试通常会在某个关键断言上出现差异。需向人工展示每个聚类的成员、两两相似度和覆盖率配置文件重叠情况,由人工确认哪些(若有)可合并。
请查看,了解AST标准化、分片/Jaccard和树编辑相似度函数,以及带可调阈值的层次聚类方法。
references/clustering.md4. Mining CI History
4. CI历史挖掘
Parse your test-result history — JUnit XML archives, or a platform that already stores it: Datadog Test Optimization, Trunk Flaky Tests, BuildPulse, CircleCI test insights. For each test compute pass rate / fail rate and the flip rate (how often consecutive runs transition pass↔fail). Two findings, two very different meanings:
Never-failing tests (zero failures / 100% pass over the window). The naive move is to delete any test that has never once failed. Wrong — never-failing does not mean delete or useless. A test most often never fails because it guards low-churn, low-risk, stable code — exactly the code nobody touches, so the test never trips. That is low defect-detection signal in this window, not zero value. Disposition: investigate, do not delete — check churn and risk of the code under test. If it covers a critical path that simply has not regressed, it stays.
Always-flaky tests. Flakiness is not decided by a single run. The real definition is different results on the same SHA / same commit — the same code produced a pass and a fail. Detect that by grouping runs by commit SHA and finding tests with both outcomes on one SHA (Trunk and BuildPulse do this natively). The naive move is "delete flaky tests to clean up CI." Wrong: a flaky test may still be your only coverage of a real path. Disposition: quarantine and fix the flake, never delete to clean up CI. Quarantine de-noises CI immediately; the root cause still gets fixed. See for runtime quarantine and self-healing of an individual flaky test.
test-reliabilitySee for the JUnit-XML aggregation script, the same-SHA flake query, and the Datadog/Trunk API pulls.
references/ci-history-mining.md解析测试结果历史——JUnit XML归档,或已存储该数据的平台:Datadog Test Optimization、Trunk Flaky Tests、BuildPulse、CircleCI test insights。为每个测试计算通过率/失败率以及翻转率(连续运行中通过↔失败的转换频率)。两种发现对应两种截然不同的含义:
永不失败的测试(在观察窗口内零失败/100%通过)。直观做法是删除所有从未失败的测试。这是错误的——永不失败并不意味着无用或可删除。测试永不失败通常是因为它守护着低变更、低风险、稳定的代码——正是无人改动的代码,因此测试从未触发失败。这意味着在当前窗口内缺陷检测信号低,而非零价值。处置方式:调查,勿删除——检查被测代码的变更频率和风险。若它覆盖的关键路径只是尚未出现回归,则需保留。
始终不稳定的测试。不稳定并非由单次运行决定。真正的定义是同一SHA/同一提交产生不同结果——相同代码出现一次通过一次失败。可通过按提交SHA分组运行结果,找出在同一SHA下同时出现两种结果的测试(Trunk和BuildPulse原生支持此功能)。直观做法是“删除不稳定测试以清理CI”。这是错误的:不稳定测试可能仍是覆盖真实路径的唯一测试。处置方式:隔离并修复不稳定问题,勿为清理CI而删除。隔离可立即减少CI噪声;根本原因仍需修复。请查看技能,了解单个不稳定测试的运行时隔离和自修复方法。
test-reliability请查看,了解JUnit-XML聚合脚本、同一SHA下的不稳定测试查询,以及Datadog/Trunk API调用方法。
references/ci-history-mining.md5. Prune Decision Rules
5. 精简决策规则
Stop deleting everything that "looks redundant." The three failure-categories are distinct states, and each gets a different disposition:
| Category | Definition (the test is...) | Disposition |
|---|---|---|
| Redundant | subsumed by another test — covers the same lines AND the survivor kills the same mutants (§2) | merge or delete — but only after the mutation-kill check confirms subsumption; quarantine first |
| Obsolete | testing a feature that was removed / dead code / a path that no longer exists | delete — the code it tested is gone; verify the target truly no longer exists, then remove |
| Low-value | never failed AND trivial (a getter, a no-op, no meaningful assertion) | quarantine / route to review — low value is not zero value; confirm before removal |
| Keep | covers something uniquely, catches defects (positive defect-detection history), or guards a high-risk path | keep — protected regardless of coverage overlap |
The discipline: a different action per category. , , , . Applying a single blanket rule to every flagged test is the anti-pattern that loses coverage. Note that "redundant" and "low-value" both route through quarantine, not straight to .
redundant => merge/delete-after-mutation-checkobsolete => deletelow-value => quarantine/reviewkeep => keeprm停止删除所有“看起来冗余”的测试。三类失效场景是不同状态,对应不同的处置方式:
| 类别 | 定义(测试...) | 处置方式 |
|---|---|---|
| 冗余 | 被另一个测试包含——覆盖相同代码行,且保留的测试能杀死相同的突变体(第2节) | 合并或删除——但仅在突变测试确认包含关系后;先隔离 |
| 过时 | 测试已移除的功能/死代码/不再存在的路径 | 删除——被测代码已不存在;确认目标确实不再存在后再移除 |
| 低价值 | 从未失败且无实际意义(仅测试 getter、空操作、无有效断言) | 隔离/提交审核——低价值并非零价值;移除前需确认 |
| 保留 | 唯一覆盖某些内容、捕获过缺陷(有缺陷检测历史),或守护高风险路径 | 保留——无论覆盖率是否重叠,均需保护 |
原则:不同类别对应不同操作。,,,。对所有标记测试应用单一规则是会导致覆盖率丢失的反模式。注意“冗余”和“低价值”均需先隔离,而非直接删除。
冗余 => 合并/突变测试确认后删除过时 => 删除低价值 => 隔离/审核保留 => 保留6. Tiering (smoke / core / extended)
6. 分层(冒烟/核心/扩展)
Restructuring a flat suite into tiers is not sorting by speed. Smoke is not the first N tests in file order, it is not a random sample, and you must not tier solely on how fast each test runs. Tier on two evidence inputs:
- Business risk / critical path — from the risk map (). Tests guarding revenue, auth, data integrity, and the top user journeys are smoke/core regardless of speed.
risk-based-testing - Defect-detection history — tests that have actually caught real bugs (mine CI history + linked bug tickets for tests that failed on a commit that fixed a defect). A test with a track record of catching regressions earns a high tier on merit.
| Tier | Goal | Selection evidence |
|---|---|---|
| Smoke | fast, runs on every push, a few minutes, critical paths only | highest-risk paths + proven defect-catchers; fast enough to gate every commit |
| Core | per-PR / merge gate | all critical + high-risk coverage, broader than smoke |
| Extended | full / nightly / slow / pre-release | everything else — exhaustive, long-running, edge cases |
Execution time as a tiering input is allowed — but only as a secondary tiebreaker, not the primary axis. Runtime and duration break ties; they never set the tier. Among equally-risky tests, prefer the faster ones for smoke. Encode tiers as markers/tags — / , Jest/Vitest tag, or a JUnit category — so selects a tier without moving files. See for the marker scheme and the defect-detection-history query.
@pytest.mark.smoke@pytest.mark.extendedpytest -m smokereferences/tiering.md将扁平套件重构为分层并非按速度排序。冒烟测试并非文件顺序中的前N个测试,也不是随机样本,且绝不能仅按测试运行速度分层。分层需基于两个证据输入:
- 业务风险/关键路径 —— 来自风险地图(技能生成)。守护收入、认证、数据完整性和核心用户旅程的测试,无论速度如何,均属于冒烟/核心层级。
risk-based-testing - 缺陷检测历史 —— 实际捕获过真实缺陷的测试(挖掘CI历史+关联缺陷工单,找出在修复缺陷的提交上失败的测试)。有捕获回归记录的测试凭功绩进入高层级。
| 层级 | 目标 | 选择依据 |
|---|---|---|
| 冒烟 | 快速,每次推送都运行,耗时数分钟,仅覆盖关键路径 | 最高风险路径 + 已证明的缺陷捕获能力;速度足够快,可作为每次提交的门禁 |
| 核心 | 作为PR/合并门禁 | 所有关键+高风险覆盖范围,比冒烟测试更全面 |
| 扩展 | 完整/夜间/慢速/预发布运行 | 所有其他测试—— exhaustive、运行时间长、覆盖边缘场景 |
执行时间可作为分层的辅助输入——但仅作为次要的平局决胜因素,而非主要依据。 运行时长用于打破平局,而非决定层级。在风险相同的测试中,优先选择速度更快的纳入冒烟测试。使用标记/标签编码层级——/、Jest/Vitest标签或JUnit类别——这样可直接选择对应层级,无需移动文件。请查看,了解标记方案和缺陷检测历史查询方法。
@pytest.mark.smoke@pytest.mark.extendedpytest -m smokereferences/tiering.md7. Destructive Safety (the grace period)
7. 破坏性操作安全机制(宽限期)
Asked to delete 600 tests, the naive agent opens one PR that s 600 files. Never remove the whole cohort in a single PR, and do not the test files. Deletion is destructive; gate it:
rmrm- Quarantine before delete — never delete directly. Mark the flagged tests skipped (,
@pytest.mark.skip(reason="curation-2026-Q2, see audit row"), Jestxfail), or move them to a.skip/quarantine/suite that still lives in the repo. They stop running but stay visible and instantly restorable.deprecated/ - Human sign-off is required. A named approver — via CODEOWNERS on the test directories, or an explicit reviewer on the PR — must approve. No "redundant tests need no review." Redundancy was a hypothesis until §2 confirmed it.
- Define an observation window. Run the suite with the cohort quarantined for a grace period — default 2 sprints / 2 releases — and monitor for escaped defects: watch production incidents and bug tickets for anything the quarantined tests would have caught. An escaped defect in the window means a test was not redundant; restore it.
- Only then delete, in small batches, each linked to its audit row (§8).
- Keep it restorable. Record the quarantine location and the commit so any test can be restored/reverted in one step. Git history is the floor, not the recovery plan.
See for the quarantine markers, a CODEOWNERS snippet for test paths, and the escaped-defect watch checklist.
references/destructive-safety.md若要删除600个测试,直观做法是提交一个PR直接删除600个文件。绝不能在单个PR中删除所有目标测试,也勿直接删除测试文件。删除是破坏性操作,需设置门禁:
- 先隔离再删除——勿直接删除。 将标记的测试标记为跳过(、
@pytest.mark.skip(reason="curation-2026-Q2, see audit row")、Jestxfail),或移至.skip/quarantine/套件,该套件仍保留在仓库中。测试停止运行但仍可见,可立即恢复。deprecated/ - 必须经人工签字确认。 指定审批人——通过测试目录的CODEOWNERS,或PR上的明确审核人——必须审批。不存在“冗余测试无需审核”的情况。冗余只是一种假设,需经第2节确认。
- 定义观察窗口。 在宽限期内运行包含隔离测试的套件——默认2个迭代周期 / 2个版本发布周期——并监控缺陷漏网情况:观察生产事件和缺陷工单,查看是否有隔离测试本应捕获的问题。若宽限期内出现漏网缺陷,则说明该测试并非冗余;需恢复。
- 仅在此之后分批删除,每批删除均关联其审计记录行(第8节)。
- 保持可恢复性。 记录隔离位置和提交记录,以便任何测试都能一步恢复/回滚。Git历史是底线,而非恢复方案。
请查看,了解隔离标记、测试路径的CODEOWNERS片段,以及漏网缺陷监控清单。
references/destructive-safety.md8. The Audit Record
8. 审计记录
The deliverable that makes every removal defensible to a future engineer or an auditor is not a count of deleted tests. It is a per-test record — one row per deleted test — with a real justification each. A bare "we removed 600 redundant tests" is unauditable.
Each row carries:
- Test id / path — what was removed.
- Category & reason — redundant / obsolete / low-value, with the specific justification ("subsumed; survivor kills identical mutant set").
- Superseded by — the surviving (superseding) test id that covers/replaces it (for redundant): it is covered by the test that subsumes it, or replaced by the test that supersedes it.
- Coverage delta — lines/branches no longer covered after removal (before/after); ideally zero net loss, proven by §1.
- Approval — approver name, the PR/commit SHA, and the date signed off.
- Restore path — quarantine location and the one-line command to recover/revert it.
Store it as a committed CSV/Markdown table (e.g. ) so it is versioned alongside the deletions. See for the full column schema and a worked example row.
docs/test-curation-log.mdreferences/audit-record.md让每一次删除都能向未来工程师或审计人员辩护的交付物并非删除测试的数量。而是每个删除测试的单独记录——每行对应一个删除的测试——每条记录都有真实的理由。仅“我们删除了600个冗余测试”是无法审计的。
每行记录包含:
- 测试ID/路径 —— 被删除的内容。
- 类别及原因 —— 冗余/过时/低价值,附带具体理由(例如“被包含;保留的测试能杀死相同的突变体集合”)。
- 替代测试 —— 替代它的保留测试ID(针对冗余测试):由包含它的测试覆盖,或由替代测试取代。
- 覆盖率变化 —— 删除前后不再覆盖的代码行/分支;理想情况下净损失为零,需经第1节证明。
- 审批信息 —— 审批人姓名、PR/提交SHA、日期。
- 恢复路径 —— 隔离位置和一步恢复/回滚的命令。
将记录存储为已提交的CSV/Markdown表格(例如),使其与删除操作一起版本化。请查看,了解完整的列 schema 和示例记录行。
docs/test-curation-log.mdreferences/audit-record.mdAnti-Patterns
反模式
1. "Same coverage, delete one"
1. “相同覆盖率,删除其中一个”
The single most damaging mistake. Identical line coverage proves the lines ran, not that the assertions match. Confirm subsumption with mutation testing (§2) before proposing any deletion.
这是最具破坏性的错误。相同代码行覆盖率仅证明代码被执行,无法证明断言匹配。在提出任何删除建议前,需通过突变测试确认包含关系(第2节)。
2. Two green signals read as a green light
2. 两个绿色信号视为绿灯
A test that has never failed in 18 months AND is fully covered (same lines) by another is the classic "surely safe to delete" case. It is not sufficient on its own — still verify, do not delete automatically. A test that never failed may mean stable code not worthless code — it often means low risk and low change, not that the test is redundant. A different oracle could still be unique to this test (a different assertion, input, or edge case it alone checks). Ask the mutation question: does the OTHER test assert/catch/kill what this one does? Only after that, quarantine and observe for a grace period, then delete. Both signals are necessary-not-sufficient (§9 below / §2 / §7).
一个18个月从未失败且被另一个测试完全覆盖(相同代码行)的测试,是典型的“肯定可以安全删除”案例。但这并不足够——仍需验证,勿自动删除。从未失败可能意味着代码稳定而非无用——通常意味着低风险、低变更,而非测试冗余。该测试可能仍具有独特的验证逻辑(独特的断言、输入或仅它覆盖的边缘场景)。需提出突变测试问题:另一个测试是否能断言/捕获/杀死该测试能处理的内容?仅在此之后,隔离并观察宽限期,再删除。两个信号都是必要非充分条件(下文第9节/第2节/第7节)。
3. One merged coverage run with no per-test context
3. 无单测试上下文的合并覆盖率运行
--cov=src--cov-context=test.coverage--cov=src--cov-context=test.coverage4. Grouping near-duplicates by filename
4. 按文件名分组近似重复测试
Filename proximity is not similarity. Cluster on AST similarity + coverage-profile signature with a tunable threshold (§3), and route clusters to humans — never auto-delete a whole cluster.
文件名接近并不代表相似性。需基于AST相似度+覆盖率配置文件特征,使用可调阈值进行聚类(第3节),并将聚类结果提交人工审核——绝不能自动删除整个聚类。
5. Deleting never-failing or flaky tests
5. 删除永不失败或不稳定的测试
Never-failing → investigate (likely low-churn code), do not delete. Flaky → quarantine and fix, do not delete to clean up CI. Flakiness is same-SHA divergence, not a single failed run.
永不失败 → 调查(可能是低变更代码),勿删除。不稳定 → 隔离并修复,勿为清理CI而删除。不稳定的定义是同一SHA下结果不一致,而非单次运行失败。
6. One disposition for all flagged tests
6. 对所有标记测试采用单一处置方式
Redundant, obsolete, and low-value need different actions (§5). Collapsing them into one "delete" bucket loses real coverage.
冗余、过时、低价值需不同的操作(第5节)。将它们归为“删除”一类会丢失真实覆盖率。
7. Tiering by speed (or "first N") alone
7. 仅按速度(或“前N个”)分层
Smoke is risk + defect-detection history, not the fastest or first N tests. Runtime is a tiebreaker, not the axis (§6).
冒烟测试基于风险+缺陷检测历史,而非最快或前N个测试。运行时长是平局决胜因素,而非分层依据(第6节)。
8. Big-bang deletion PR
8. 大规模删除PR
600 tests 'd in one PR with no quarantine, no sign-off, no observation window. Always quarantine → sign-off → observe → delete in batches (§7).
rm在单个PR中删除600个测试,无隔离、无签字确认、无观察窗口。始终遵循隔离 → 签字确认 → 观察 → 分批删除的流程(第7节)。
9. "It's in git history, just delete it"
9. “代码在Git历史中,直接删除即可”
Git history is not a recovery plan — nobody watches for the defect the deleted test would have caught. The quarantine grace period plus escaped-defect monitoring is the actual safety net.
Git历史并非恢复方案——无人会监控被删除测试本应捕获的缺陷。隔离宽限期加漏网缺陷监控才是真正的安全保障。
Verification
验证
Prove the audit actually holds before anyone deletes anything, smallest check first:
- Per-test contexts were captured, not a flat report. returns a count roughly equal to your test count (not 0/1). Zero means
sqlite3 .coverage "SELECT COUNT(*) FROM context WHERE context != '';"did not run.--cov-context=test - No deletion rests on coverage equality alone. For every row, the audit log's
redundantcolumn is filled (mutation_check+ run id).yesequals the number of rows whosegrep -c 'redundant' docs/test-curation-log.mdis non-blank.mutation_check - Quarantine, not removal, hit the repo first. shows no test files deleted in the quarantine PR; the flagged tests are skipped/xfail or moved under
git log --diff-filter=D --name-only -- tests/ | grep test_, still collectible (tests/quarantine/or equivalent lists them).pytest --collect-only -m skip - Tiers select correctly. returns only the risk/defect-catcher cohort and runs under the smoke budget;
pytest -m smoke --collect-onlyaccounts for every test (no test is untagged).pytest -m "smoke or core or extended" --collect-only - The audit record is complete. No row has a blank
redundant,superseded_by, orcoverage_delta;mutation_checkiscoverage_deltafor every removal claiming zero net loss.0 lines / 0 branches
在删除任何内容前,先证明审计是可靠的,从最小检查开始:
- 已捕获单测试上下文,而非扁平报告。 返回的计数大致等于测试数量(而非0/1)。若为0,则说明未运行
sqlite3 .coverage "SELECT COUNT(*) FROM context WHERE context != '';"。--cov-context=test - 无删除仅基于覆盖率相等。 每个记录行的审计日志中
冗余列已填写(mutation_check+ 运行ID)。yes等于grep -c 'redundant' docs/test-curation-log.md列非空的行数。mutation_check - 仓库中先出现隔离,而非删除。 显示隔离PR中未删除任何测试文件;标记的测试被标记为skip/xfail或移至
git log --diff-filter=D --name-only -- tests/ | grep test_,仍可被收集(tests/quarantine/或等效命令可列出这些测试)。pytest --collect-only -m skip - 分层选择正确。 仅返回风险/缺陷捕获类测试,且运行时长符合冒烟测试预算;
pytest -m smoke --collect-only包含所有测试(无测试未标记)。pytest -m "smoke or core or extended" --collect-only - 审计记录完整。 无记录行的
冗余、superseded_by或coverage_delta为空;所有声称净损失为零的删除记录,其mutation_check为coverage_delta。0 lines / 0 branches
Done When
完成标准
- A per-test coverage fingerprint exists for the suite (built with /
--cov-context=testor the Istanbul per-test equivalent), and uniquely-covered lines per test are computed.--cov-branch - Every "redundant" candidate has a mutation-testing result attached proving the survivor kills the same mutants — no deletion proposed on coverage equality alone.
- Near-duplicate clusters were produced from AST + coverage-profile similarity at a stated threshold and routed to human review; no cluster was auto-deleted.
- CI history was mined for never-failing (disposition: investigate) and same-SHA flaky tests (disposition: quarantine/fix) — neither category deleted on those signals.
- Each flagged test has exactly one of {redundant, obsolete, low-value, keep} with the matching disposition applied.
- Tiers (smoke/core/extended) are assigned via markers/tags using risk + defect-detection history as inputs; (or equivalent) selects a tier.
pytest -m smoke - No test was deleted without: a quarantine period served, a named approver / CODEOWNERS sign-off recorded, and the observation window completed with no escaped defect.
- A committed per-test audit record exists with one row per deletion containing category, superseded-by, coverage delta, approver+SHA+date, and restore path.
- 已为测试套件生成单测试覆盖率指纹(使用/
--cov-context=test或Istanbul单测试等效配置),并计算出每个测试的唯一覆盖代码行。--cov-branch - 每个“冗余”候选均附有突变测试结果,证明保留的测试能杀死相同的突变体——无仅基于覆盖率相等提出的删除建议。
- 已基于AST+覆盖率配置文件相似度,在指定阈值下生成近似重复聚类,并提交人工审核;无聚类被自动删除。
- 已挖掘CI历史,找出永不失败的测试(处置方式:调查)和同一SHA下的不稳定测试(处置方式:隔离/修复)——两类测试均未仅基于上述信号被删除。
- 每个标记测试均被明确归类为{冗余、过时、低价值、保留}之一,并应用了对应的处置方式。
- 已使用标记/标签,基于风险+缺陷检测历史分配了冒烟/核心/扩展层级;(或等效命令)可选择对应层级。
pytest -m smoke - 所有测试删除均满足:已完成隔离期、已记录指定审批人/CODEOWNERS签字确认、已完成观察窗口且无漏网缺陷。
- 已提交单测试审计记录,每个删除测试对应一行,包含类别、替代测试、覆盖率变化、审批人+SHA+日期及恢复路径。
Related Skills
相关技能
- ai-qa-review — Judges whether an individual test is well-written (smells, weak assertions, testability). This skill decides whether a test should exist at all; ai-qa-review decides whether an existing one is good. Run ai-qa-review on the survivors after curation.
- coverage-analysis — Owns coverage thresholds, gap analysis, and the mutation-testing setup at the project level. This skill consumes per-test coverage for redundancy decisions; go there for ratchets and CI gating.
- test-reliability — Runtime self-healing and quarantine of a single flaky test as it fails. This skill finds the flaky cohort across CI history; test-reliability fixes one at a time.
- risk-based-testing — Produces the risk matrix this skill's "keep" disposition and tiering depend on. Run it first if no risk map exists.
- qa-project-context — Universal dependency: stack, runner, risk map, and ownership that drive every decision here.
- ai-qa-review —— 判断单个测试是否编写规范(代码异味、弱断言、可测试性)。本技能决定测试是否应该存在;ai-qa-review决定现有测试是否优质。精简完成后,可对保留的测试运行ai-qa-review。
- coverage-analysis —— 负责项目层面的覆盖率阈值、差距分析和突变测试设置。本技能使用单测试覆盖率进行冗余判定;如需设置阈值和CI门禁,请使用该技能。
- test-reliability —— 单个不稳定测试失败时的运行时自修复和隔离。本技能找出CI历史中的不稳定测试群体;test-reliability逐个修复不稳定测试。
- risk-based-testing —— 生成本技能“保留”判定和分层依赖的风险矩阵。若无风险地图,请先运行该技能。
- qa-project-context —— 通用依赖:技术栈、测试运行器、风险地图和所有权,这些因素驱动本技能的所有决策。
Reference Files (in references/
)
references/参考文件(位于references/
目录)
references/- coverage-fingerprinting.md — SQLite context queries, per-test line/branch set construction, uniquely-covered + subsumption computation, and the JS/Istanbul per-test equivalent.
.coverage - mutation-confirmation.md — mutmut/cosmic-ray/StrykerJS config scoped to suspect lines and the kill-set comparison that confirms subsumption.
- clustering.md — AST normalization, Jaccard/cosine/tree-edit similarity, and agglomerative clustering with a tunable threshold.
- ci-history-mining.md — JUnit-XML aggregation, same-SHA flake detection query, and Datadog/Trunk API pulls.
- tiering.md — Marker/tag scheme for smoke/core/extended and the defect-detection-history query.
- destructive-safety.md — Quarantine markers, CODEOWNERS for test paths, and the escaped-defect watch checklist.
- audit-record.md — Full column schema for the deletion log and a worked example row.
- coverage-fingerprinting.md —— SQLite上下文查询、单测试代码行/分支集合构建、唯一覆盖+包含关系计算,以及JS/Istanbul单测试等效方案。
.coverage - mutation-confirmation.md —— 针对可疑代码行的mutmut/cosmic-ray/StrykerJS配置,以及确认包含关系的杀死集合对比方法。
- clustering.md —— AST标准化、Jaccard/余弦/树编辑相似度,以及带可调阈值的层次聚类方法。
- ci-history-mining.md —— JUnit-XML聚合、同一SHA下不稳定检测查询,以及Datadog/Trunk API调用方法。
- tiering.md —— 冒烟/核心/扩展的标记/标签方案,以及缺陷检测历史查询方法。
- destructive-safety.md —— 隔离标记、测试路径的CODEOWNERS,以及漏网缺陷监控清单。
- audit-record.md —— 删除日志的完整列schema和示例记录行。