You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
What bug does it catch that nothing else catches?
What has it caught, or which real bug made us want it?
What does it cost — harness code, minutes a month, time added to a PR?
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.
A2 — nightly-macos job.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.
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 now — paths-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
Not reviewed, cheap by construction: size, build cache, deploy/preview/release wrappers, conformance lanes.
B2 — fuzzing v2.observers · any time Give CLI args and Maestro commands real input generators (today their mutations die in the tokenizer) and assert the specific error code, not "some error". Done when generators reach past the tokenizer into command validation, the two known bugs (fix(cli): reject excess positionals #1433, the replay parse leak) are rediscovered from a clean corpus, and nightly runtime doesn't grow. Stands on its own.
B3 — fuzzing calibration.observers · wave 3 (after B2) Measure whether the fuzzer can rediscover bugs we already have on file. Nothing measures this, so we don't know if the nightly run does anything. Small, bounded experiment; if it can't rediscover a seeded set within the budget, it doesn't go into CI.
B4 — assert the right reason.observers · wave 1 21 assertions accept any failure, not the one they name. Fix them, add sync/async helpers that assert code (and details.reason where it matters), allow explicit exemptions where the contract really is "propagate this exact error". No broad "is this assertion specific enough" scanner.
B5 — tiny limits.observers · last Make a few hard-coded caps overridable so tests can actually reach them (fix: add iOS private AX snapshot fallback #758 was this class). And events.ndjson has no rotation cap — that's a bug, file it on its own.
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.
Delete A2 — one line.
Fix or switch off A1.
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.
Get the 116 XCTests running somewhere (A7), no deleting yet.
Do B4, then the scoped B1.
Decide A3; audit A4 per entry.
Instrument A5/A6 — redundancy and ownership are the deletion test, not silence.
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).
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:
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
replays-nightly.ymlfull-tier jobs.cost · wave 1Red 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.nightly-macosjob.cost · wave 1Byte-identical tomacos.yml, which already ran on the same commit. Delete it.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.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.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.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.harness · wave 1 (run) → wave 3 (classify)CI runs 37 of 153 tests (a hand-written-only-testinglist); 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.cost · keepGreen, cheap, never found a bug. Keep the fast PR sweep; keep the nightly under the cheap-lane note above.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 now —paths-ignoreon ios/android/linux/macos(/ci) skipsdocs/**andREADME.mdbut notCONTEXT.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 forreplay-iosat all and assignsswift-runner-iosonly 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 3Not reviewed, cheap by construction: size, build cache, deploy/preview/release wrappers, conformance lanes.
Add
observers · wave 3 (after B4)After a session closes and after shutdown: no daemon-owned child processes beyond the declared baseline, no request/session resources left, nothing unexpected in an isolated state dir. Owned resources, not global process counts — simulator and XCTest own things we don't. Both leaks we know about (web: agent-browser Chrome fleets leak and can spin at 100% CPU for days — add idle lifecycle/reaper #1109, iOS simulator recorder leaks on session/daemon teardown while recording is active (0-byte recording.mp4, "Host recording is already in progress") #1324) were found by hand; the check must go red against their pre-fix behaviour. Around the smoke tests, no added minutes.observers · any timeGive CLI args and Maestro commands real input generators (today their mutations die in the tokenizer) and assert the specific error code, not "some error". Done when generators reach past the tokenizer into command validation, the two known bugs (fix(cli): reject excess positionals #1433, the replay parse leak) are rediscovered from a clean corpus, and nightly runtime doesn't grow. Stands on its own.observers · wave 3 (after B2)Measure whether the fuzzer can rediscover bugs we already have on file. Nothing measures this, so we don't know if the nightly run does anything. Small, bounded experiment; if it can't rediscover a seeded set within the budget, it doesn't go into CI.observers · wave 121 assertions accept any failure, not the one they name. Fix them, add sync/async helpers that assertcode(anddetails.reasonwhere it matters), allow explicit exemptions where the contract really is "propagate this exact error". No broad "is this assertion specific enough" scanner.observers · lastMake a few hard-coded caps overridable so tests can actually reach them (fix: add iOS private AX snapshot fallback #758 was this class). Andevents.ndjsonhas no rotation cap — that's a bug, file it on its own.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.
Run the A9 measurement— done twice; ship thepaths-ignorefix now (A9-1), hold routing (A9-2) until the selector can express Apple ownership.events.ndjsonrotation 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