The execArgv fix (PR #200) made vitest actually honor the 8 GB heap
cap — and the next CI run promptly proved that 8 GB is still not
enough. Log: every test passes but the worker hits
"Ineffective mark-compacts near heap limit" at exactly 8011 MB,
the new ceiling. Doubling again to 16 GB would be near the
GitHub-runner total RAM (16 GB) and start swapping.
The real culprit is per-file leak accumulation: 117 test files
share one vitest fork; each file lazy-loads ngspice WASM
(~24 MB), wires up MixedModeScheduler / zustand singletons, and
leaves some of that state alive in module-level closures even
after the file finishes. Sum over the suite ≈ 8 GB+ retained.
Split the run with vitest's built-in `--shard N/M`:
- matrix.shard: [1, 2] alongside matrix.node-version: [20, 22]
= 4 parallel runners
- each runner executes `npx vitest run --shard ${shard}/2`
- vitest hashes file paths into deterministic shards (same
flaky file always lands in the same shard)
- each runner only carries ~60 files of leak state → fits in
the existing 8 GB cap from poolOptions.forks.execArgv
Coverage upload gated to shard 1 / node 22 to avoid the two
shards racing to overwrite the same artifact name. Coverage
itself runs once on the full suite (best-effort, may OOM, but
`continue-on-error: true` keeps it non-blocking).
The real fix is dispose hooks on the leaking singletons, but
that's a multi-PR cleanup of code paths I haven't touched in
this work item; sharding unblocks CI in the meantime.