Selector evaluation: fast_select vs testmon (WP2.1)¶
Status: COMPLETE — decision made (see DECIDE below). Plan docs/superpowers/plans/2026-09-15-test-platform-improvement.md WP2.1; spec §Phase 2: "Pick one and delete or clearly demote the other… recorded with its rationale." All numbers below come from the corrected derive backfill (raw .log re-parse) and the frozen-truth rebench — NOT from the live driver lines, which the v1 parser had poisoned (defect ledger item 1).
Two selectors competed for the fast tier's selection role:
scripts/fast_select.py— static: dotted-reference truth from test-file text, extended (WP2.2) with the producer-graph closure.pytest --testmon2.2.0 — dynamic: coverage-derived from a warmed.testmondatadatabase, transitive by construction.
Protocol (v3)¶
20 sampled commits touching backend/*.py (33-candidate sweep across feat/phase2 + main history, spread across the timeline). One detached git worktree per commit, shared venv, execution strictly serial (repo rule). Universe: backend/tests/unit (+ contracts where present at that HEAD), -n 8 --dist=worksteal -m 'not gpu' --timeout=90 -p no:randomly. Both selectors run AS OF TODAY against the sampled commit's own test corpus (the honest retrospective pairing); fast_select's selection is tier-filtered to unit+contracts and its out-of-tier count recorded.
Ground truth, two arms¶
- Outcome-difference arm — failures(
<sha>) Δ failures(<sha>^): tests THIS commit changed the outcome of (broke or fixed). Honest to history, but undefined on commits that change no outcome (14/20 here — the deciding arm could not be this one). - Injected-fault arm — inject
raise AssertionErrorinto the first multi-line function of the first changed backend.py; truth = tests that fail faulted but not clean. A fault reaching nothing = INERT, recorded as such, excluded from recall. 12/20 injectable (NO_PY_TARGET= 8: the sampled commit had no eligible production.py— test-only or infra commits); 0 INERT among the 12 — every injected fault broke ≥1 test.
Harness-defect ledger (found, fixed, backfilled — each one could have lied)¶
- v1 fails() parser: tail-truncated node IDs before stripping the
FAILEDprefix, so every truth/affected/fs.fail/tm.fail artifact was the literal wordFAILED. Live driver recall lines were wrong (most pessimistically: real fs 3/6 printed as 0/6). Fixed parser →derive.shre-derived every artifact from the RAW logs (runs themselves were valid). - comm -3 tab prefix: the right column of
comm -3is tab-led; downstreamcut -d: -f1kept the tab, soaffected.filesjoined nothing — FALSE 0/1 recall for BOTH selectors on case 09872e45 (both had selected the file). Fixed withsed 's/^\t//'. - testmon db recycle: worktree
.testmondatacould vanish between arms; harness db-guard re-warms and markswarm_rerun(0 occurrences in this run — guard never needed to fire). -mdeactivation claim disproven: testmon docs suggest marker filtering may skip selection; did not reproduce —--testmonselected normally under-m 'not gpu'in all 20 cases.
Results (corrected; recall = files, frozen lists)¶
Outcome arm (truth = affected files; - = affected 0, undefined):
| case | affected | shipped fs | testmon | closure (WP2.2) |
|---|---|---|---|---|
| 096d38be | 2 | 1/2 | 2/2 | 2/2 |
| 09872e45 | 1 | 1/1 | 1/1 | 1/1 |
| 42acc048 | 1 | 0/1 | 1/1 | 1/1 |
| 8ea70057 | 6 | 3/6 | 5/6 | 6/6 |
| a4507909 | 4 | 0/4 | 4/4 | 4/4 |
| a743cd64 | 2 | 1/2 | 1/2 | 2/2 |
| (14 more) | 0 | — | — | — (undefined) |
| total | 15 | 6 (40%) | 14 (93%) | 15 (100%) |
Fault arm (truth = files broken by the injected fault; 8 cases NO_PY_TARGET, no injectable production file — recorded, not scored):
| case | truth | shipped fs | testmon | closure (WP2.2) |
|---|---|---|---|---|
| 096d38be | 3 | 3/3 | 3/3 | 3/3 |
| 09872e45 | 3 | 3/3 | 3/3 | 3/3 |
| 15d7b5a2 | 14 | 10/14 | 8/14 | 14/14 |
| 3396d3ef | 105 | 48/105 | 67/105 | 105/105 |
| 6062d5d5 | 1 | 1/1 | 1/1 | 1/1 |
| 813a2a66 | 2 | 2/2 | 2/2 | 2/2 |
| 8861a6e4 | 1 | 1/1 | 1/1 | 1/1 |
| 8ea70057 | 2 | 2/2 | 2/2 | 2/2 |
| 978bb04c | 1 | 1/1 | 1/1 | 1/1 |
| a1c3c289 | 2 | 2/2 | 2/2 | 2/2 |
| a4507909 | 1 | 1/1 | 1/1 | 1/1 |
| a743cd64 | 1 | 1/1 | 1/1 | 1/1 |
| total | 136 | 75 (55%) | 92 (68%) | 136 (100%) |
Closure column = scripts/fast_select.py at 697aa1a5, replayed against the frozen truth lists (never re-derived post-fix — the lists predate it).
Cost¶
| dimension | fast_select (shipped/closure) | testmon |
|---|---|---|
| selection time | 0–1s / 0–2s | 6–17s |
| selection run cost | none (static) | full parent commit run to WARM the db: 82–114s typical, 165s outlier |
| stale-db behavior | n/a | db miss = full-suite fallback (measured as run cost, e.g. 71–109s rows where selection was stale) |
| cold cost total per push | 0–2s | ~90–180s BEFORE selection even answers |
fast_select's over-selection was measured (fs_out_of_tier rows recorded per case); the tier never under-runs and the selection cost stays seconds.
DECIDE — keep fast_select (+WP2.2 closure); DEMOTE testmon to advisory¶
Evidence-based rationale (also in the ledger):
- Recall on the deciding arm is 100% vs 68%/55% — the closure, which the bake-off itself justified building (WP2.2), dominates on both arms and every miss class (transitive intermediates, package re-exports, fixture-indirect conftest, changed-test identity, compose filename). testmon misses 44/136 fault files even WARM with full coverage data — transitive-by-construction does not mean complete-at-selection-time (stale-coverage gaps show exactly here).
- Cost is not close: closure answers in 0–2s with no state; testmon's honest cold cost is a full parent run (82–165s) PLUS 6–17s selection. Pre-push budgets the closure fits trivially, testmon spends a third of a 300s budget before the first test runs.
- Determinism/no-db-in-worktree: closure output is a pure function of tree+diff (CI-reproducible, no
.testmondatato warm, share, or go stale); testmon's correctness depends on db currency the repo cannot guarantee (worktrees, CI caching,.gitignored). - Why demote and not delete: testmon remains the only tool measuring the COVERAGE-actual dependency (what a test really executed). That is exactly the auditor role for offline audit runs — cross-checking the static selector's blind classes and feeding this evaluation's future re-runs. The bake-off verdict (loser on both recall and cost for the gate role) says: out of the gate path, kept advisory with this file as its evidence dossier.
Per-case data: /tmp/wp21/results/<sha>/ (raw logs + corrected artifacts + fs.meta/tm.meta), roll-ups in rebench.tsv; protocol scripts (driver.sh, derive.sh, rebench.sh) under /tmp/wp21/ (sandbox-local; the protocol's durable record is THIS file + the ledger rows).