From 26087fb02dedd4e2e7dc423b57f9e09e9d0a1580 Mon Sep 17 00:00:00 2001 From: Aalhad Date: Wed, 29 Jul 2026 20:49:13 +0530 Subject: [PATCH] test_runner: expand directory arguments to find test files When a directory is passed as an argument to node --test, the test runner treats it as a file pattern. Node.js internal Glob matches the directory itself, causing MODULE_NOT_FOUND when the runner tries to import the directory as a module. This restores the pre-v21 behavior by detecting directory arguments and expanding them to search for default test file patterns within them. Fixes: https://github.com/nodejs/node/issues/64555 --- lib/internal/test_runner/runner.js | 19 +++++++++++++++++++ test/parallel/test-runner-cli.js | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 3bfe719ce5be..d7867774ddb2 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -18,6 +18,7 @@ const { ObjectAssign, PromisePrototypeThen, PromiseWithResolvers, + RegExpPrototypeSymbolReplace, SafeMap, SafePromiseAll, SafePromiseAllReturnVoid, @@ -34,6 +35,7 @@ const { } = primordials; const { spawn } = require('child_process'); +const { statSync } = require('fs'); const { finished } = require('internal/streams/end-of-stream'); const { availableParallelism } = require('os'); const { resolve, sep, isAbsolute } = require('path'); @@ -153,6 +155,23 @@ function createTestFileList(patterns, cwd) { const hasUserSuppliedPattern = patterns != null; if (!patterns || patterns.length === 0) { patterns = [kDefaultPattern]; + } else { + patterns = ArrayPrototypeMap(patterns, (pattern) => { + // A directory argument is expanded to search for the default test files + // within it, matching the behavior from before glob patterns were + // supported. Glob patterns always use forward slashes, so a trailing + // path separator (of either kind) is stripped before appending. + const resolved = isAbsolute(pattern) ? pattern : resolve(cwd, pattern); + try { + if (statSync(resolved).isDirectory()) { + return `${RegExpPrototypeSymbolReplace(/[\/]+$/, pattern, '')}/${kDefaultPattern}`; + } + } catch { + // Not a directory or not accessible; leave the pattern as-is so the + // existing "Could not find" handling still applies. + } + return pattern; + }); } const glob = new Glob(patterns, { __proto__: null, diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index f563d67d1a9a..0702c5c1500c 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -62,6 +62,27 @@ for (const isolation of ['none', 'process']) { assert.doesNotMatch(stdout, /ok 4 - this should pass/); } + { + // A directory argument is searched for the default test files within it, + // both bare and with a trailing separator. + // Refs: https://github.com/nodejs/node/issues/64555 + for (const dir of ['matching-patterns', 'matching-patterns/']) { + const args = ['--test', '--test-reporter=tap', + '--no-experimental-strip-types', + `--test-isolation=${isolation}`, dir]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + + assert.match(stdout, /ok 1 - this should pass/); + assert.match(stdout, /ok 2 - this should pass/); + assert.match(stdout, /ok 3 - this should pass/); + } + } + { // Should match files with "-test.(c|m)(t|j)s" suffix when typescript support is enabled const args = ['--test', '--test-reporter=tap', '--no-warnings',