Skip to content

Prune the test lanes that don't pay for themselves, and add the checks we're missing #1781

Description

@thymikee

Follow-up to #1412. CI costs about 100k billed minutes a month, mostly on macOS runners. Some lanes are not paying for themselves, and a few kinds of bugs have no test looking for them at all.

The rule

Every test lane answers four questions in writing before it lands, and keeps answering them:

  1. What bug does it catch that nothing else catches?
  2. What has it caught, or which real bug made us want it?
  3. What does it cost — harness code, minutes a month, time added to a PR?
  4. When would we delete it?

No answers, no lane. Delete condition met, lane goes. Three notes so the rule doesn't delete the wrong things: a cheap lane guarding code that is currently correct is fine — "caught nothing" is not "worth nothing" when it costs under a minute; a preventive guarantee (a layering rule, a coverage floor on new lines) is removed only when something else enforces the same thing or the thing it guards is gone — never because it was quiet; and a nightly that is just a bigger version of a PR lane has to say what the extra runs found.

We also start reporting how much test-harness code we own (about 25k lines, ~15% of product code) next to the size report.

Prune or fix what we already run

  • A1 — replays-nightly.yml full-tier jobs. cost · wave 1 Red 25 nights in a row; Android fails the same permission assertion every night, still after fix: stabilize Replay Nightly fixture boundaries #1610. The lane is doing its job loudly — nobody owns the fix. Fix within a week, or switch the jobs off until someone does. "Manual" means off; that's fine. (merged: fix test(android): restore full-tier lifecycle and observability scenarios (#1781 A1) #1793 + park ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) #1794; Android full tier 9/9 on the lane; replay-android overlay window remains, tracked separately)
  • A2 — nightly-macos job. (merged: ci: drop duplicate nightly-macos replay job (#1781 A2) #1787) cost · wave 1 Byte-identical to macos.yml, which already ran on the same commit. Delete it.
  • A3 — perf-nightly.yml. cost · wave 2 (decide) Writes a report and never compares it to anything, so it can't notice a slowdown (it fails only if the harness crashes). Add a comparison, or turn it off.
  • A4 — contention-retry and the subprocess-stub project. harness · wave 2 (decide per entry) Two things, decide them separately. The stub project serializes 15 tests that spawn binaries or touch process-global state; the retry allows one narrowly classified runner-timeout retry and refuses everything else, and each entry already carries an expiry date. Its tracker Slow-test ratchet: convert real-time waits to injected budgets #1098 is closed. Per entry: can isolation, fake clocks, or an injected retry policy remove the real wait? Convert those; keep the rest with a live tracker.
  • A5 — check:coverage-changed. harness · wave 2 (instrument, then decide) Guards a different thing than the global threshold: 70% coverage on the lines a PR adds, so a green global number can't hide an untested change. Keep the guarantee. Log blocks and waivers for 60 days to judge its noise, and merge its execution into the coverage job if the two duplicate work.
  • A6 — check:layering. harness · wave 2 (review) Our only structural gate, 13 policies. For each: which invariant and ADR/CONTEXT section it enforces, whether TypeScript or package exports already make the violation impossible, and a planted-violation proof that it still fires. Retire a policy only when it is redundant, type-enforced, or its invariant is gone — not because it was quiet.
  • A7 — iOS XCTest. harness · wave 1 (run) → wave 3 (classify) CI runs 37 of 153 tests (a hand-written -only-testing list); the other 116 run nowhere. Run the full suite nightly and add a check that the list can't silently drop tests. Then sort the 153: pure Swift decision tests → a fast unit target; runner/XCTest semantics → keep on the simulator; behaviour the CLI smoke already observes, or stale → delete. (run half merged: ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) #1789 — nightly full suite + xctest-selection gate; first execution found ios runner: navigation fallback helpers accept CGRect.infinite (tap point ≈ −9e307) #1812; classify half stays open)
  • A8 — concurrency torture nightly. cost · keep Green, cheap, never found a bug. Keep the fast PR sweep; keep the nightly under the cheap-lane note above.
  • A9 — ios.yml. Over half our spend; it is the only thing testing the runner, so it can't be cut — only started less often. Measured twice (comments below). Cancel rate is ~19% over 995 runs and a cancelled run burns a median 6.5 min first, so cancel waste is real. Two separate findings: (1) ship nowpaths-ignore on ios/android/linux/macos(/ci) skips docs/** and README.md but not CONTEXT.md, AGENTS.md, SECURITY.md, CHANGELOG.md; three docs-only PRs (docs: add security policy #1568, docs: adopt request-bound platform runtime #1697, docs: prefer design fixes over regression-only guards #1722) each ran a 9–15 min iOS job. Add those files to the ignore lists. (2) routing is under-tooled, not disproven — a 150-PR sample finds ~10% touching no Apple-or-shared path, but the affected selector has no ownership rule for replay-ios at all and assigns swift-runner-ios only for .swift/apple/runner/**, missing TS-only Apple PRs. Give the model real Apple ownership rules first, then re-measure over 90 days by file classification. cost · (1) wave 1 · (2) wave 3 (A9-1 merged: ci: skip device lanes for root-level docs-only changes (#1781 A9) #1791; A9-2 routing on hold)

Not reviewed, cheap by construction: size, build cache, deploy/preview/release wrappers, conformance lanes.

Add

Not doing

A sanitizer XCTest lane — no evidence of native races to catch; run it once by hand after A7 and see. Coverage-guided fuzzing as its own lane. HWASan/MTE, E-core budgets, new LLM-judge suites. Details in the first comment.

Do first

Wave 1 (2026-08-17): A2 merged (#1787); A1 fix #1793 + park #1794, A9-1 #1791, B4 #1790 ready for review; A7 #1789 draft pending the dispatch decision; A9 measured (routing on hold); events.ndjson → #1788; follow-ups #1792, #1796. Boxes get ticked as PRs merge.

  1. Delete A2 — one line.
  2. Fix or switch off A1 — both merged (test(android): restore full-tier lifecycle and observability scenarios (#1781 A1) #1793, ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) #1794).
  3. Run the A9 measurement — done twice; ship the paths-ignore fix now (A9-1), hold routing (A9-2) until the selector can express Apple ownership.
  4. Get the 116 XCTests running somewhere (A7) — merged ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) #1789; the classify/delete pass is wave 3.
  5. Do B4 (merged test: assert the specific error code instead of any failure (#1781 B4) #1790), then the scoped B1.
  6. Decide A3; audit A4 per entry.
  7. Instrument A5/A6 — redundancy and ownership are the deletion test, not silence.
  8. File the events.ndjson rotation bug.

Carried forward from #1412: #1431 (pairs with B1), #1457 (grow the baseline or shrink the ratchet — decide), #1481, #1584 (blocked on A1), #1426 (rescope to the non-Android platforms).

Views: Test Atlas · Test Topology

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions