Skip to content

fix(loader-fs): ignore vitest environment-teardown import races#5980

Merged
killagu merged 1 commit into
nextfrom
fix/mock-teardown-import-race
Jun 21, 2026
Merged

fix(loader-fs): ignore vitest environment-teardown import races#5980
killagu merged 1 commit into
nextfrom
fix/mock-teardown-import-race

Conversation

@killagu

@killagu killagu commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Motivation

Test (ubuntu-latest, 24) flakily fails with:

EnvironmentTeardownError: Cannot load '<file>' ... after the environment was torn down. This is not a bug in Vitest.

Observed in CI loading tegg/plugin/controller's duplicate-proto-name-app/.../AppController.ts, and reproduced locally in the @eggjs/mock suite loading logrotator's rotate_by_file.ts — failing unrelated tests (e.g. mock_httpclient_next "should auto restore after each case").

Root cause (traced)

The framework loads files via dynamic import() through RealLoaderFS.loadFile. Instrumenting the dangling load shows it is a legitimate, fully-awaited boot-time load — e.g. the schedule plugin loading schedule files during configDidLoad:

RealLoaderFS.loadFile  ← getExports  ← ScheduleLoader.parse/.load
  ← loadSchedule  ← Scheduler.init (agent) / ScheduleWorker.init (app)  ← Boot.configDidLoad

Every link awaits: loadSchedule awaits the loader, configDidLoad awaits loadSchedule, ready() awaits configDidLoad. Timers / serverDidReady / strategy start() do not import. So there is no fire-and-forget async leak to fix at the source.

It outlives teardown because of the suite's isolate: false (threads) config: an import() issued while constructing an app in one test file's module-runner environment can have follow-up settling (tsx transform / transitive resolution) that completes after vitest tears that environment down to move to the next file. vitest itself labels this "This is not a bug in Vitest". loadFile wrapped and rethrew it, so it became an unhandled rejection that, under isolate: false, gets blamed on whatever unrelated test file is running. Same class of teardown race addressed for a different symptom in #5978.

Fix

Because the load is correct and awaited, there's nothing to fix at the source — the only artifact is the benign vitest-internal error. Detect it in loadFile (by name === 'EnvironmentTeardownError' and/or the "after the environment was torn down" message, walking the cause chain) and resolve undefined instead of throwing.

  • Gated on process.env.VITEST so it is unmistakably a test-runner-only accommodation that can never alter production loader behavior.
  • Loaders already treat an empty module as "no exports", so this is a safe no-op.
  • Genuine load-time failures still propagate unchanged.

Test evidence

  • Empirical: on next, pnpm vitest run plugins/mock/test fails with the EnvironmentTeardownError; with this fix that error is gone. (A separate, pre-existing mock error cross-file leak from app.test.ts/agent.test.ts remains — different root cause, out of scope; it was previously masked by the teardown error.)
  • Unit: new loader-fs regression tests assert loadFile resolves undefined for a teardown-style import error and still throws (wrapped) for a genuine load failure. Full @eggjs/loader-fs suite: 4 passed.
  • oxlint --type-aware, oxfmt --check, and tsgo --noEmit clean on the changed files.

Note: the same import()-races-teardown pattern also exists in @eggjs/tegg-loader's LoaderUtil.loadFile; observed failures all flowed through @eggjs/loader-fs, so this PR fixes that path. The tegg path can get the same guard as a follow-up if it surfaces.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved module-loading error handling during test runs: if a module import occurs after the test environment has been torn down, the loader now treats it as a benign no-op and resolves undefined instead of failing.
    • True load-time failures are still reported as before with the expected loader-fs error message.
  • Tests
    • Added new fixtures and Vitest coverage to verify teardown-related import races are handled gracefully, while genuine load errors still reject.

Copilot AI review requested due to automatic review settings June 20, 2026 15:43

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a5ca73e-1e25-48ec-a8b7-d6b82baf0afa

📥 Commits

Reviewing files that changed from the base of the PR and between 631404a and ecd2b4a.

📒 Files selected for processing (4)
  • packages/loader-fs/src/index.ts
  • packages/loader-fs/test/fixtures/loadfile/normal-error.js
  • packages/loader-fs/test/fixtures/loadfile/teardown-error.js
  • packages/loader-fs/test/loader_fs.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/loader-fs/test/fixtures/loadfile/teardown-error.js
  • packages/loader-fs/test/fixtures/loadfile/normal-error.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/loader-fs/test/loader_fs.test.ts
  • packages/loader-fs/src/index.ts

📝 Walkthrough

Walkthrough

RealLoaderFS.loadFile gains a new isEnvironmentTeardownError helper that detects Vitest environment-teardown failures by inspecting an error's name, message, and up to 5 levels of nested cause. When matched, loadFile logs a debug message and returns undefined instead of throwing. Two test fixtures and two new test cases validate both paths.

Changes

Vitest Teardown Error Handling in loadFile

Layer / File(s) Summary
isEnvironmentTeardownError helper and loadFile catch extension
packages/loader-fs/src/index.ts
Adds isEnvironmentTeardownError that walks up to 5 cause levels for EnvironmentTeardownError name or the "after the environment was torn down" message substring. Uses it in RealLoaderFS.loadFile's catch block to return undefined instead of re-throwing for matched teardown errors.
Test fixtures and new test cases
packages/loader-fs/test/fixtures/loadfile/teardown-error.js, packages/loader-fs/test/fixtures/loadfile/normal-error.js, packages/loader-fs/test/loader_fs.test.ts
teardown-error.js throws an EnvironmentTeardownError; normal-error.js throws a generic load-time error. Two test cases assert that loadFile resolves undefined for the teardown case and rejects with the [@eggjs/loader-fs] load file: prefix for the genuine failure.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • gxkl
  • akitaSummer
  • jerryliang64

Poem

🐇 Hippity-hop through the test suite's night,
When vitest tears down mid-flight,
No error to throw, no panic to spread—
Just a quiet undefined returned instead.
The real boom still bubbles, wrapped up with care,
A rabbit who knows which errors to spare! 🌙

🚥 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(loader-fs): ignore vitest environment-teardown import races' accurately and concisely summarizes the main change: handling EnvironmentTeardownError exceptions that occur during vitest teardown when imports are still in flight.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mock-teardown-import-race

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 20, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: ecd2b4a
Status: ✅  Deploy successful!
Preview URL: https://72b774fd.egg-cci.pages.dev
Branch Preview URL: https://fix-mock-teardown-import-rac.egg-cci.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 introduces a mechanism to detect and gracefully handle Vitest's EnvironmentTeardownError during dynamic imports in RealLoaderFS.loadFile. By swallowing this specific error and returning undefined, it prevents stray dynamic imports from causing unhandled rejections after a test environment has been torn down. The review feedback suggests improving the type safety and robustness of the error-traversing helper isEnvironmentTeardownError by explicitly checking that the traversed value is an object or function before accessing its properties.

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.

Comment on lines +27 to +37
function isEnvironmentTeardownError(e: unknown): boolean {
let cur = e as { name?: unknown; message?: unknown; cause?: unknown } | undefined | null;
for (let depth = 0; cur && depth < 5; depth++) {
if (cur.name === 'EnvironmentTeardownError') return true;
if (typeof cur.message === 'string' && cur.message.includes('after the environment was torn down')) {
return true;
}
cur = cur.cause as typeof cur;
}
return false;
}

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.

medium

To improve type safety and runtime robustness, we should explicitly check that cur is an object or function before accessing its properties. Although JavaScript allows property access on most primitives, explicitly verifying the type prevents potential issues and aligns with cleaner TypeScript practices.

function isEnvironmentTeardownError(e: unknown): boolean {
  let cur = e;
  for (let depth = 0; cur && depth < 5; depth++) {
    if (typeof cur === 'object' || typeof cur === 'function') {
      const err = cur as { name?: unknown; message?: unknown; cause?: unknown };
      if (err.name === 'EnvironmentTeardownError') return true;
      if (typeof err.message === 'string' && err.message.includes('after the environment was torn down')) {
        return true;
      }
      cur = err.cause;
    } else {
      break;
    }
  }
  return false;
}

@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.60%. Comparing base (4f49d4b) to head (ecd2b4a).
⚠️ Report is 11 commits behind head on next.

Files with missing lines Patch % Lines
packages/loader-fs/src/index.ts 91.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             next    #5980   +/-   ##
=======================================
  Coverage   85.59%   85.60%           
=======================================
  Files         669      669           
  Lines       19892    19904   +12     
  Branches     3942     3947    +5     
=======================================
+ Hits        17026    17038   +12     
  Misses       2478     2478           
  Partials      388      388           

☔ 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 20, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: ecd2b4a
Status: ✅  Deploy successful!
Preview URL: https://09c463a3.egg-v3.pages.dev
Branch Preview URL: https://fix-mock-teardown-import-rac.egg-v3.pages.dev

View logs

@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 (3)
packages/loader-fs/test/fixtures/loadfile/teardown-error.js (1)

1-10: 💤 Low value

Consider using .ts or .mjs extension for test fixtures.

The coding guidelines specify using TypeScript throughout all packages. While this fixture serves its purpose as .js, consider renaming it to .ts or .mjs for consistency with the monorepo's TypeScript-first approach.

♻️ Optional refactor to TypeScript

Rename to teardown-error.ts:

-'use strict';
-
 // Simulates a module whose import loses the race with a vitest test-environment
 // teardown: the runtime raises an `EnvironmentTeardownError`. `RealLoaderFS.loadFile`
 // should treat this as a benign no-op and resolve `undefined` instead of throwing.
 const err = new Error(
   "Cannot load 'teardown-error.js' imported from import.ts after the environment was torn down. This is not a bug in Vitest.",
 );
 err.name = 'EnvironmentTeardownError';
 throw err;
🤖 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 `@packages/loader-fs/test/fixtures/loadfile/teardown-error.js` around lines 1 -
10, The test fixture teardown-error.js uses a JavaScript extension but the
monorepo follows a TypeScript-first approach for consistency across all
packages. Rename the file teardown-error.js to teardown-error.ts to align with
the coding guidelines and maintain consistency with the rest of the fixture
files in the monorepo.

Source: Coding guidelines

packages/loader-fs/src/index.ts (1)

29-29: 💤 Low value

Consider extracting the depth limit as a named constant.

The hardcoded 5 in the loop condition could be extracted as a named constant (e.g., MAX_CAUSE_DEPTH = 5) for better clarity and maintainability.

♻️ Optional refactor
+const MAX_CAUSE_DEPTH = 5;
+
 // Detect vitest's "environment was torn down" error, raised when a dynamic
 // import() resolves after the test environment that issued it has already been
 // torn down. It is reported by name (`EnvironmentTeardownError`) and/or message,
 // possibly nested behind a `cause`, so walk a short cause chain. This condition
 // exists only in the vitest runner during teardown, never in production.
 function isEnvironmentTeardownError(e: unknown): boolean {
   let cur = e as { name?: unknown; message?: unknown; cause?: unknown } | undefined | null;
-  for (let depth = 0; cur && depth < 5; depth++) {
+  for (let depth = 0; cur && depth < MAX_CAUSE_DEPTH; depth++) {
     if (cur.name === 'EnvironmentTeardownError') return true;
     if (typeof cur.message === 'string' && cur.message.includes('after the environment was torn down')) {
       return true;
     }
     cur = cur.cause as typeof cur;
   }
   return false;
 }
🤖 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 `@packages/loader-fs/src/index.ts` at line 29, The for loop in the file has a
hardcoded depth limit value of 5 in the condition depth < 5. Extract this magic
number as a named constant (e.g., MAX_CAUSE_DEPTH with a value of 5) at the
module level, and then replace the hardcoded 5 in the for loop condition with
this constant reference. This improves code readability and makes it easier to
adjust the depth limit in the future.
packages/loader-fs/test/fixtures/loadfile/normal-error.js (1)

1-4: 💤 Low value

Consider using .ts or .mjs extension for test fixtures.

Like teardown-error.js, this fixture could use a .ts or .mjs extension to align with the monorepo's TypeScript-first guidelines.

♻️ Optional refactor to TypeScript

Rename to normal-error.ts:

-'use strict';
-
 // A genuine load-time failure must still propagate (wrapped) from loadFile.
 throw new Error('boom: real load failure');
🤖 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 `@packages/loader-fs/test/fixtures/loadfile/normal-error.js` around lines 1 -
4, Rename the test fixture file `normal-error.js` to `normal-error.ts` to align
with the monorepo's TypeScript-first guidelines and maintain consistency with
other similar fixture files in the codebase. The file content can remain the
same, only the file extension needs to be changed from `.js` to `.ts`.

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 `@packages/loader-fs/src/index.ts`:
- Line 29: The for loop in the file has a hardcoded depth limit value of 5 in
the condition depth < 5. Extract this magic number as a named constant (e.g.,
MAX_CAUSE_DEPTH with a value of 5) at the module level, and then replace the
hardcoded 5 in the for loop condition with this constant reference. This
improves code readability and makes it easier to adjust the depth limit in the
future.

In `@packages/loader-fs/test/fixtures/loadfile/normal-error.js`:
- Around line 1-4: Rename the test fixture file `normal-error.js` to
`normal-error.ts` to align with the monorepo's TypeScript-first guidelines and
maintain consistency with other similar fixture files in the codebase. The file
content can remain the same, only the file extension needs to be changed from
`.js` to `.ts`.

In `@packages/loader-fs/test/fixtures/loadfile/teardown-error.js`:
- Around line 1-10: The test fixture teardown-error.js uses a JavaScript
extension but the monorepo follows a TypeScript-first approach for consistency
across all packages. Rename the file teardown-error.js to teardown-error.ts to
align with the coding guidelines and maintain consistency with the rest of the
fixture files in the monorepo.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56bd7a2b-397d-4f79-9ee3-ef8e288b5634

📥 Commits

Reviewing files that changed from the base of the PR and between 4f49d4b and 18afdaa.

📒 Files selected for processing (4)
  • packages/loader-fs/src/index.ts
  • packages/loader-fs/test/fixtures/loadfile/normal-error.js
  • packages/loader-fs/test/fixtures/loadfile/teardown-error.js
  • packages/loader-fs/test/loader_fs.test.ts

@killagu killagu force-pushed the fix/mock-teardown-import-race branch from 18afdaa to 92a43fc Compare June 20, 2026 16:14
@killagu

killagu commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Refined (92a43fcf): scoped the swallow explicitly to the vitest runner — if (process.env.VITEST && isEnvironmentTeardownError(e)). The EnvironmentTeardownError identity already cannot occur outside vitest, so this is belt-and-braces, but it makes the test-only intent unmistakable at the swallow site and guarantees production loader behavior is never altered. The teardown error only arises in vitest’s in-process module runner (where VITEST is set); cluster/child-process workers run real Node and never hit it. loader-fs suite still 4/4 green; lint/fmt/typecheck clean.

Copilot AI review requested due to automatic review settings June 20, 2026 16:22
@killagu killagu force-pushed the fix/mock-teardown-import-race branch from 92a43fc to 631404a Compare June 20, 2026 16:22

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

`Test (ubuntu-latest, 24)` flakily failed with `EnvironmentTeardownError:
Cannot load '<file>' ... after the environment was torn down. This is not a
bug in Vitest.` (e.g. loading `duplicate-proto-name-app`'s `AppController.ts`,
or `logrotator`'s `rotate_by_file.ts`).

## Root cause

The framework loads files via dynamic `import()` through
`RealLoaderFS.loadFile`. Tracing the dangling load shows it is a *legitimate,
fully-awaited boot-time load* — e.g. the schedule plugin loading schedule
files in `configDidLoad` (`loadFile <- getExports <- ScheduleLoader <-
loadSchedule <- Scheduler.init/ScheduleWorker.init <- configDidLoad`). There
is no fire-and-forget async leak.

It outlives teardown because of the suite's `isolate: false` (threads) config:
an `import()` issued while constructing an app in one test file's module-runner
environment can have follow-up settling (tsx transform / transitive resolution)
that completes after vitest tears that environment down to move to the next
file. vitest itself labels this "not a bug in Vitest". `loadFile` wrapped and
rethrew it, so it became an unhandled rejection that, under `isolate: false`,
is attributed to whatever unrelated test file is running — failing it at
random. Same class of teardown race as #5978, different symptom.

## Fix

Since the load is correct and awaited, there is nothing to fix at the source;
the only artifact is the benign vitest-internal error. Detect it in `loadFile`
(by `name === 'EnvironmentTeardownError'` and/or the "after the environment was
torn down" message, walking the `cause` chain) and resolve `undefined` instead
of throwing. Loaders already treat an empty module as "no exports", so this is
a safe no-op; genuine load-time failures still propagate unchanged.

The detection is gated on `process.env.VITEST`, so it is unmistakably a
test-runner-only accommodation that can never alter production loader behavior.

Verified against the `@eggjs/mock` suite: on `next` it fails with this
teardown error; with the fix that error is gone. Adds regression coverage for
both the swallow and the still-throws-on-real-error paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@killagu killagu force-pushed the fix/mock-teardown-import-race branch from 631404a to ecd2b4a Compare June 20, 2026 16:25
@killagu killagu merged commit 6be44d2 into next Jun 21, 2026
37 of 38 checks passed
@killagu killagu deleted the fix/mock-teardown-import-race branch June 21, 2026 01:38
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.

2 participants