Skip to content

fix(core): share imported module state and keep context-bound APIs live across files under isolate: false#1376

Draft
fi3ework wants to merge 1 commit into
mainfrom
fix-no-isolate-module-sharing
Draft

fix(core): share imported module state and keep context-bound APIs live across files under isolate: false#1376
fi3ework wants to merge 1 commit into
mainfrom
fix-no-isolate-module-sharing

Conversation

@fi3ework

@fi3ework fi3ework commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Under isolate: false, one worker process runs many test files: @rstest/core is reset per file (its module cache is evicted and re-imported) while user modules persist — a helper imported by several files is evaluated only once per worker. Two problems followed from that asymmetry, both fixed here.

1. Imported module state was not shared across files (#1373)

The worker teardown cleared the entire module cache after every file, evicting the shared per-worker runtime chunk that owns the only __webpack_module_cache__. The next file re-instantiated that runtime, so every bundled module factory ran again and module-scope state (singletons, caches, lazily-initialized resources) did not survive across files — defeating the main reason to opt into non-isolated mode.

Teardown invalidation is now selective:

  • Keep the shared runtime chunk so __webpack_module_cache__ — and the state of every already-evaluated non-entry module — persists across files. Only the test-entry and setup modules are evicted so their bodies re-run per file.
  • Accumulate per-entry asset maps into one persistent per-worker map, so a later file's chunks stay resolvable by the kept runtime chunk's import.meta hooks (wasm / dynamic-import resolution). The map is reset only on a full cache clear.
  • Normalize setup paths with pathe before matching, so setup files reliably re-run per file on Windows / mixed separators (previously a raw-path collection clobber left the targeted setup-reset list empty).

2. Context-bound APIs went stale when value-copied into a shared helper

Because user modules persist, any context-bound @rstest/core value captured at module top-level in a persisted helper — export const test = base.extend({}), a re-exported onTestFinished, { ...rstest }, or a snapshotted expect.poll — froze to the first file's torn-down context and silently misbehaved from the second file on.

Fixed with a single live-binding contract: each context-bound member is a stable forwarder, built once, that re-resolves the running file's state from a per-file global slot at call time — never closing over a per-file instance. The collection surface (runtimeAPI) and the full runner surface (runnerAPI) are built once; createRuntimeAPI only constructs the file's runtime and publishes its slot. expect additionally self-delegates to the live GLOBAL_EXPECT to rescue an already-copied expect.poll/.soft. The contract — the four slots, the shared value-copy residual, and why expect is the one self-delegating member — is documented in one place (runtime/api/index.ts).

3. Shared module state went stale across watch rebuilds

Sharing user modules means keeping the runtime chunk (which owns the only __webpack_module_cache__) across files. In watch mode the worker pool is reused across rebuilds, so that kept cache also survived rebuild boundaries — under isolate: false a changed shared module was re-run with the previous build's exports until the process restarted.

A per-compile buildId is now threaded to the worker. When it changes (every watch rebuild), the worker fully flushes its module cache before loading the new build, so each rebuild re-evaluates shared modules. Intra-build sharing across files is unchanged.

Behavior

With isolate: false, an imported module now evaluates once per worker and is shared across the files that run in it, and context-bound APIs always act on the running file; the test entry, setup files, and @rstest/core still reset per file. Applies to both ESM and CommonJS output.

A new e2e surface guard drives the whole context-bound API surface (test.extend, hooks, describe, expect.poll, rstest.fn/clearMocks) through a persisted shared helper from a non-first file; a watch regression test asserts that an edited shared module is re-evaluated on rerun; and the isolate docs note that module-scope code runs once per worker (use setup files for per-file setup).

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ad9c97bfa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/worker/runInPool.ts
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Rsdoctor Bundle Diff Analysis

Found 13 projects in monorepo, 2 projects with changes.

📊 Quick Summary
Project Total Size Change
adapter-rsbuild 4.1 KB 0
adapter-rslib 24.7 KB 0
adapter-rspack 8.0 KB 0
browser 2.0 MB 0
browser-react 3.7 KB 0
browser-ui 809.6 KB 0
coverage-istanbul 16.0 KB 0
coverage-v8 17.5 KB 0
core/browser 980.2 KB +2.1 KB (0.2%)
core/loaders 869.0 B 0
core/main 1.7 MB +3.5 KB (0.2%)
vscode/extension 27.0 MB 0
vscode/worker 14.5 KB 0
📋 Detailed Reports (Click to expand)

📁 core/browser

Path: packages/core/.rsdoctor/browser/rsdoctor-data.json

📌 Baseline Commit: 6360bcba54 | PR: #1398

Metric Current Baseline Change
📊 Total Size 980.2 KB 978.1 KB +2.1 KB (0.2%)
📄 JavaScript 972.1 KB 970.0 KB +2.1 KB (0.2%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 8.1 KB 8.1 KB 0

📦 Download Diff Report: core/browser Bundle Diff

📁 core/main

Path: packages/core/.rsdoctor/main/rsdoctor-data.json

📌 Baseline Commit: 6360bcba54 | PR: #1398

Metric Current Baseline Change
📊 Total Size 1.7 MB 1.7 MB +3.5 KB (0.2%)
📄 JavaScript 1.6 MB 1.6 MB +3.5 KB (0.2%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 81.6 KB 81.6 KB 0

📦 Download Diff Report: core/main Bundle Diff

Generated by Rsdoctor GitHub Action

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e703cf8d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/worker/loadEsModule.ts
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

pkg.pr.new preview 5e703cf · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/browser@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/browser-react@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/core@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@5e703cf
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@5e703cf

fi3ework added a commit that referenced this pull request Jun 4, 2026
Under `isolate: false` the shared-module fix persists user modules across
files, but `test.extend(...)` captured the per-file runner eagerly: a fixture
module doing `export const test = base.extend(...)` evaluated once, so from the
second file on every registration hit the first file's torn-down runner and
threw "Test '<name>' cannot run".

Make the runner API intrinsically late-bound, mirroring the existing
`RSTEST_API` proxy: publish the current file's runner as `globalThis.RSTEST_RUNTIME`
(reassigned per file) and resolve it at every leaf registration call instead of
closing over `runtimeInstance`. `getLocation` moves onto the runner so a
persisted extended test also computes locations against the current file.

Closes #1376.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

pkg.pr.new preview 101313b · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@101313b
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@101313b
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@101313b
  • pnpm add https://pkg.pr.new/@rstest/browser@101313b
  • pnpm add https://pkg.pr.new/@rstest/browser-react@101313b
  • pnpm add https://pkg.pr.new/@rstest/core@101313b
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@101313b
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@101313b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 101313be9f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/runner/runtime.ts
fi3ework added a commit that referenced this pull request Jun 4, 2026
A follow-up to the test.extend() fix: under `isolate: false` a context-bound
`@rstest/core` API value-copied into a module shared across files goes stale.
A shared helper doing `Object.assign(impl, expect)` (or `const { poll } = expect`)
captures the first file's concrete `expect`, whose `expect.poll` is bound to that
file's `getCurrentTest`; from the second file on it threw "expect.poll() must be
called inside a test".

Make the per-file `expect` self-delegate: when invoked from a stale cross-file
reference it forwards to the current file's live expect (`globalThis[GLOBAL_EXPECT]`,
reassigned per file), so test attribution and assertion state track the running
file. The per-test local expect leaves `isFileExpect: false` so it never delegates
and keeps `test.concurrent` assertion isolation. For the current file's own expect
`live === expect`, so this is a no-op fast path.

Refs #1376.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

pkg.pr.new preview 6a4cc28 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/browser@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/browser-react@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/core@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@6a4cc28
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@6a4cc28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a4cc28506

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/runner/runtime.ts Outdated
Comment thread packages/core/src/runtime/api/expect.ts Outdated
@fi3ework fi3ework marked this pull request as draft June 4, 2026 16:12
fi3ework added a commit that referenced this pull request Jun 4, 2026
…solate: false

Continues the #1376 family: under `isolate: false` a context-bound
`@rstest/core` API value-copied into a module shared across files goes stale.
The file-level hooks (`afterAll`/`beforeAll`/`afterEach`/`beforeEach`) were left
bound to the first file's runner, so a shared helper re-exporting a hook (e.g.
`export const beforeEach = beforeEach`) silently registered into the first
file's torn-down collector from the second file on. `onTestFinished`/
`onTestFailed` had the same staleness against the per-file test runner and threw
"can only be called inside a test".

Route the hooks through `currentRuntime()` (the per-file `RSTEST_RUNTIME`, like
`it`/`describe`), and `onTestFinished`/`onTestFailed` through a new per-file
`globalThis.RSTEST_RUNNER` resolved at call time. The remaining `rstest`/`rs`
utilities object is the one known, non-idiomatic residual (re-exporting the whole
namespace), deferred until a real repro surfaces.

Refs #1376.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

pkg.pr.new preview 41191b8 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@41191b8
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@41191b8
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@41191b8
  • pnpm add https://pkg.pr.new/@rstest/browser@41191b8
  • pnpm add https://pkg.pr.new/@rstest/browser-react@41191b8
  • pnpm add https://pkg.pr.new/@rstest/core@41191b8
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@41191b8
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@41191b8

@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying rstest with  Cloudflare Pages  Cloudflare Pages

Latest commit: 88fac87
Status: ✅  Deploy successful!
Preview URL: https://ee874416.rstest.pages.dev
Branch Preview URL: https://fix-no-isolate-module-sharin.rstest.pages.dev

View logs

@fi3ework

fi3ework commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 429ac70f7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/runtime/worker/runInPool.ts
fi3ework added a commit that referenced this pull request Jun 5, 2026
Under `isolate: false` a single worker runs many test files: `@rstest/core` is
reset per file (its module cache is evicted and re-imported, #1373) while user
modules persist — a helper imported by several files is evaluated only once.
Two problems followed from that asymmetry:

1. Context-bound `@rstest/core` APIs value-copied into a persisted helper (e.g.
   `export const test = base.extend({})`, a re-exported hook, `onTestFinished`,
   `{ ...rstest }`, or a snapshotted `expect.poll`) froze to the FIRST file's
   torn-down context and silently misbehaved from the second file on.

2. Setup files were matched against the module cache by raw path, so on Windows
   / mixed separators the cache-control plugin failed to re-run them per file.

Fix the binding problem with a single live-binding contract: each context-bound
member is a STABLE forwarder, built once at module load, that re-resolves the
running file's state from a per-file global slot at call time — never closing
over a per-file instance. The collection surface (`runtimeAPI`) and the full
runner surface (`runnerAPI`) are built once; `createRuntimeAPI` only constructs
the file's runtime and publishes its slot. `expect` additionally self-delegates
to the live `GLOBAL_EXPECT` to rescue an already-copied `expect.poll`/`.soft`.
The contract (the four slots, the shared value-copy residual, and why `expect`
is the one self-delegating member) is documented in one place,
`runtime/api/index.ts`.

Fix the setup-file problem by normalizing paths with `pathe` before matching.

Add an e2e surface guard that drives the whole context-bound API surface
(`test.extend`, hooks, `describe`, `expect.poll`, `rstest.fn`/`clearMocks`)
through a persisted shared helper from a non-first file, and document that
module-scope code runs once per worker under `isolate: false`.

Closes #1376
@fi3ework fi3ework force-pushed the fix-no-isolate-module-sharing branch from 429ac70 to a8d3b0f Compare June 5, 2026 03:46
@fi3ework fi3ework changed the title fix(core): share imported module state across files under isolate: false fix(core): share imported module state and keep context-bound APIs live across files under isolate: false Jun 5, 2026
@fi3ework fi3ework force-pushed the fix-no-isolate-module-sharing branch 7 times, most recently from bdf1fe3 to 4f70360 Compare June 8, 2026 11:23
Under `isolate: false` a single worker runs many test files: `@rstest/core` is
reset per file (its module cache is evicted and re-imported, #1373) while user
modules persist — a helper imported by several files is evaluated only once.
Two problems followed from that asymmetry:

1. Context-bound `@rstest/core` APIs value-copied into a persisted helper (e.g.
   `export const test = base.extend({})`, a re-exported hook, `onTestFinished`,
   `{ ...rstest }`, or a snapshotted `expect.poll`) froze to the FIRST file's
   torn-down context and silently misbehaved from the second file on.

2. Setup files were matched against the module cache by raw path, so on Windows
   / mixed separators the cache-control plugin failed to re-run them per file.

Fix the binding problem with a single live-binding contract: the whole injected
API surface (`runtimeAPI`, `runnerAPI`, `expect`, `rstest`) is built ONCE per
worker with a stable identity, and every context-bound member resolves the
running file's `FileContext` at call time — one module-level binding holding
the file's worker state, collection registrar, and test runner, republished as
one unit by `createRunner` — never closing over a per-file instance. Per-file
state is RESET, not rebuilt: `expect` clears its assertion bookkeeping and
re-establishes the live `testPath` getter, `rstest` drops its stub/timer/mock
bookkeeping and rewinds the shared `invocationCallOrder` counter. Because the
file-level `expect` is a build-once singleton, a
value-copied `expect.poll`/`.soft` can never go stale, so no cross-file
delegation is needed; the per-test local expect intentionally stays a pinned
per-test instance to keep `test.concurrent` isolation. Construction and
publication are separated (`createRuntimeAPI` is a pure factory; only
`createRunner` publishes the context), and globalThis carries only true
cross-boundary contracts (`RSTEST_API` for the user bundle, `GLOBAL_EXPECT`
for the assertion ecosystem). The contract is documented in one place,
`runtime/api/index.ts`.

Fix the setup-file problem by normalizing paths with `pathe` before matching.

Sharing user modules means keeping the runtime chunk (which owns the only
`__webpack_module_cache__`) across files. In watch mode the pool reuses the
worker across rebuilds, so that kept cache also survived rebuild boundaries and
a changed shared module was served stale from the previous build. Thread a
per-compile `buildId` to the worker and fully flush its module cache when the id
changes, so every rebuild re-evaluates shared modules while in-build sharing is
preserved.

Add an e2e surface guard that drives the whole context-bound API surface
(`test.extend`, hooks, `describe`, `expect.poll`, `rstest.fn`/`clearMocks`)
through a persisted shared helper from a non-first file, a watch regression test
asserting an edited shared module is re-evaluated on rerun, and document that
module-scope code runs once per worker under `isolate: false`.

Closes #1373
@fi3ework fi3ework force-pushed the fix-no-isolate-module-sharing branch from 4f70360 to 88fac87 Compare June 10, 2026 04:10
@fi3ework fi3ework marked this pull request as ready for review June 11, 2026 15:17
@fi3ework fi3ework marked this pull request as draft June 11, 2026 15:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88fac87241

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// unwound elsewhere (the runner's config-gated `unstubAll*`/`*AllMocks` and the
// per-file `useRealTimers`), so only the tracking maps/registry are cleared.
const resetForFile = (): void => {
mocks.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep shared mocks registered across files

When isolate: false shares a helper that creates export const mock = rstest.fn() while file A is collected, file B imports the cached helper rather than creating the mock again. This reset removes that still-live mock from the only mocks registry, so file B's configured clearMocks/resetMocks/restoreMocks hooks and manual rstest.clearAllMocks() no longer touch it, letting call history or implementations leak between tests despite the reset options.

Useful? React with 👍 / 👎.

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.

[Bug]: Imported module state is not shared across test files under isolate: false

1 participant