Skip to content

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

@fi3ework

Description

@fi3ework

Version

System:
  OS: macOS (Darwin 24)
npmPackages:
  @rstest/core: 0.10.3

Also present on main (commit 956d5cc2).

Details

Under isolate: false, a module that is imported into multiple test files is re-evaluated once per test file within the same worker, instead of once per worker. Module-level state therefore does not persist across files, even though the worker process is reused.

Expected: with isolate: false, a module evaluated in a worker should be reused across the test files that run in that worker (evaluate once); only the test-entry module should re-run per file. State held at module scope (singletons, lazily-initialized resources, caches) should survive across files.

Actual: the module's top-level code runs again for every file, resetting all module-scope state.

Root cause (verified in source)

Worker teardown unconditionally clears the entire module cache after every file when !isolate:

  • runtime/worker/runInPool.ts:419-423 — teardown calls clearModuleCache() whenever !isolate.
  • runtime/worker/loadEsModule.ts:349-352clearModuleCache()esmCache.clear() (ESM / .mjs output). The CJS path is the same: runtime/worker/loadModule.ts:336-339moduleCache.clear().

The bundle is already structured to share state across files: every test entry statically imports a single per-worker runtime chunk (rstest-runtime.mjs) that owns the only module-instance cache (__webpack_module_cache__). Because that runtime chunk also lives in esmCache, clearModuleCache() evicts it too — so the next file re-instantiates the runtime, gets a fresh __webpack_module_cache__, and every bundled module factory (including imported helpers) re-executes.

The modules that should re-run per file already have a precise, targeted eviction path that does not require the blanket clear:

  • runtime/worker/runInPool.ts:329-332 injects __rstest_clean_core_cache__() per file.
  • core/plugins/moduleCacheControl.ts:15-26 — it deletes only __webpack_module_cache__['@rstest/core'] and the registered setup-file ids (setupIds).

So @rstest/core and setup files are already reset per file by their own mechanism; the additional blanket clearModuleCache() is what defeats cross-file sharing of every other module.

Suggested direction

Make module-cache invalidation selective under isolate: false: keep the runtime chunk and already-evaluated non-entry modules in the cache, and evict only the current test-entry module (so its body re-runs), letting the existing __rstest_clean_core_cache__ mechanism continue to handle @rstest/core + setup files. Note the test entry's SourceTextModule still needs to be evicted from esmCache for the entry to re-run.

Reproduce

No external repro needed — the behavior is deterministic with a single worker (both files land in the same worker, so shared.ts should evaluate exactly once).

Minimal reproduction (key files)
// shared.ts — module-scope state; should initialize once per worker
console.log('[shared] evaluated, pid =', process.pid);
let value: number | undefined;
export const getValue = () => (value ??= Date.now());
// a.test.ts
import { expect, test } from '@rstest/core';
import { getValue } from './shared';
test('a', () => {
  console.log('[a] getValue =', getValue());
  expect(getValue()).toBeDefined();
});
// b.test.ts
import { expect, test } from '@rstest/core';
import { getValue } from './shared';
test('b', () => {
  console.log('[b] getValue =', getValue());
  expect(getValue()).toBeDefined();
});
// rstest.config.ts
import { defineConfig } from '@rstest/core';

export default defineConfig({
  isolate: false,
  // force both files into one worker for a deterministic repro
  pool: { maxWorkers: 1 },
});

Reproduce Steps

  1. Run rstest.
  2. Observe that [shared] evaluated is logged twice (once per test file) with the same pid, and that [a] getValue / [b] getValue print different values.
  3. Expected with isolate: false: [shared] evaluated logged once, and both files observe the same getValue().

Encountered while migrating the e2e test suite in web-infra-dev/rsbuild#7776.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions