Skip to content

fix(test): eliminate vitest isolate:false cross-file state leaks#5964

Merged
killagu merged 3 commits into
nextfrom
worktree-vitest-isolate-false
Jun 9, 2026
Merged

fix(test): eliminate vitest isolate:false cross-file state leaks#5964
killagu merged 3 commits into
nextfrom
worktree-vitest-isolate-false

Conversation

@killagu

@killagu killagu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Motivation

The root vitest.config.ts runs the whole monorepo with pool: 'threads' + isolate: false. Under this mode every test file in a worker shares one Node realm (module registry, globalThis, module-level state). A couple of modules cached realm-global state without resetting it, which leaked across files/projects and produced nondeterministic CI failures (different files fail run-to-run).

This PR fixes the two deterministic leaks at their source.

Root causes & fixes

  1. @eggjs/utils import.ts — snapshot loader / isESM poisoning (cross-project).
    setSnapshotModuleLoader() set a module-level loader and flipped module-level isESM to false with no way to unset, and snapshot-import.test.ts's afterEach was a no-op. After that test ran, every later file in the worker resolved modules in CJS + snapshot mode and failed with Can not find plugin @eggjs/<x> / Cannot find module '@eggjs/<x>/package.json' (ajv-plugin, typebox-validate, view-nunjucks, standalone, …).

    • setSnapshotModuleLoader(undefined) now clears the loader and restores the auto-detected isESM; the test clears it in afterEach.
  2. @eggjs/mock mockContext() — wrong-app context reuse.
    mockContext() reused this.currentContext without checking the context belongs to this app. With a shared/lingering async-local context (multiple mm.app() apps in one realm, or isolate:false carry-over), app2.mockContext() returned app1's context, binding helpers/services to the wrong app config — e.g. security/surl custom-protocol returning '', security/csrf 401s.

    • Guarded the reuse with this.currentContext.app === this.

Test evidence (local, Node 22, utoo install)

  • Full suite under isolate:false, --retry 0: 15 failing files → 3.
  • The remaining 3 are not isolation bugs: orm-plugin (needs MySQL), security/ssrf (needs DNS) — both pass in CI; multipart/file-mode is a pre-existing load flake that also fails under isolate:true and is absorbed by CI's --retry 2.
  • With CI's actual --retry 2, the only remaining local failures are the two environmental ones above.

See wiki/concepts/vitest-isolate-false-state-leaks.md for the full diagnosis and triage method.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented module-format state from leaking across imports in the same runtime.
    • Tightened context reuse to stop state sharing across different application instances.
  • Tests

    • Simplified test cleanup to reliably reset snapshot loader state between tests.
  • Documentation

    • Added detailed docs and index entry explaining state leaks with Vitest pool: 'threads' + isolate: false, plus a dated log entry.

killa and others added 2 commits June 8, 2026 01:00
setSnapshotModuleLoader() set a module-level loader and flipped the
module-level `isESM` to false with no way to unset it, and
snapshot-import.test.ts had a no-op afterEach. Under vitest
`isolate: false` (root config uses pool:threads + isolate:false) the
module registry is shared across files, so after the snapshot test ran
every later file resolved modules in CJS + snapshot mode and failed with
`Can not find plugin @eggjs/<x>` / `Cannot find module
'@eggjs/<x>/package.json'`.

Make setSnapshotModuleLoader(undefined) clear the loader and restore the
auto-detected isESM, and clear it in the test teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mockContext() reused this.currentContext without checking the context
belongs to this app. When the async-local context storage is shared
across app instances (multiple mm.app() apps in one realm, or vitest
`isolate: false` carry-over) app2.mockContext() could return app1's
context, binding helpers/services to the wrong app config — e.g.
security `surl` custom-protocol returning '' and csrf 401s.

Guard the reuse with `this.currentContext.app === this`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 09:34
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Fixes two cross-realm test-state leaks under Vitest with pool: 'threads' and isolate: false: the snapshot module loader now restores the initially detected ESM/CJS mode when cleared, and mockContext() only reuses async-local context belonging to the same app instance. Documentation and tests updated.

Changes

Vitest isolate:false State Leak Fixes

Layer / File(s) Summary
Snapshot module loader state restoration
packages/utils/src/import.ts, packages/utils/test/snapshot-import.test.ts
Module-level constant detectedIsESM snapshots the initially auto-detected ESM format; `setSnapshotModuleLoader(loader: SnapshotModuleLoader
Mock app context scope tightening
plugins/mock/src/app/extend/application.ts
mockContext() only reuses an existing async-local context when currentContext.app === this and it is not marked reused, preventing cross-app context leaks.
State leak diagnosis and documentation
wiki/concepts/vitest-isolate-false-state-leaks.md, wiki/index.md, wiki/log.md
Adds a concept page documenting isolate:false state leaks, triage steps, root causes (snapshot loader, mock context reuse, undici dispatcher risk), and updates the wiki index and log to reference the investigation and fixes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • eggjs/egg#5853: Adds and modifies snapshot loader behavior in packages/utils/src/import.ts; related to snapshot import/module-registry handling and state restoration.

Suggested reviewers

  • jerryliang64
  • fengmk2

Poem

🐰 I hop through threads and tidy the lair,

where snapshots linger in open air.
Now loaders reset and contexts stay true,
each app keeps its bounds — tidy and new.
Tests breathe easy; the rabbit chews a carrot too.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(test): eliminate vitest isolate:false cross-file state leaks' directly and clearly summarizes the main objective of the pull request: eliminating cross-file state leaks caused by vitest's isolate:false configuration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-vitest-isolate-false

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: d8c6128
Status: ✅  Deploy successful!
Preview URL: https://adda386e.egg-v3.pages.dev
Branch Preview URL: https://worktree-vitest-isolate-fals.egg-v3.pages.dev

View logs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses nondeterministic test failures caused by cross-file state leaks when running Vitest with isolate: false. It fixes module-resolution poisoning in @eggjs/utils by restoring the auto-detected isESM state when clearing the snapshot module loader, and ensures that @eggjs/mock only reuses active async-local contexts belonging to the current application instance. Additionally, it documents these isolation issues and their fixes in the workspace wiki. No review comments were provided, so there is no feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two confirmed realm-global state leaks that show up when the monorepo test suite runs under Vitest with pool: 'threads' + isolate: false, where multiple test files share the same Node realm (module registry / globalThis / module-level state) and can nondeterministically affect each other.

Changes:

  • Add an explicit “unset” path for setSnapshotModuleLoader() that clears the snapshot loader and restores the originally auto-detected isESM mode.
  • Ensure the snapshot-import test suite resets snapshot-loader state in afterEach() to prevent cross-file contamination.
  • Prevent @eggjs/mock mockContext() from reusing an async-local context that belongs to a different app instance.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/utils/src/import.ts Makes snapshot-loader state reversible by restoring isESM when clearing the loader.
packages/utils/test/snapshot-import.test.ts Adds teardown to clear snapshot-loader state after each test.
plugins/mock/src/app/extend/application.ts Guards context reuse to avoid returning a context from a different app instance.
wiki/concepts/vitest-isolate-false-state-leaks.md Adds a concept doc explaining isolate:false leak mechanics and the specific fixes.
wiki/index.md Links the new concept doc from the wiki index.
wiki/log.md Logs the diagnosis and the source files/pages updated for this work.

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.32%. Comparing base (b015dce) to head (d8c6128).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5964      +/-   ##
==========================================
+ Coverage   85.30%   85.32%   +0.02%     
==========================================
  Files         670      670              
  Lines       19552    19553       +1     
  Branches     3863     3864       +1     
==========================================
+ Hits        16678    16683       +5     
+ Misses       2481     2479       -2     
+ Partials      393      391       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: d8c6128
Status: ✅  Deploy successful!
Preview URL: https://363afc32.egg-cci.pages.dev
Branch Preview URL: https://worktree-vitest-isolate-fals.egg-cci.pages.dev

View logs

Record the root causes (import.ts snapshot loader / isESM leak, mock
mockContext wrong-app reuse), the CI-faithful triage method (Node 22/24
+ utoo install, run-alone rule), and which remaining failures are
environmental or pre-existing load flakes rather than isolate bugs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@killagu killagu force-pushed the worktree-vitest-isolate-false branch from 47b0ce5 to d8c6128 Compare June 8, 2026 09:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
wiki/concepts/vitest-isolate-false-state-leaks.md (1)

24-29: ⚡ Quick win

Add explicit Inference: / staleness qualifiers for synthesized and time-sensitive claims.

The nondeterminism diagnosis and “15 → 3 failing files” outcome are synthesis and point-in-time observations, but they are not explicitly marked. Please tag those statements with Inference: and add a freshness qualifier (for example, “as of 2026-06-08”) to align with wiki policy.

As per coding guidelines: “List major source paths in source_files frontmatter and mark non-obvious synthesis as Inference:” and “mark stale or unresolved claims when freshness is uncertain.”

Also applies to: 91-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wiki/concepts/vitest-isolate-false-state-leaks.md` around lines 24 - 29, Mark
the synthesized, time-sensitive statements by prepending "Inference:" and adding
a freshness qualifier (e.g., "as of 2026-06-08") to the nondeterminism diagnosis
and the outcome phrase; specifically update the sentence containing "failures
are **nondeterministic**" and the phrase "15 → 3 failing files" to read with an
"Inference:" prefix and a date qualifier, and apply the same treatment to the
similar claims around lines referenced as 91-93 so all non-obvious synthesis and
potentially stale observations are explicitly labeled and dated.

Source: Coding guidelines

wiki/log.md (1)

9-9: ⚡ Quick win

Mark synthesized diagnosis text as Inference: in the log note.

This note mixes direct facts with causal synthesis (why failures happened and why remaining ones are excluded). Prefix the synthesized portion with Inference: so the log conforms to the wiki evidence-labeling rule.

As per coding guidelines: “mark non-obvious synthesis as Inference:”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wiki/log.md` at line 9, The log note mixes observed facts with synthesized
causal explanation; update the entry so the synthesized diagnosis is prefixed
with "Inference:" — for example, in the sentence describing the two realm-global
leaks, prepend "Inference:" before the explanation that (1)
setSnapshotModuleLoader left `_snapshotModuleLoader`/`isESM=false` set and (2)
mock.mockContext() reused `currentContext`, so the factual repro details remain
unchanged but the causal synthesis is clearly labeled as inference.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@wiki/concepts/vitest-isolate-false-state-leaks.md`:
- Around line 24-29: Mark the synthesized, time-sensitive statements by
prepending "Inference:" and adding a freshness qualifier (e.g., "as of
2026-06-08") to the nondeterminism diagnosis and the outcome phrase;
specifically update the sentence containing "failures are **nondeterministic**"
and the phrase "15 → 3 failing files" to read with an "Inference:" prefix and a
date qualifier, and apply the same treatment to the similar claims around lines
referenced as 91-93 so all non-obvious synthesis and potentially stale
observations are explicitly labeled and dated.

In `@wiki/log.md`:
- Line 9: The log note mixes observed facts with synthesized causal explanation;
update the entry so the synthesized diagnosis is prefixed with "Inference:" —
for example, in the sentence describing the two realm-global leaks, prepend
"Inference:" before the explanation that (1) setSnapshotModuleLoader left
`_snapshotModuleLoader`/`isESM=false` set and (2) mock.mockContext() reused
`currentContext`, so the factual repro details remain unchanged but the causal
synthesis is clearly labeled as inference.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf60ed5a-d4c3-4d10-8a01-db51e1fc4c40

📥 Commits

Reviewing files that changed from the base of the PR and between 47b0ce5 and d8c6128.

📒 Files selected for processing (3)
  • wiki/concepts/vitest-isolate-false-state-leaks.md
  • wiki/index.md
  • wiki/log.md
✅ Files skipped from review due to trivial changes (1)
  • wiki/index.md

@jerryliang64 jerryliang64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@killagu killagu merged commit 1836bce into next Jun 9, 2026
26 checks passed
@killagu killagu deleted the worktree-vitest-isolate-false branch June 9, 2026 12:06
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.

3 participants