Skip to content

CI and test-suite rework: suffix-driven classes, one runner/planner, linear pipeline, flakes fixed for real - #6

Merged
iserdmi merged 11 commits into
mainfrom
ci-rework
Jul 9, 2026
Merged

CI and test-suite rework: suffix-driven classes, one runner/planner, linear pipeline, flakes fixed for real#6
iserdmi merged 11 commits into
mainfrom
ci-rework

Conversation

@iserdmi

@iserdmi iserdmi commented Jul 9, 2026

Copy link
Copy Markdown
Member

What

The full CI/test reorganization that came out of the v0.2.0 release-run flake cluster (instead of the quarantine chase on the archived ci-flakes branch).

Test classes in the filename

Every test file now carries its class — .unit (pure logic, 45) / .int (real processes/servers, no browser, 47) / .e2e (real browser, 18). The suffix IS the dispatch registry: scripts/slow-tests.ts and scripts/test-parallel.ts are gone, and the suffix is enforced against each file's contents (browser use — direct import or transitive gotoServer/gotoClient — ⇒ .e2e; real machinery ⇒ not .unit).

One runner/planner: scripts/test.ts

Discovers, verifies, plans and runs. Two lanes: parallel groups (unit / int-N / pinned heavies) + solo (every e2e + 4 heavy int). Wall-clock guard per file (tree-kill + browser reap), fresh-process retry, honest failure extraction, and "all green but the process won't exit" = pass with a loud ⚠, not a 10-minute red. scripts/*.unit.test.ts now actually run in CI (the old runner never scanned scripts/). Aliases: testf/testa/tests + test:unit|int|e2e|plan.

Linear pipeline

decide → build → check → test → gate/publish. check consumes the build artifact (no second build). Matrices self-size from the plan. The fast lane installs no Playwright at all (no browser files there by construction). --skip-tests is gone — every tag tests. New test-one.yml: dispatch one file on a chosen OS with a repeat count.

The three v0.2.0 flakes — fixed, zero quarantines

  1. Windows core-rsc non-exit → root-caused live on a Windows machine (bridge session): bun 1.3.14 busy-spins on the .unref()'d hole-deadline timer after it fires. fix(rsc): drop the .unref() — verified 5/5 clean exits on Windows; minimal repro recorded in dev/backlog/test-non-exit.md (bun issue pending).
  2. vite HMR pair → not load: since the ssr-batch the vite Fast Refresh remount is deterministic (documented product limitation, card open). Both tests now fork on vite right after the first edit and assert what vite guarantees (propagation; server half on a fresh page). Plus honest budgets: waitStarted 90s on vite, 15s waitContent ceilings.
  3. rsc.slow ubuntu rotation → the file carried 4 browser launches; now four rsc-*.e2e files (one launch per solo runner), chromium.launch retried 3×40s, prod server booted+warmed in beforeAll off the test budget.

Test plan

  • Local: full testa111/111 files, 0 fail, 0 warn, 15m42s; types + lint green.
  • CI: full matrix on this branch (--run-tests) — all ~59 jobs green on ubuntu+windows, no quarantines; Windows core-rsc exits clean in 431ms (was the 10-min timeout).
  • Windows fix verified 5/5 on a real Windows machine via the agent bridge.

After merge

  • Re-cut the release (0.2.1 or re-tag) and resume the passthrough (igrich/start0 migration edits are waiting).
  • Archive the ci-flakes branch per the archive-tag convention.
  • File the bun issue from the recorded repro.

iserdmi added 11 commits July 9, 2026 22:14
Classes: .e2e = drives a real browser (Playwright/CDP), .int = boots real
processes / dev servers / createTestThings / TestProject, .unit = pure
in-process logic. The former .fast/.slow pairs map to .int/.e2e. The suffix
becomes the single source of truth for test dispatch — the slow-tests.ts
registry dies in the follow-up.
The release commit bumped every package.json to 0.2.0 but the lock kept the
0.1.12 workspace entries; any bun install regenerates this diff.
Comments, dev docs, and the (soon to die) scripts registry now point at the
.unit/.int/.e2e names. CHANGELOG.md deliberately untouched — history stays
history.
The class suffix (.unit/.int/.e2e) is now the whole registry: test.ts discovers
files, ENFORCES the suffix against each file's real imports (browser import ⇒
.e2e, real machinery ⇒ not .unit), plans the two lanes (parallel groups + solo
= every e2e + the SOLO_INT heavies), and runs everything through one guarded
runner — wall-clock timeout, tree-kill + browser reap, fresh-process retry,
honest failure extraction (real markers only, tail fallback), and
passed-but-would-not-exit counted as a warned pass instead of a 10-minute red.

Replaces scripts/slow-tests.ts + scripts/test-parallel.ts. ci-decide now only
decides policy (oses/publish) and forwards the plan; --skip-tests is gone —
every tag tests, prerelease or stable. scripts/*.unit.test.ts now actually run
(the old runner never scanned scripts/). New aliases: testf = fast (unit+int
sans solo), testa = all, tests = solo lane, test:unit/int/e2e/plan.
build runs on every run and its dist artifact feeds check, test AND publish —
check no longer does its own duplicate build. Test matrices come straight from
the plan (scripts/test.ts via ci-decide): test-fast = one runner per group with
no Playwright at all (the fast lane carries no browser files by construction),
test-solo = one runner per heavy-int/e2e file through the same guarded runner
(the bash retry loop dies — retries live in test.ts). publish now requires the
whole pipeline green with no skipped-stage special case (--skip-tests is gone).
New test-one.yml: dispatch a single file on a chosen OS with a repeat count —
reproduce a flaky release-matrix leg without burning the full matrix.
…erve, retry chromium launch

rsc.e2e.test.tsx carried four browser launches; one bad describe cascaded into
the rest and the file concentrated 4× the launch-flake exposure on loaded CI
runners. Now each project (bun dev / bun build / vite dev / vite build) is its
own rsc-*.e2e file = its own solo runner with exactly one launch; the fixture
pages and every shared assertion body live in tests/utils/rsc-e2e.tsx.

Salvaged from the ci-flakes investigation (the quarantines were NOT taken):
chromium.launch retries 3× with a 40s per-attempt timeout (a pipe-terminated
launch fails fast instead of burning the 180s default), and the build files
boot + warm the prod server in beforeAll (warmProdServe polls /rsc with bounded
attempts) — the boot is off the per-test 60s budget, which was bimodal on
loaded runners (~730ms warm vs a full 60s hang).
…dgets

Root cause of the 'flaky' HMR pair: since the ssr-batch, vite Fast Refresh
remounts the point-rendered page on a file edit DETERMINISTICALLY (state resets,
'Hay 0'), so the state-continuation waits ('Hay 1'/'Hay 11') could never appear
on vite — that's a documented product limitation (vite-fast-refresh-point-state-
loss.md, architectural fix open), not a timing flake. Both tests now fork on
vite right after the first edit: the client test asserts propagation only (its
vite branch just starts earlier), the server test asserts the server half on a
FRESH page — state 0 + one click must return the edited inc: 10.

Load-hardening on top: waitStarted is 90s on vite projects (cold deps optimizer
— a slow start is not a failed start), the HMR pair's waitContent calls carry
15s ceilings (poll returns the moment content lands), and the client test
retries on a fresh project (retry 2) like its server sibling.

Cards: ci-flakes.md maps the whole v0.2.0 flake cluster to its fixes;
test-non-exit.md tracks the bun-on-Windows green-but-won't-exit hang the runner
now reports as a warned pass.
…sy-spin

Root-caused live on a Windows machine (bridge session): with the .unref() in
place, the fired deadline timer in this exact context (async subtree through
normalize + settle inside the superstore ALS) busy-spins the bun 1.3.14 event
loop at 100% forever — the green test file's process never exits, which is what
red-flagged every v0.2.0 Windows fast lane. A bare setTimeout().unref() does
not reproduce, and skipping the post-fire clearTimeout does not help — the
.unref() itself is the trigger. Verified 5/5 clean exits on Windows and the
full suite on macOS with the fix.

The settle-time clearTimeout already covers the normal path; the one semantic
change is bounded — an exit with a genuinely un-settled hole waits at most
holeTimeoutMs for the deadline to fire. Full story: dev/backlog/test-non-exit.md

--run-tests
… transitive launches

build.e2e/assets.e2e call tp.gotoServer(), which lazily inits a real
PlaywrightBrowser — no direct playwright import, so the class check missed them
and the solo runner skipped the Playwright install (the one red job of the
first full-matrix run: 'Executable doesn't exist'). verifyClasses now treats
gotoServer/gotoClient/initBrowser/setBrowser as browser markers, so a
transitive launch can't hide in .int again; both files leave SOLO_INT (e2e is
solo by construction).

--run-tests
A defer() whose subtree never settles + the fired unref'd deadline timer is
enough (needs @point0/core); four dependency-free pure-JS variants all exit
clean, so the trigger lives in the promise/timer topology of register(). Bun
issue is the remaining step. --skip-ci
--skip-ci in a PR's tip commit used to skip the whole matrix while the gate
stayed green — an untested change could merge. decide now resolves the
pull_request branch before the flag check, so the only thing that skips a PR's
matrix is a provably docs-only diff; --skip-ci/--run-tests remain effective on
the maintainer's own branch pushes. Pinned by a new invariant test.
@iserdmi
iserdmi merged commit 90a2b41 into main Jul 9, 2026
65 checks passed
@iserdmi
iserdmi deleted the ci-rework branch July 9, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant