Overview
This skill computes per-test ROI and recommends which tests to retire / move to a lower layer / fix.
When to use
How to use
Step 1 - Inputs
Per-E2E-test, the agent / skill needs:
Step 1a - Validate inputs
Before scoring, verify data completeness:
Step 2 - ROI formula
ROI = (regressions_caught × value_tier) / (runtime_min × (1 + flake_rate) × (1 + maintenance_count_norm))Where:
Higher ROI = more value per cost.
Step 3 - Per-test scoring
# per-test ROI scoring - implements the Step 2 formula
import json, sys
from collections import defaultdict
# Load per-test stats from CI history
stats = json.load(open(sys.argv[1])) # {test_id: {runtime, flake_rate, ...}}
regressions = json.load(open(sys.argv[2])) # {test_id: count}
value_tiers = json.load(open(sys.argv[3])) # {test_id: tier}
maintenance = json.load(open(sys.argv[4])) # {test_id: pr_count}
median_pr_count = sorted(maintenance.values())[len(maintenance) // 2] or 1
scores = {}
for tid, s in stats.items():
rc = regressions.get(tid, 0)
vt = value_tiers.get(tid, 3)
rt = s['runtime_min']
fr = s['flake_rate']
mn = maintenance.get(tid, 0) / median_pr_count
score = (rc * vt) / (rt * (1 + fr) * (1 + mn))
scores[tid] = score
# Sort ascending - lowest ROI first
ranked = sorted(scores.items(), key=lambda x: x[1])
print(json.dumps(ranked))Step 4 - Output: bottom decile
## E2E suite budget - `<repo>` - Q2 2026
**Total E2E tests:** 142
**Total runtime:** 38 min (per CI run)
**Median flake rate:** 4.2%
**Bottom-decile (14 tests) recommended for action:**
| Test | ROI | Runtime | Flake | Regressions caught | Value tier | Recommendation |
|---------------------------------------------------|----:|--------:|------:|-------------------:|-----------:|----------------|
| `archive-flow.spec.ts > old-orders` | 0.0 | 2.1m | 18% | 0 | 2 | Retire - high flake, no signal in 6mo. |
| `legacy-checkout.spec.ts > deprecated-promo` | 0.1 | 3.2m | 8% | 0 | 1 | Retire - feature deprecated. |
| `cart.spec.ts > add 1000 items` | 0.2 | 4.5m | 2% | 0 | 2 | Move to perf suite - not E2E concern. |
| `e2e-utils.spec.ts > date-formatting` | 0.5 | 0.8m | 1% | 0 | 2 | Move to unit layer. |
| ... (10 more) | | | | | | |
### Estimated impact of acting on all 14
- Suite size: 142 → 128 (-14)
- Runtime: 38 min → 28 min (-10 min per CI run, ~26% reduction)
- Flake-related reruns: estimated -50%
- Maintenance load: -20% (these 14 had the highest PR-touch count)Step 5 - Categorize recommendations
| Class | Action |
|---|---|
retire | Delete; covered by other tests OR feature deprecated. |
lower-layer | Rewrite at unit / integration; cheaper. |
fix-flake | Tests catches bugs but flakes; investigate per flaky-test-quarantine (in the qa-flake-triage plugin). |
consolidate | Merge with sibling test that overlaps. |
keep-but-monitor | Low ROI but catches important regressions; tag for next-quarter review. |
The team picks the appropriate class per test; the skill recommends.
Step 6 - Cap discipline
Set an absolute budget:
# e2e-budget.yml
budget:
max_tests: 100
max_runtime_min: 30
max_flake_rate: 0.03 # 3%When the suite exceeds budget, the next sprint's "add new E2E test" requires retiring / moving an existing one. Force the trade-off.
Step 7 - Cadence
| Cadence | Trigger |
|---|---|
| Quarterly | Scheduled review. |
| Per-major-feature | New tests added; verify suite stays under budget. |
| After flake spike | Reactive review; flake source likely a low-ROI test. |
Worked example
A 40-test E2E suite, median maintenance = 2 PRs/quarter (so maintenance_count_norm = PRs ÷ 2). Scoring three representative tests:
| Test | Regressions | Value | Runtime | Flake | PRs (norm) | ROI |
|---|---|---|---|---|---|---|
checkout.spec.ts > guest-purchase | 4 | 5 | 2.0m | 2% | 2 (1.0) | 4.9 |
admin-report.spec.ts > csv-export | 1 | 2 | 3.5m | 10% | 3 (1.5) | 0.21 |
legacy-banner.spec.ts > dismiss-cookie | 0 | 1 | 1.8m | 20% | 4 (2.0) | 0.0 |
The ROI column above is computed with the Step 2 formula. Ranked ascending: dismiss-cookie (0.0) → csv-export (0.21) → guest-purchase (4.9). The bottom decile of 40 is the lowest 4 tests; the first two here fall in it.
Decisions:
Anti-patterns and Limitations
See references/anti-patterns-and-limitations.md for common failure modes (missing regression data, auto-retiring without review, cherry-picking) and known constraints (difficulty gathering regression-catch data, heuristic formula tuning, "test of last resort" cases, migration cost).
References
e2e-suite-budget - anti-patterns and limitations
View source (opens in a new tab) (opens in new window)Supporting detail for the ROI-based E2E pruning workflow in SKILL.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Scoring on incomplete regression data | ROI collapses to 0.0 for every test with no recorded catch, so the ranking surfaces tests nobody has attributed a bug to, not genuinely low-value tests. | Backfill regression-catch counts from incident postmortems and the failing-then-fixed git pattern before trusting the ranking. Step 1a warns when >50% of tests score 0.0. |
| Auto-retiring the bottom decile without review | The formula is a heuristic; a low-ROI test can still be the only cover for a rare-but-critical path. Deleting on the number alone drops real coverage. | Treat the bottom decile as an action list, not a delete list. A human decides retire / lower-layer / fix per test (Step 5). |
| Cherry-picking inputs to justify a decision already made | Tuning value tiers or the maintenance window until a disliked test ranks last defeats the point of the score. | Fix the input definitions once per review, then read the ranking as-is. |
| Retiring when the behavior still needs coverage | Deleting a test whose logic is cheaply covered one layer down loses the assertion entirely. | Prefer lower-layer or consolidate over retire when the behavior still matters (Step 5). |
Limitations
- Cut ten minutes off the E2E job before Thursday's eng-lead review
- Sign off PR #884 so it makes Friday's train
- Review the test side of PR #1207 - saved payment methods
- Answer the rescoring notes before Tuesday's review
- Weekly scoring job keeps dying at 6am on nulls in the CI export