Repository: nodejs/node
Pull request #57924: test_runner: unify --require and --import behavior when isolation none
Author: login: pmarchini
Labels: name: process
description: Issues and PRs related to the process subsystem.
name: author ready
description: PRs that have at least one approval, no pending requests for changes, and a CI started.
name: needs-ci
description: PRs that need a full CI run.
name: commit-queue-squash
description: Add this label to instruct the Commit Queue to squash all the PR commits into the first one.
name: test_runner
description: Issues and PRs related to the test runner subsystem.
name: backport-requested-v22.x
description: PRs awaiting manual backport to the v22.x-staging branch.
Created: 2025-04-18T13:45:36Z | Merged: 2025-05-06T06:28:29Z

I'm opening this PR as a draft to discuss the implementation.  
The goal is to address issue #57728.  
This PR unifies the behaviour of `--require` and `--import` for the specific case where the test runner is run with isolation `none`.


Diff:
diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js
index 87c7dca048b72d..fda47897da9f06 100644
--- a/lib/internal/main/test_runner.js
+++ b/lib/internal/main/test_runner.js
@@ -5,8 +5,8 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
@@ -16,10 +16,12 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
-markBootstrapComplete();
-
 const options = parseCommandLine();
+const isTestIsolationDisabled = options.isolation === 'none';
+// We set initializeModules to false as we want to load user modules in the test runner run function
+// if we are running with --test-isolation=none
+prepareTestRunnerMainExecution(!isTestIsolationDisabled);
+markBootstrapComplete();
 
 if (isUsingInspector() && options.isolation === 'process') {
   process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' +
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index 65c161584f3d34..8877184fcc54df 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -50,6 +50,15 @@ function prepareMainThreadExecution(expandArgv1 = false, initializeModules = tru
   });
 }
 
+function prepareTestRunnerMainExecution(loadUserModules = true) {
+  return prepareExecution({
+    expandArgv1: false,
+    initializeModules: true,
+    isMainThread: true,
+    forceDefaultLoader: !loadUserModules,
+  });
+}
+
 function prepareWorkerThreadExecution() {
   prepareExecution({
     expandArgv1: false,
@@ -87,7 +96,7 @@ function prepareShadowRealmExecution() {
 }
 
 function prepareExecution(options) {
-  const { expandArgv1, initializeModules, isMainThread } = options;
+  const { expandArgv1, initializeModules, isMainThread, forceDefaultLoader } = options;
 
   refreshRuntimeOptions();
 
@@ -147,7 +156,7 @@ function prepareExecution(options) {
   }
 
   if (initializeModules) {
-    setupUserModules();
+    setupUserModules(forceDefaultLoader);
   }
 
   return mainEntry;
@@ -712,6 +721,7 @@ module.exports = {
   prepareMainThreadExecution,
   prepareWorkerThreadExecution,
   prepareShadowRealmExecution,
+  prepareTestRunnerMainExecution,
   markBootstrapComplete,
   loadPreloadModules,
   initializeFrozenIntrinsics,
diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js
index bbc2515473c41d..0f40fa1016b504 100644
--- a/lib/internal/test_runner/runner.js
+++ b/lib/internal/test_runner/runner.js
@@ -88,6 +88,7 @@ const {
 const { Glob } = require('internal/fs/glob');
 const { once } = require('events');
 const { validatePath } = require('internal/fs/utils');
+const { loadPreloadModules } = require('internal/process/pre_execution');
 const {
   triggerUncaughtException,
   exitCodes: { kGenericUserError },
@@ -692,6 +693,7 @@ function run(options = kEmptyObject) {
   };
   const root = createTestTree(rootTestOptions, globalOptions);
   let testFiles = files ?? createTestFileList(globPatterns, cwd);
+  const { isTestRunner } = globalOptions;
 
   if (shard) {
     testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
@@ -765,6 +767,16 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // BEFORE the creation of the root test (that would cause them to get lost).
+          if (isTestRunner) {
+            // If we are not coming from the test runner entry point, the user-required and imported
+            // modules have already been loaded.
+            // Since it's possible to delete modules from require.cache, a CommonJS module
+            // could otherwise be executed twice.
+            loadPreloadModules();
+          }
           const userImports = getOptionValue('--import');
           for (let i = 0; i < userImports.length; i++) {
             await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
diff --git a/test/known_issues/test-runner-no-isolation-hooks.mjs b/test/known_issues/test-runner-no-isolation-hooks.mjs
deleted file mode 100644
index 7ef087ce0158ab..00000000000000
--- a/test/known_issues/test-runner-no-isolation-hooks.mjs
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as common from '../common/index.mjs';
-import * as fixtures from '../common/fixtures.mjs';
-import { test } from 'node:test';
-
-const testArguments = [
-  '--test',
-  '--test-isolation=none',
-];
-
-const testFiles = [
-  fixtures.path('test-runner', 'no-isolation', 'one.test.js'),
-  fixtures.path('test-runner', 'no-isolation', 'two.test.js'),
-];
-
-const order = [
-  'before(): global',
-
-  'before one: <root>',
-  'suite one',
-
-  'before two: <root>',
-  'suite two',
-
-  'beforeEach(): global',
-  'beforeEach one: suite one - test',
-  'beforeEach two: suite one - test',
-
-  'suite one - test',
-  'afterEach(): global',
-  'afterEach one: suite one - test',
-  'afterEach two: suite one - test',
-
-  'before suite two: suite two',
-  'beforeEach(): global',
-  'beforeEach one: suite two - test',
-  'beforeEach two: suite two - test',
-
-  'suite two - test',
-  'afterEach(): global',
-  'afterEach one: suite two - test',
-  'afterEach two: suite two - test',
-
-  'after(): global',
-  'after one: <root>',
-  'after two: <root>',
-].join('\n');
-
-/**
- * TODO: The `--require` flag is processed in `loadPreloadModules` (process/pre_execution.js) BEFORE
- * the root test is created by the test runner. This causes a global `before` hook to register (and
- * run) but then the root test-case is created, causing the "subsequent" hooks to get lost. This
- * behaviour (CJS route only) is different from the ESM route, where test runner explicitly handles
- * `--import` in `root.runInAsyncScope` (test_runner/runner.js).
- * @see https://github.com/nodejs/node/pull/57595#issuecomment-2770724492
- * @see https://github.com/nodejs/node/issues/57728
- * Moved from test/parallel/test-runner-no-isolation-hooks.mjs
- */
-test('use --require to define global hooks', async (t) => {
-  const { stdout } = await common.spawnPromisified(process.execPath, [
-    ...testArguments,
-    '--require', fixtures.path('test-runner', 'no-isolation', 'global-hooks.cjs'),
-    ...testFiles,
-  ]);
-
-  const testHookOutput = stdout.split('\n▶')[0];
-
-  t.assert.equal(testHookOutput, order);
-});
diff --git a/test/parallel/test-runner-global-setup-teardown.mjs b/test/parallel/test-runner-global-setup-teardown.mjs
index eceba774d161e3..9498efbc73fc5b 100644
--- a/test/parallel/test-runner-global-setup-teardown.mjs
+++ b/test/parallel/test-runner-global-setup-teardown.mjs
@@ -388,11 +388,16 @@ async function runTest(
       const cjsPath = join(testFixtures, 'global-setup-teardown', 'required-module.cjs');
       const esmpFile = fixtures.fileURL('test-runner', 'global-setup-teardown', 'imported-module.mjs');
 
-      it('should run required module before globalSetup', async () => {
+      // The difference in behavior is due to how --require and --import are handled by
+      // the main entry point versus the test runner entry point.
+      // When isolation is 'none', both --require and --import are handled by the test runner.
+      const shouldRequireAfterSetup = runnerEnabled && isolation === 'none';
+      const shouldImportAfterSetup = runnerEnabled;
+
+      it(`should run required module ${shouldRequireAfterSetup ? 'after' : 'before'} globalSetup`, async () => {
         const setupFlagPath = tmpdir.resolve('setup-for-required.tmp');
         const teardownFlagPath = tmpdir.resolve('teardown-for-required.tmp');
 
-        // Create a setup file for test-file.js to find
         fs.writeFileSync(setupFlagPath, '');
 
         const { stdout } = await runTest({
@@ -415,108 +420,63 @@ async function runTest(
         assert.match(stdout, /Global setup executed/);
         assert.match(stdout, /Global teardown executed/);
 
-        // Verify that the required module was executed before the global setup
         const requiredExecutedPosition = stdout.indexOf('Required module executed');
         const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
-        assert.ok(requiredExecutedPosition < globalSetupExecutedPosition,
-                  'Required module should have been executed before global setup');
 
-        // After all tests complete, the teardown should have run
+        assert.ok(
+          shouldRequireAfterSetup ?
+            requiredExecutedPosition > globalSetupExecutedPosition :
+            requiredExecutedPosition < globalSetupExecutedPosition,
+          `Required module should have been executed ${shouldRequireAfterSetup ? 'after' : 'before'} global setup`
+        );
+
         assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
         const content = fs.readFileSync(teardownFlagPath, 'utf8');
         assert.strictEqual(content, 'Teardown was executed');
-
-        // Setup flag should have been removed by teardown
         assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
       });
 
-      // This difference in behavior is due to the way --import is being handled by
-      // run_main entry point or test_runner entry point
-      if (runnerEnabled) {
-        it('should run imported module after globalSetup', async () => {
-          const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
-          const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
-
-          // Create a setup file for test-file.js to find
-          fs.writeFileSync(setupFlagPath, 'non-empty');
-
-          const { stdout } = await runTest({
-            isolation,
-            globalSetupFile: 'basic-setup-teardown.mjs',
-            importPath: './imported-module.js',
-            env: {
-              SETUP_FLAG_PATH: setupFlagPath,
-              TEARDOWN_FLAG_PATH: teardownFlagPath
-            },
-            additionalFlags: [
-              `--import=${esmpFile}`,
-            ],
-            runnerEnabled
-          });
-
-          assert.match(stdout, /pass 2/);
-          assert.match(stdout, /fail 0/);
-          assert.match(stdout, /Imported module executed/);
-          assert.match(stdout, /Global setup executed/);
-          assert.match(stdout, /Global teardown executed/);
-
-          // Verify that the imported module was executed after the global setup
-          const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
-          const importedExecutedPosition = stdout.indexOf('Imported module executed');
-          assert.ok(globalSetupExecutedPosition < importedExecutedPosition,
-                    'Imported module should be executed after global setup');
-
-          // After all tests complete, the teardown should have run
-          assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
-          const content = fs.readFileSync(teardownFlagPath, 'utf8');
-          assert.strictEqual(content, 'Teardown was executed');
-
-          // Setup flag should have been removed by teardown
-          assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
-        });
-      } else {
-        it('should run imported module before globalSetup', async () => {
-          const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
-          const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
-
-          // Create a setup file for test-file.js to find
-          fs.writeFileSync(setupFlagPath, 'non-empty');
-
-          const { stdout } = await runTest({
-            isolation,
-            globalSetupFile: 'basic-setup-teardown.mjs',
-            importPath: './imported-module.js',
-            env: {
-              SETUP_FLAG_PATH: setupFlagPath,
-              TEARDOWN_FLAG_PATH: teardownFlagPath
-            },
-            additionalFlags: [
-              `--import=${esmpFile}`,
-            ],
-            runnerEnabled
-          });
-
-          assert.match(stdout, /pass 2/);
-          assert.match(stdout, /fail 0/);
-          assert.match(stdout, /Imported module executed/);
-          assert.match(stdout, /Global setup executed/);
-          assert.match(stdout, /Global teardown executed/);
-
-          // Verify that the imported module was executed before the global setup
-          const importedExecutedPosition = stdout.indexOf('Imported module executed');
-          const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
-          assert.ok(importedExecutedPosition < globalSetupExecutedPosition,
-                    'Imported module should be executed before global setup');
-
-          // After all tests complete, the teardown should have run
-          assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
-          const content = fs.readFileSync(teardownFlagPath, 'utf8');
-          assert.strictEqual(content, 'Teardown was executed');
-
-          // Setup flag should have been removed by teardown
-          assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
+      it(`should run imported module ${shouldImportAfterSetup ? 'after' : 'before'} globalSetup`, async () => {
+        const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
+        const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
+
+        fs.writeFileSync(setupFlagPath, 'non-empty');
+
+        const { stdout } = await runTest({
+          isolation,
+          globalSetupFile: 'basic-setup-teardown.mjs',
+          importPath: './imported-module.js',
+          env: {
+            SETUP_FLAG_PATH: setupFlagPath,
+            TEARDOWN_FLAG_PATH: teardownFlagPath
+          },
+          additionalFlags: [
+            `--import=${esmpFile}`,
+          ],
+          runnerEnabled
         });
-      }
+
+        assert.match(stdout, /pass 2/);
+        assert.match(stdout, /fail 0/);
+        assert.match(stdout, /Imported module executed/);
+        assert.match(stdout, /Global setup executed/);
+        assert.match(stdout, /Global teardown executed/);
+
+        const importedExecutedPosition = stdout.indexOf('Imported module executed');
+        const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
+
+        assert.ok(
+          shouldImportAfterSetup ?
+            importedExecutedPosition > globalSetupExecutedPosition :
+            importedExecutedPosition < globalSetupExecutedPosition,
+          `Imported module should have been executed ${shouldImportAfterSetup ? 'after' : 'before'} global setup`
+        );
+
+        assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
+        const content = fs.readFileSync(teardownFlagPath, 'utf8');
+        assert.strictEqual(content, 'Teardown was executed');
+        assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
+      });
 
       it('should execute globalSetup and globalTeardown correctly with imported module containing tests', async () => {
         const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
diff --git a/test/parallel/test-runner-no-isolation-hooks.mjs b/test/parallel/test-runner-no-isolation-hooks.mjs
index 7896ae2fec819f..d67195bae832c7 100644
--- a/test/parallel/test-runner-no-isolation-hooks.mjs
+++ b/test/parallel/test-runner-no-isolation-hooks.mjs
@@ -68,3 +68,15 @@ test('use --import (ESM) to define global hooks', async (t) => {
 
   t.assert.equal(testHookOutput, order);
 });
+
+test('use --require to define global hooks', async (t) => {
+  const { stdout } = await common.spawnPromisified(process.execPath, [
+    ...testArguments,
+    '--require', fixtures.path('test-runner', 'no-isolation', 'global-hooks.cjs'),
+    ...testFiles,
+  ]);
+
+  const testHookOutput = stdout.split('\n▶')[0];
+
+  t.assert.equal(testHookOutput, order);
+});


Reviews:
user: login: JakobJingleheimer
body: Thanks for picking this up!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T20:28:53Z
user: login: pmarchini
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T10:15:07Z
user: login: JakobJingleheimer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T10:28:26Z
user: login: pmarchini
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T05:35:02Z
user: login: pmarchini
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T08:27:30Z
user: login: cjihrig
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T18:15:45Z
user: login: atlowChemi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T17:39:05Z
user: login: cjihrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:42:00Z
user: login: pmarchini
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T17:43:21Z
user: login: cjihrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:51:35Z
user: login: pmarchini
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T19:27:28Z
user: login: atlowChemi
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-03T19:57:16Z

Review comments:
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationNone = getOptionValue('--test-isolation') === 'none';
+
+// We set initializeModules to false as we want to load user modules in the test runner run function
+// if we are running with --test-isolation=none
+prepareTestRunnerMainExecution(!isTestIsolationNone);
path: lib/internal/main/test_runner.js
user: login: JakobJingleheimer
body: very nit: I think saying "disabled" will be more broadly/immediately understood than "none".

```suggestion
const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';

// We set initializeModules to false as we want to load user modules in the test runner run function
// if we are running with --test-isolation=none
prepareTestRunnerMainExecution(!isTestIsolationDisabled);
```
created_at: 2025-04-22T20:17:37Z
author_association: MEMBER
diff_hunk: @@ -765,9 +765,15 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
-          const userImports = getOptionValue('--import');
-          for (let i = 0; i < userImports.length; i++) {
-            await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // before the creation of the root test.
path: lib/internal/test_runner/runner.js
user: login: JakobJingleheimer
body: ```suggestion
          // BEFORE the creation of the root test (that would cause them to get lost).
```
created_at: 2025-04-22T20:20:05Z
author_association: MEMBER
diff_hunk: @@ -765,9 +765,15 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
-          const userImports = getOptionValue('--import');
-          for (let i = 0; i < userImports.length; i++) {
-            await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // before the creation of the root test.
+          const userModules = [
+            ...getOptionValue('--require'),
+            ...getOptionValue('--import'),
+          ];
path: lib/internal/test_runner/runner.js
user: login: JakobJingleheimer
body: I think the approach here is logical in terms of steps. But 2 caveats:

* `CascadedLoader` (aka `ESMLoader` aka `ModuleLoader`) does not handle CJS modules exactly the same as `CJSLoader`; if we're in ESM mode, this is "correct" (ESMLoader should handle it); but if not ESM mode, for good or ill, it could lead to unexpected behaviour.
* Does this prevent the `--require`d module(s) executing prior to this? If not, they could be stateful or something else crazy which would get lost when the clone gets created here.
created_at: 2025-04-22T20:28:25Z
author_association: MEMBER
diff_hunk: @@ -765,9 +765,15 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
-          const userImports = getOptionValue('--import');
-          for (let i = 0; i < userImports.length; i++) {
-            await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // before the creation of the root test.
+          const userModules = [
+            ...getOptionValue('--require'),
+            ...getOptionValue('--import'),
+          ];
path: lib/internal/test_runner/runner.js
user: login: pmarchini
body: Thanks for your review, @JakobJingleheimer 🚀 

Regarding nr1: ok! I suspected that. I'll take a look to understand if we can just use the default method we're using for `--require` in the "default" entry point.

nr2: Yes, it prevents the default handling of `--require` from happening, it's now being executed only at this point!
created_at: 2025-04-23T10:15:07Z
author_association: MEMBER
diff_hunk: @@ -765,9 +765,15 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
-          const userImports = getOptionValue('--import');
-          for (let i = 0; i < userImports.length; i++) {
-            await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // before the creation of the root test.
+          const userModules = [
+            ...getOptionValue('--require'),
+            ...getOptionValue('--import'),
+          ];
path: lib/internal/test_runner/runner.js
user: login: JakobJingleheimer
body: W00t!
created_at: 2025-04-23T10:28:26Z
author_association: MEMBER
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationNone = getOptionValue('--test-isolation') === 'none';
+
+// We set initializeModules to false as we want to load user modules in the test runner run function
+// if we are running with --test-isolation=none
+prepareTestRunnerMainExecution(!isTestIsolationNone);
path: lib/internal/main/test_runner.js
user: login: pmarchini
body: applied the suggestion!
created_at: 2025-04-24T05:35:02Z
author_association: MEMBER
diff_hunk: @@ -765,9 +765,15 @@ function run(options = kEmptyObject) {
             SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
             promise;
 
-          const userImports = getOptionValue('--import');
-          for (let i = 0; i < userImports.length; i++) {
-            await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
+          // We need to setup the user modules in the test runner if we are running with
+          // --test-isolation=none and --test in order to avoid loading the user modules
+          // before the creation of the root test.
+          const userModules = [
+            ...getOptionValue('--require'),
+            ...getOptionValue('--import'),
+          ];
path: lib/internal/test_runner/runner.js
user: login: pmarchini
body: I've updated the logic to use `loadPreloadModules` and applied the other suggestions.  
Before marking this PR as "ready to be reviewed", I'd like to ask you to take another look at these changes 😁
created_at: 2025-04-24T08:27:29Z
author_association: MEMBER
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';
path: lib/internal/main/test_runner.js
user: login: atlowChemi
body: Can we/should we move `const options = parseCommandLine();` to above `prepareTestRunnerMainExecution` and use `isolation` from there, to avoid using `getOptionValue` here?

@cjihrig cc
created_at: 2025-05-01T17:39:05Z
author_association: MEMBER
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';
path: lib/internal/main/test_runner.js
user: login: cjihrig
body: Yes, we shouldn't use `getOptionValue()` anywhere but in `parseCommandLine()`.
created_at: 2025-05-01T17:42:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';
path: lib/internal/main/test_runner.js
user: login: pmarchini
body: ParseCommandLine, if I'm not mistaken, can't be used before init! That's why I had to make a redundant call.

I'll take another look just to be sure and report back here with the result
created_at: 2025-05-01T17:43:21Z
author_association: MEMBER
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';
path: lib/internal/main/test_runner.js
user: login: cjihrig
body: Ah, this is in `lib/internal/main/test_runner.js`... that should be fine then since that file has nothing to do with the `run()` API.
created_at: 2025-05-01T17:51:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,18 +5,23 @@ const {
 } = primordials;
 
 const {
-  prepareMainThreadExecution,
   markBootstrapComplete,
+  prepareTestRunnerMainExecution,
 } = require('internal/process/pre_execution');
 const { isUsingInspector } = require('internal/util/inspector');
 const { run } = require('internal/test_runner/runner');
 const { parseCommandLine } = require('internal/test_runner/utils');
+const { getOptionValue } = require('internal/options');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
 let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
   debug = fn;
 });
 
-prepareMainThreadExecution(false);
+const isTestIsolationDisabled = getOptionValue('--test-isolation') === 'none';
path: lib/internal/main/test_runner.js
user: login: pmarchini
body: My bad 😬 we can refactor it to avoid using `getOptionValue`.  
I'm running the tests, once I have a local green flag, I'll push a fixup commit!  
Thanks @atlowChemi for catching it!
created_at: 2025-05-01T19:27:28Z
author_association: MEMBER

PR comments:
user: login: nodejs-github-bot
created_at: 2025-04-18T13:45:41Z
body: Review requested:

- [ ] @nodejs/test_runner
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-04-25T17:24:29Z
body: ## [Codecov](https://app.codecov.io/gh/nodejs/node/pull/57924?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 90.19%. Comparing base ([`52d95f5`](https://app.codecov.io/gh/nodejs/node/commit/52d95f53e466016120048fb43b3732ff9089ecd7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs)) to head ([`b866b7e`](https://app.codecov.io/gh/nodejs/node/commit/b866b7e7b38d4aeaab76d4fd8a577149cb23cd8a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs)).
:warning: Report is 1091 commits behind head on main.

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main   #57924      +/-   ##
==========================================
- Coverage   90.26%   90.19%   -0.07%     
==========================================
  Files         630      630              
  Lines      185933   186470     +537     
  Branches    36450    36624     +174     
==========================================
+ Hits       167829   168186     +357     
- Misses      10972    11061      +89     
- Partials     7132     7223      +91     
```

| [Files with missing lines](https://app.codecov.io/gh/nodejs/node/pull/57924?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs) | Coverage Δ | |
|---|---|---|
| [lib/internal/main/test\_runner.js](https://app.codecov.io/gh/nodejs/node/pull/57924?src=pr&el=tree&filepath=lib%2Finternal%2Fmain%2Ftest_runner.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-bGliL2ludGVybmFsL21haW4vdGVzdF9ydW5uZXIuanM=) | `100.00% <100.00%> (ø)` | |
| [lib/internal/process/pre\_execution.js](https://app.codecov.io/gh/nodejs/node/pull/57924?src=pr&el=tree&filepath=lib%2Finternal%2Fprocess%2Fpre_execution.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-bGliL2ludGVybmFsL3Byb2Nlc3MvcHJlX2V4ZWN1dGlvbi5qcw==) | `94.91% <100.00%> (-1.05%)` | :arrow_down: |
| [lib/internal/test\_runner/runner.js](https://app.codecov.io/gh/nodejs/node/pull/57924?src=pr&el=tree&filepath=lib%2Finternal%2Ftest_runner%2Frunner.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs#diff-bGliL2ludGVybmFsL3Rlc3RfcnVubmVyL3J1bm5lci5qcw==) | `89.03% <100.00%> (+0.15%)` | :arrow_up: |

... and [71 files with indirect coverage changes](https://app.codecov.io/gh/nodejs/node/pull/57924/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nodejs)
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: nodejs-github-bot
created_at: 2025-04-30T20:38:48Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66519/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-02T10:48:59Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66540/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-02T17:49:40Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66550/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T07:27:02Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66557/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T10:25:50Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66558/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T11:19:36Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66559/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T14:05:58Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66561/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T15:02:29Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66563/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T19:00:32Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66571/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-03T20:24:38Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66572/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-04T12:48:44Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66580/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-04T18:44:07Z
body: CI: https://ci.nodejs.org/job/node-test-pull-request/66595/
author_association: COLLABORATOR
user: login: nodejs-github-bot
created_at: 2025-05-06T06:28:30Z
body: Landed in 7e24ebc78079bc7551b6dd96e1f0d249f4599605
author_association: COLLABORATOR
user: login: richardlau
created_at: 2025-09-19T16:20:27Z
body: This doesn't land cleanly on v22.x-staging so a manual backport will be necessary if this is to be released there.
author_association: MEMBER