Repository navigation
fix(test): isolate process.env to stop flaky auth/pairs suite #151 - #161
Conversation
…e656#151) Root cause: Vitest threads pool shares one process across parallel files, so process.env mutations (ADMIN_API_KEY, ADMIN_TOKEN, venue flags, etc.) race mid-assertion. Switch to forks, enforce env snapshot/restore setup, and keep ADMIN_API_KEY set through pairs inject.
Miracle656
left a comment
There was a problem hiding this comment.
Take this one over #160 — you opened two PRs for #151 nine minutes apart, they conflict with each other (both add vitest.setup.ts and edit vitest.config.ts), and only one can land. This is the better of the two, for a specific reason.
Why this one
The decisive difference is when vitest.setup.ts snapshots process.env. This PR snapshots in beforeEach; #160 snapshots once at module load and restores after every test — which wipes anything a file set in its own beforeAll from test 2 onward. Demonstrated with an identical probe under each setup:
### under #160's setup
✓ test 1 sees it
→ expected undefined to be 'set-in-beforeAll'
Tests 1 failed | 1 passed (2)
### under this PR's setup
✓ test 1 sees it
✓ test 2 still sees it
Tests 2 passed (2)
That is also why #160 had to convert pairIssuerMatch.test.ts from beforeAll to beforeEach — it was patching damage its own setup file caused. As an always-on shared setup file, that is a landmine for every future test.
Three changes before I merge
1. The stated root cause is wrong. The header comment says "Vitest's default threads pool runs multiple files concurrently inside one Node process". Vitest 4.1.5's default pool is already forks — I probed inside a test and got isMainThread = true, i.e. a child process. Files do not run concurrently in one process, and process.env cannot race concurrently across them. (Env does persist across files run sequentially in a reused fork, which is the real mechanism — #160's phrasing is closer.) Please correct the comment and the PR body; a wrong explanation in a shared setup file will mislead whoever touches it next.
2. pool: 'forks' in vitest.config.ts is a no-op — it is already the default. Keep it as an explicit pin if you like, but the comment should say "pinning the existing default", not imply a change.
3. The timeouts are too loose. testTimeout: 60_000 and a 120s property timeout will hide a genuine hang as a slow pass. #160 used 20s; take that, and 30s for the property test.
Then cherry-pick #160's extra per-file restores in src/__tests__/usage.test.ts and src/__tests__/x402-quota.test.ts — strictly additive and compatible with your setup file.
Two things to know
The flake is real — I reproduced it three times, always in a full run, always passing in isolation, with rotating victims exactly as #151 describes. An earlier check of mine got seven green runs and wrongly concluded it was fixed; it was luck. So this PR is worth having.
But neither PR touches the root hazard. src/config.ts:245-253 memoises each NetworkConfig in a module-level Map on first access. Restoring process.env does not invalidate _networkCache — only vi.resetModules() does. Both PRs are test-side only, so the memoisation footgun survives either merge. I will keep #151 open with that noted, or open a follow-up.
Also: both PR bodies claim 20/20 passed. With no CI on fork PRs that is unverifiable, and I managed four runs per branch — not enough to confirm a roughly 1-in-10 flake either way. Worth being precise about what you actually observed.
…eilings Review follow-up on Miracle656#151. - vitest.setup.ts: the header blamed Vitest's `threads` pool running files concurrently in one process. Vitest 4's default pool is already `forks`, so files never share a process; what actually leaks is `process.env` *within* a reused fork, which runs several files sequentially. Comment corrected. - vitest.config.ts: `pool: 'forks'` pins the existing default rather than changing it, and the comment now says so. `testTimeout` 60_000 -> 20_000 so a genuine hang fails the run instead of passing slowly. - tests/aggregator.property.test.ts: property timeout 120_000 -> 30_000. - src/__tests__/usage.test.ts, src/__tests__/x402-quota.test.ts: adopt the per-file env restores from Miracle656#160 (additive, compatible with the shared setup).
|
Corrected the write-up and tightened the ceilings, @Miracle656 — specifically the three points from your review:
I also cherry-picked #160's per-file restores in Pushed as a single commit on the head branch. Could you take another look when you have a moment? |
Miracle656
left a comment
There was a problem hiding this comment.
Re-reviewed at d713f17. All three changes are in, and I verified the fix does what it claims rather than taking the green on trust. Merging.
The three points
| # | Point | Status |
|---|---|---|
| 1 | Stated root cause is wrong (threads vs forks) |
Fixed |
| 2 | pool: 'forks' presented as a change |
Fixed |
| 3 | 60s/120s timeouts too loose | Fixed |
| — | Cherry-pick #160's per-file restores | Done |
On 1, the new header in vitest.setup.ts says it correctly — "Vitest 4's default pool is already forks, so files do not run concurrently in one process. What leaks instead is process.env within a fork — a reused child process runs several files sequentially". That is the real mechanism. On 2, the comment now reads "This pins Vitest's existing default rather than changing it". On 3, testTimeout: 20_000 and the property test at 30_000 — and you also dropped the stray second }, 15000) argument that was silently overriding the inline option. Good catch, I had not spotted that one.
src/__tests__/usage.test.ts and src/__tests__/x402-quota.test.ts both carry #160's per-file restores, and auth.test.ts now deletes rather than assigning undefined (which Node stringifies to "undefined"). All strictly additive.
Verification
The branch is 21 commits behind main, so I tested the merge result, not the tip — the tip alone runs 47 files / 401 tests and would have told me nothing about main's nine newer suites.
Baseline, origin/main, cold cache:
Test Files 4 failed | 51 passed | 1 skipped (56)
Tests 3 failed | 529 passed | 6 skipped (538)
FAIL src/__tests__/middleware/x402.test.ts > x402 middleware > returns 200 when payment header is valid
Error: Test timed out in 5000ms.
main + this PR, merged locally, four runs (three warm, one with node_modules/.vite deleted to force a cold transform):
Test Files 55 passed | 1 skipped (56)
Tests 537 passed | 1 skipped (538) ×4
The cold run is the interesting one — 69s wall, 226s of import work, and nothing timed out. The 5s default was a live flake source independent of env leakage, and testTimeout: 20_000 closes it. The merge itself is clean; your changes to tests/aggregator.property.test.ts and vitest.config.ts do not collide with main's.
Still open, as a follow-up and not against you
src/config.ts memoises each NetworkConfig in a module-level _networkCache on first access, and restoring process.env does not invalidate it — only vi.resetModules() does. Your setup file cannot reach that, and neither could #160. I am keeping #151 open with that noted so the memoisation footgun is tracked separately.
Thanks for the turnaround, and for correcting the explanation rather than just the symptom — a wrong comment in an always-on setup file would have cost someone real time later.
Overview
Fixes intermittent Vitest failures (~1 in 5 full runs) where
auth.test.tsandpairs.test.ts(and rotating victims such asnetworkVenueConfig.test.ts) fail only in the full suite — never in isolation.Root cause
process.envleaks between files that share a test worker. Vitest 4's default pool is alreadyforks, so files never run concurrently in a single process against each other; the mechanism is that a reused fork child process runs several files sequentially, and an env key set by one file is still set when the next file starts.POST /pairsreadsADMIN_API_KEYat request time, so a preceding file that left the key cleared yields a spurious 401; other suites inherit stale venue flags the same way. That is why the failures rotate and always pass in isolation.networkVenueConfigadditionally saw the 5s default exceeded under fork load whenresetModules+SDK import ran.Related Issue
Closes #151
Changes
Isolation (the real fix)
vitest.setup.ts— snapshotprocess.envinbeforeEach, restore inafterEach(enforced, not convention).vitest.config.ts—pool: 'forks'pins the existing Vitest default (kept explicit, not a behavioural change);setupFiles;testTimeout: 20_000— tight enough that a genuine hang fails rather than passing slowly.Test hygiene
src/__tests__/pairs.test.ts— stop restoringADMIN_API_KEYbeforeinject(route reads env at request time); clear inafterEach.src/__tests__/auth.test.ts— restoreADMIN_TOKENwithdeletewhen originally unset (Node stringifiesundefined→"undefined").src/__tests__/usage.test.ts,src/__tests__/x402-quota.test.ts— per-file env restores adopted from fix(tests): isolate process.env to stop flaky auth/pairs failures #160 (additive, compatible with the shared setup file).tests/aggregator.property.test.ts— property-case timeout120_000→30_000.Verification Results
Pre-fix baseline on the same machine: ~1/10 full runs failed (
networkVenueConfig/ property timeout / env races). After fix: 20 consecutive greens.process.envleaking across sequentially-reused fork workerspool: 'forks'keeps file parallelism (and is the default anyway)vitest.setup.ts