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-352 — clearModuleCache() → esmCache.clear() (ESM / .mjs output). The CJS path is the same: runtime/worker/loadModule.ts:336-339 → moduleCache.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
- Run
rstest.
- Observe that
[shared] evaluated is logged twice (once per test file) with the same pid, and that [a] getValue / [b] getValue print different values.
- 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.
Version
Also present on
main(commit956d5cc2).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 callsclearModuleCache()whenever!isolate.runtime/worker/loadEsModule.ts:349-352—clearModuleCache()→esmCache.clear()(ESM /.mjsoutput). The CJS path is the same:runtime/worker/loadModule.ts:336-339→moduleCache.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 inesmCache,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-332injects__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/coreand setup files are already reset per file by their own mechanism; the additional blanketclearModuleCache()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'sSourceTextModulestill needs to be evicted fromesmCachefor 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.tsshould evaluate exactly once).Minimal reproduction (key files)
Reproduce Steps
rstest.[shared] evaluatedis logged twice (once per test file) with the samepid, and that[a] getValue/[b] getValueprint different values.isolate: false:[shared] evaluatedlogged once, and both files observe the samegetValue().Encountered while migrating the e2e test suite in web-infra-dev/rsbuild#7776.