Investigation Β· CI & test strategy
Are the tests worth it, and where should each gate live?
You sent a voice memo asking six things about our test/CI/deploy machinery. This is the investigation you asked for β your questions as sections, answered with measured data where it exists, marked as opinion where it doesn't. Every number is tagged: π’ measured (run ID / API), π‘ estimate (source named), π΄ opinion.
"I'm just going to ramble. I hope you transcribe this, but I'm just curious β how valuable are these tests, because this has been a constant problem this week where tests are blocking and merges are blocking developer progress. We want high throughput, smooth systemsβ¦ design, plan, implement, open PR, hopefully PR reviewed and merged as fast as possible β and a huge bottleneck has been the PR reviewer, but that's a pretty necessary system. Then the other is making sure we're not merging any issues. So we do need tests, but the question becomes where do we do all this? β¦ Maybe merge to main but deploy in batches β every hour or two, and those batches are where the tests runβ¦ and we just always work on preview. β¦ how do we reduce this gap? β¦ the GitHub reviewer β do we need multi-turn or is one turn enough? β¦ we have over 30,000 tests, so I don't want to create so many that it balloons and bloats our quality and friction. Should we give expiration dates to certain testsβ¦ remove tests once they expireβ¦ slim down tests, or test per impact? So if we change file B, only test file B and maybe adjacent siblings. Analyze the current infrastructure and propose a preview or artifactβ¦ use pretty HTML so I can follow along."
TL;DR β the six answers in one screen
| Your question | Short answer | Confidence |
|---|---|---|
| 1. Are the tests worth it? | Yes β but not equally. Round-1 review + the affected-subset suite earn their keep; the full suite on every event does not. | π’/π΄ |
| 2. Where should each gate live? | Already fixed tonight: cheap affected tests gate PR + main; the full suite is nightly + hub-escalation. Batching is a downgrade from what we now have. | π’ |
| 3. Is the reviewer earning its cost? | Round 1 yes (67% of PRs change after it). Round 2+ is a coin flip β make it opt-in. | π‘ |
| 4. Test expiration / lifecycle | Don't expire by date. Expire by coverage + failure signal. Concrete mechanism below. | π΄ |
| 5. Test-per-impact (file B β test B + siblings) | ~60% shipped tonight (the affected-graph model). The remaining 40% is sibling-adjacency + the deletion-safety story. | π’ |
| 6. A pretty HTML doc to follow along | You're reading it. | π’ |
Methodology & data sources
Every number below traces to one of: the GitHub billing API
(real spend export), the Actions runs API (per-job billed
minutes, ceil to the 1-min floor GitHub bills at), or a replay
(real merged trees re-run through the new selection logic). The measured
dataset β 145 push-run timings Jul 22β23, step-level before/afters, the
30-push replay split, per-actor claude.yml attribution, and the
review-family run counts β was gathered during the 2026-07-23 cost sprint.
π’ measured = a run ID or the billing API backs it Β· π‘ estimate = derived, source named Β· π΄ opinion = a recommendation, not a measurement.
1 Β· Are the tests worth it?
The honest answer is that "the tests" is the wrong unit β the question is which tests, run how often. We have π‘ ~30k tests (your number; the suite runs 12-way sharded). The cost was never the tests existing β it was running all of them on every merge.
π’ Before tonight, the full 12-shard suite ran on
every push:main β 46 full runs on Jul 23, 51 on Jul 22
β at 57β59 billed job-min each (runs API, median 59, IDs
30052721572β¦30060253715). That's ~2,700 job-min/day on the test
lane alone, the single biggest line item behind July running 172% of the
$200 budget.
π΄ So: tests are worth it as a merge gate (they stop real regressions β see Β§3, where the bot + suite caught a prod-freezing deploy bug tonight). They are not worth running exhaustively on every event. The fix isn't fewer tests β it's running the relevant ones per event and the full set on a cadence. Which is exactly what shipped tonight.
2 Β· Where should each gate live? (and the batched-deploy question)
π’ As of tonight the gates are placed like this:
| Event | What runs | Why |
|---|---|---|
| PR push | Affected-subset tests (capped) + review round 1 | Fast feedback; catches the author's own diff |
| Merge to main | Affected-subset again (its own diff) β deploy gate | Every merge tested exactly for what it changed |
| Hub-sized merge (>200 files) | Escalates to the full 12-shard suite | Wide blast radius still gets full coverage before deploy |
| Nightly | Full 12-shard suite | Cross-file backstop for anything the affected graph misses |
π’ Result, measured on the first real runs: push:main billed cost 59 β 10 job-min (β83%), mergeβprod promote 4.8 β 3.7 min, and the reviewer stays where it's cheap (the branch).
On "deploy in batches (hourly/2h/6h)"
π΄ I'd argue against it, and here's the trade with tonight's numbers as the baseline. Batching means: merge freely, hold deploys, run heavy tests at the batch boundary, ship on a timer. The case for it is that you run the expensive suite fewer times. But we just achieved that without batching β the affected-subset model already cut the per-merge cost 83%, and the full suite already only runs nightly + on hub-merges. Batching would buy little additional cost saving.
π’ What batching would cost: latency. Tonight mergeβprod is 3.7 min continuous. An hourly batch reintroduces up to 60 min of deploy staleness by construction; a 6h batch, up to 6h. It also re-creates the "which of the N merged PRs broke the batch?" untangling problem that the merge queue had (that pain is exactly why ADR-0107 retired the queue).
On "always work on preview / preview mirrors raw data with PII protection"
π΄ We're close but not there. Previews are opt-in
today (a [preview] commit builds one). "Always on preview with a
real-time data mirror" is a bigger build β it needs the anonymized-subset
pipeline (ADR-0097 already rewrites PII in-flight into a fake
+1206/@example.test convention for stage). Wiring
that into a per-branch live preview is a genuine project, not a config flip.
Worth scoping separately if you want it; flagging honestly that it's not
a tonight-sized change.
3 Β· Is the GitHub reviewer earning its cost? (multi-turn vs one turn)
π‘ Proxy method, stated as such: "a commit landed after review round N" can include unrelated pushes, so these are upper bounds on review-driven change. Sample: last 57 merged PRs with bot reviews, ex-Dependabot.
| Signal | Value | Read |
|---|---|---|
| PRs that changed after review round 1 | 67% | Round 1 clearly earns its keep |
| PRs that go multi-round (2+) | 44% | Nearly half |
| β¦of those, any code change after round 2 | 48% | A coin flip |
| Median review rounds / max | 1 / 7 | One PR ate seven rounds |
| Cost per extra round | ~5.6 min | Wall-clock, blocks merge |
π’ Round 1 is not the thing to cut β tonight alone the bot caught a deploy-ordering race that would have frozen every prod promote, plus real bugs in the cost-sprint scripts. That's the reviewer paying for itself in a single night.
review check auto-passes when the delta
is small and touches only files round 1 already saw; anything bigger (new
files, >N lines, workflow/security paths) still demands full re-review, and
anyone can force one with @claude review. Trade: we accept small
unreviewed deltas on already-reviewed files to save ~6β12 min/multi-round PR.
This one needs your sign-off β it's a quality tradeoff, not a pure win.
To make this π’ instead of π‘: trace whether post-round-2 commits actually touch the lines round 2 flagged. That's scriptable and it's the next cut.
4 Β· Test expiration & lifecycle
π΄ Don't expire tests by date β a calendar-old test can be the one guarding your highest-traffic path. Expire by signal. Concrete mechanism proposal:
- Tag, don't delete. Add a machine-readable pragma to a test's header β the reason it exists (regression for bug #N, drift guard, characterization). A test with no stated reason is the candidate, not a test that's simply old.
- Prune on two signals, both required: (a) zero unique coverage β every line it touches is covered by another test β and (b) it has never failed (never caught a real regression) across a long window. Either alone is not enough.
- Deletion-safety story is non-negotiable. "Slim the suite" without one is exactly how the #2923 / #2874 class of regressions came back (drift guards deleted, then the drift landed unnoticed). Any pruning PR must show the coverage-delta of what it removes.
π΄ Honest caution: pruning is a real project with a real downside, and the savings are modest (tests are cheap to hold; they're expensive to run exhaustively β which Β§2 already fixed). I'd rank this below shipping the round-2-opt-in and the sibling-adjacency work.
5 Β· Test-per-impact (change file B β test B + siblings)
π’ This is ~60% shipped as of tonight. The
affected-graph model (vitest --changed) now gates both
the PR lane and the main lane β a change selects only the test files whose
import graph touches the diff. That's your "test file B" already live.
π΄ The open 40%:
- Sibling-adjacency selection β the "and maybe adjacent siblings" half. The import graph catches direct dependents; it doesn't catch a sibling that shares behavior without importing. Proposal: extend selection to the directory-sibling set for changed files, gated behind the same cap so it can't balloon.
- The escalation backstop stays. Whatever the selector misses, the nightly full suite + the hub-merge escalation catch β that's the safety net that makes aggressive selection safe. Don't remove it.
What already shipped tonight (so you can see the baseline)
| PR | Change | Measured effect |
|---|---|---|
| #4444 | Affected-subset on push:main + hub escalation | push cost 59β10 job-min (β83%) |
| #4450 | tscβtsgo type-check | ~240s β 23s (β83%) |
| #4451 | Native-arm worker build (killed QEMU) | 18 min β 3.0 min build (β83%) |
| #4454 | Stale-run cancellation on all 16 PR workflows + drift guard | β |
π’ End-to-end PR-open β live in prod, measured on a controlled canary through the full gate: 43 min median β 8.2 min (β81%). The final cost verdict is one complete billed day under the new model (Jul 24) against the $200/mo budget β pulled from the billing API when the day closes.
Codex pressure-test β my take and Codex's, side by side
Per Gera's ask, the whole design above was fed to OpenAI Codex (independent model, full context, explicitly told not to hand-wave agreement). Verbatim verdict: "The cost posture is sensible, but the system is not production-grade as written." Below is where we agree, where I concede, and where I push back β nothing softened.
| Topic | My design | Codex's take | Resolution |
|---|---|---|---|
| Promotion evidence | Each push tests its own event.before..sha range; tip-of-main guard prevents out-of-order promotes. |
π΄ Sharpest catch: the tip guard proves ordering, not completeness. If push A's run goes red, push B's green run (which tested only A..B) still promotes a deploy containing A's untested regression. Wants a per-artifact deployment cursor: test last-deployed-SHA..candidate, hold promotion if anything in that cumulative range lacks evidence. |
Concede β real gap, adopt. This is the strongest finding in the review. Promote should verify every push:main run since the last deployed SHA is green (or re-test the cumulative range), per deployable. |
| Hub escalation | >200 affected files β full 12-shard suite before deploy. | Threshold is a capacity control, not a risk model β a 5-line auth/config change is riskier than 500 generated files. Wants risk-class rules (lockfile, workflow YAML, config, env contracts, shared test setup β always full) + full-suite debt that persists to later candidates until a full run passes. | Concede both. Risk-class escalation rules are a small ci.yml diff; debt propagation folds into the deployment-cursor work above. |
| 24h nightly backstop | Accepted risk window; nightly red pages a human. | Not sane alone β but notes the ~12 hub-escalation full suites/day shrink the effective gap to ~2h on average. Wants: freshness rule (no green full suite in N hours of active deploying β schedule one), post-deploy smoke per artifact, promotion freeze on red, and push-workflow-absence alerting (the exact dark-deploy failure we already lived through tonight). | Adopt smoke + freeze + absence-alert; freshness rule over a fixed midday run. Reject universal auto-rollback β Codex itself flags worker/Lambda side effects + Temporal replay compatibility make blind rollback worse; freeze-then-human is right for those. |
| Auto-merge | Armed at PR-open for all PRs; PAT so merges fire push workflows (#4474). | Arm at ready, not open (green β author intent). PAT = personnel blast radius β use a scoped GitHub App token. Re-recommends merge queue (merge_group) so evidence covers the actual merge candidate. Wants CODEOWNERS human review on security/CI/billing/migration surfaces. |
Concede arm-at-ready + App token + CODEOWNERS-on-sensitive-paths (all cheap). Push back on the merge queue: we measured it for a week β 66 trains for 63 PRs, it never batched, median time-to-merge 20+ min (ADR-0107). Codex is arguing from first principles without that history; the cumulative-cursor fix addresses the same correctness concern without resurrecting the queue. |
| Reviewer round-2 opt-in | Round 1 mandatory; round 2+ opt-in (48% of round-2s show no follow-up commit). | π΄ False economy: 48% is not low, and "any later commit" is a weak value metric. Wants delta-scoped incremental round 2 (auto-skip only formatting/docs/generated), full re-review forced on new files / deps / CI / auth / migrations, and explicitly: don't classify safety by line count β small post-review fixes are where subtle mistakes live. | Concede. Tonight is its own receipt: the bot's round-2 on #4444 caught two real bugs in my round-1 fix, one of which would have frozen every prod deploy. Withdraw the blanket opt-in; propose delta-scoped incremental re-review instead. |
| Test pruning | No date expiry; prune by coverage-overlap + failure-history signal. | Coverage+history is only candidate generation β coverage proves execution, not equivalent assertions. Concrete pipeline: classify by criticality (incident-derived tests tagged), candidate-local mutation testing (retained tests must kill the same mutants), prefer consolidation over deletion, deletion PR carries the evidence, revert-to-restore. Explicit non-reasons: "never failed", "old", "slow", "flaky". | Adopt wholesale β it's my Β§4 direction made concrete, and candidate-local mutation testing is the missing safety mechanism. Codex agrees a tombstone database would be over-engineered at our size. |
| Sibling-adjacency (memo Q5) | Extend selection to directory siblings, capped. | Weak heuristic β runs irrelevant tests while still missing remote semantic consumers (Temporal contracts, SQS payloads, generated schemas). Only adopt if shadow data proves recall improves. | Concede β demote. Replace with selector-correctness telemetry first (shadow-run unselected tests on a sample, track false-negative recall); only ship adjacency if the data says it earns its cost. |
Gaps Codex named that the design missed entirely
- π΄ Affected-graph blind spots as a class: dynamic imports, Next.js root layouts/middleware, config/env/lockfile changes, fixtures/snapshots/prompts opened by pathname, generated clients, deletions/renames, and cross-artifact semantic edges (Temporal signals, SQS payloads, webhook contracts) that no import graph sees. Fix: explicit broad-impact rules + never silently cap the affected set (cap-crossing must escalate, not truncate).
- Flaky-test policy: record first-attempt failures even when retry passes; quarantine lane with owner + issue + expiry; never quarantine critical smoke/contract tests indefinitely.
- Shard integrity: assert the 12-shard manifest is complete + disjoint β a test silently dropped from discovery is invisible today.
- Immutable artifact promotion: deploy the exact tested build digest; don't rebuild post-test and call it equivalent.
- Cache trust boundaries: untrusted PR branches must not write caches trusted main builds restore; include selector version + shard manifest in cache keys.
- LLM-reviewer as single point of failure: audited break-glass path with human approval; treat repo prompt-injection as a real threat (the bot holds no secrets or mutation authority β true today, keep it pinned).
The production-ready sequence (revised for the review)
- Now (correctness, small diffs): deployment cursor + full-suite debt in
vercel-promote/deploy gates Β· risk-class escalation rules (lockfile/workflow/config β full) Β· push-workflow-absence alert Β· arm-at-ready Β· App token replaces the PAT. - Next (safety net): per-artifact post-deploy smoke + promotion freeze Β· full-suite freshness rule Β· shard-integrity assertion Β· flake quarantine lane.
- Then (efficiency, data-gated): selector-correctness telemetry β sibling-adjacency only if recall data supports it Β· consolidation-based test pruning with candidate-local mutation testing Β· delta-scoped incremental round-2 review (replaces the opt-in proposal).
The three decisions this leaves you
- Batched deploys? My read: no, as a cost lever (Β§2). Yes, only if you want a human release gate β decide that in the deploy-model doc.
- Round-2+ reviews: the blanket opt-in proposal is withdrawn after the Codex review (and after round-2 caught two real bugs in my own #4444 fix tonight). The replacement on the table: delta-scoped incremental re-review β auto-skip only formatting/docs/generated, full re-review forced on new files / deps / CI / auth / migrations.
- Sibling-adjacency + test pruning? Both buildable, both ranked below the two above because tonight's work already captured most of the cost. Say the word and I'll scope them as their own PRs.