@vitest-agent/plugin 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { Context, Effect, Layer, LogLevel, Option } from "effect";
4
4
  import { TestProjectInlineConfiguration } from "vitest/config";
5
5
  import { ResolvedConfig, VitestPluginContext } from "vitest/node";
6
6
  import { SourceMap } from "magic-string";
7
+ import { WorkspacesSyncOptions } from "@effected/workspaces/node-sync";
7
8
  import * as NodeServices from "@effect/platform-node/NodeServices";
8
9
  import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
9
10
  //#region src/utils/tag.d.ts
@@ -31,6 +32,85 @@ declare class Tag {
31
32
  static make(name: string, options?: TagOptions): Tag;
32
33
  }
33
34
  //#endregion
35
+ //#region src/utils/walker-fs.d.ts
36
+ /**
37
+ * The filesystem operations the discovery walkers need, supplied by the
38
+ * caller. Node's built-ins satisfy it directly via {@link nodeWalkerFs}, which
39
+ * is what every production call site uses.
40
+ *
41
+ * The port exists because the walkers used to call `node:fs/promises` outright,
42
+ * which made them untestable against anything but a real temporary directory —
43
+ * a virtual filesystem cannot intercept a direct `node:fs` call. Every test in
44
+ * this package that exercised discovery therefore had to build, populate and
45
+ * tear down a real tmp tree.
46
+ *
47
+ * Deliberately NOT `fs.promises`-shaped: the walkers need exactly two
48
+ * operations, and a wider surface would invite call sites to reach past the
49
+ * port. It is also NOT shaped like `@effected/workspaces`'s `SyncFileSystem`
50
+ * (which this package consumes separately, in `discover-projects.ts`) — that
51
+ * port answers entry *names*, and the walkers need the entry *type* that
52
+ * `readdir({ withFileTypes: true })` returns in the same syscall. Reading names
53
+ * and then stat-ing each one is the syscall-doubling this module exists to
54
+ * avoid; see {@link WalkerEntry}.
55
+ *
56
+ * @packageDocumentation
57
+ */
58
+ /**
59
+ * One directory entry, carrying the name and type together.
60
+ *
61
+ * A structural subset of Node's `Dirent`, so a real `Dirent` satisfies it
62
+ * verbatim and `readdir({ withFileTypes: true })` needs no adaptation.
63
+ * @public
64
+ */
65
+ interface WalkerEntry {
66
+ /** The entry's own name, not a path. */
67
+ readonly name: string;
68
+ /** Whether this entry is a regular file. */
69
+ readonly isFile: () => boolean;
70
+ /** Whether this entry is a directory. */
71
+ readonly isDirectory: () => boolean;
72
+ }
73
+ /**
74
+ * The filesystem operations the discovery walkers need.
75
+ *
76
+ * Both operations may reject; every walker absorbs a rejection as "this path
77
+ * contributes nothing" rather than propagating it, matching the behavior the
78
+ * `node:fs` calls had when they were inline.
79
+ * @public
80
+ */
81
+ interface WalkerFileSystem {
82
+ /**
83
+ * The entries inside the directory at `dir`, with their types. May reject —
84
+ * a rejection reads as an unreadable directory and skips it.
85
+ */
86
+ readonly readDirectory: (dir: string) => Promise<ReadonlyArray<WalkerEntry>>;
87
+ /**
88
+ * The type of the entry at `path`, or `null` when it does not exist or
89
+ * cannot be read. Never rejects.
90
+ */
91
+ readonly statEntry: (path: string) => Promise<WalkerEntryStat | null>;
92
+ }
93
+ /**
94
+ * What the walkers read off a `stat`: the entry's type, plus the modification
95
+ * time the discovery cache's directory signature is built from.
96
+ * @public
97
+ */
98
+ interface WalkerEntryStat {
99
+ /** Whether the path holds a regular file. */
100
+ readonly isFile: boolean;
101
+ /** Whether the path holds a directory. */
102
+ readonly isDirectory: boolean;
103
+ /** Modification time in milliseconds since the epoch. */
104
+ readonly mtimeMs: number;
105
+ }
106
+ /**
107
+ * {@link WalkerFileSystem} over `node:fs/promises` — the binding every
108
+ * production call site uses, and the default when a walker is called without
109
+ * an explicit port.
110
+ * @public
111
+ */
112
+ declare const nodeWalkerFs: WalkerFileSystem;
113
+ //#endregion
34
114
  //#region src/utils/discover-strategy.d.ts
35
115
  /**
36
116
  * Resolved metadata about a discovered test module.
@@ -73,6 +153,12 @@ interface DiscoverInput {
73
153
  readonly workspaceRoot: string;
74
154
  /** Parsed `package.json` contents, when available. */
75
155
  readonly packageJson?: PackageJson;
156
+ /**
157
+ * Filesystem port the walkers read through. Defaults to `node:fs` when
158
+ * omitted, which is what every production call site does; tests inject a
159
+ * virtual volume instead of building a real temporary directory.
160
+ */
161
+ readonly fs?: WalkerFileSystem;
76
162
  }
77
163
  /**
78
164
  * Context object passed to a `ClassifyFn`.
@@ -938,6 +1024,16 @@ interface DiscoverProjectsOptions {
938
1024
  readonly name: string;
939
1025
  readonly path: string;
940
1026
  }>;
1027
+ /**
1028
+ * Filesystem port the test-file and signature walks read through. Defaults
1029
+ * to `node:fs`; tests inject a virtual volume instead of a real tmp tree.
1030
+ */
1031
+ readonly fs?: WalkerFileSystem;
1032
+ /**
1033
+ * Sync operations `@effected/workspaces` resolves the workspace root and
1034
+ * package list through. Defaults to its `nodeSyncOps` binding.
1035
+ */
1036
+ readonly syncOps?: WorkspacesSyncOptions;
941
1037
  }
942
1038
  /**
943
1039
  * Scan all workspace packages and additional entries through the active strategy and return projects + tags.
@@ -1006,7 +1102,7 @@ declare function combineClassifiers(...fns: ReadonlyArray<ClassifyFn>): Classify
1006
1102
  * @returns Absolute paths of matched test files
1007
1103
  * @public
1008
1104
  */
1009
- declare function findTestFiles(dir: string, patterns: ReadonlyArray<string>): Promise<ReadonlyArray<string>>;
1105
+ declare function findTestFiles(dir: string, patterns: ReadonlyArray<string>, fs?: WalkerFileSystem): Promise<ReadonlyArray<string>>;
1010
1106
  //#endregion
1011
1107
  //#region src/layers/ReporterLive.d.ts
1012
1108
  /**
@@ -1247,5 +1343,5 @@ declare const COVERAGE_AUTOUPDATE: Readonly<{
1247
1343
  lenient: (n: number) => number;
1248
1344
  }>;
1249
1345
  //#endregion
1250
- export { type AddProjectInput, AgentPlugin, type AgentPluginConstructorOptions, AgentReporter, type AgentReporterConstructorOptions, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, type ClassifyContext, type ClassifyFn, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, DefaultDiscoverStrategy, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, DiscoverStrategy, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, ReporterLive, Tag, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
1346
+ export { type AddProjectInput, AgentPlugin, type AgentPluginConstructorOptions, AgentReporter, type AgentReporterConstructorOptions, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, type ClassifyContext, type ClassifyFn, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, DefaultDiscoverStrategy, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, DiscoverStrategy, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, ReporterLive, Tag, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, type WalkerEntry, type WalkerEntryStat, type WalkerFileSystem, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
1251
1347
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -8,6 +8,7 @@ import { captureEnvVars } from "./utils/capture-env.js";
8
8
  import { captureSettings, hashSettings } from "./utils/capture-settings.js";
9
9
  import { processFailure } from "./utils/process-failure.js";
10
10
  import { AgentReporter } from "./reporter.js";
11
+ import { nodeWalkerFs } from "./utils/walker-fs.js";
11
12
  import { findTestFiles } from "./utils/find-test-files.js";
12
13
  import { Tag } from "./utils/tag.js";
13
14
  import { DefaultDiscoverStrategy, DiscoverStrategy } from "./utils/discover-strategy.js";
@@ -28,4 +29,4 @@ const COVERAGE_LEVELS_PER_FILE = AgentPlugin.COVERAGE_LEVELS_PER_FILE;
28
29
  const COVERAGE_AUTOUPDATE = AgentPlugin.COVERAGE_AUTOUPDATE;
29
30
 
30
31
  //#endregion
31
- export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
32
+ export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/plugin",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "description": "Vitest plugin for the vitest-agent ecosystem: owns persistence, classification, baselines, trends, and dispatches rendering to a configurable reporter.",
6
6
  "keywords": [
@@ -41,13 +41,13 @@
41
41
  "dependencies": {
42
42
  "@effect/platform-node": "4.0.0-rc.109",
43
43
  "@effect/sql-sqlite-node": "4.0.0-rc.109",
44
- "@effected/workspaces": "^0.14.2",
45
- "@vitest-agent/cli": "2.2.1",
46
- "@vitest-agent/mcp": "2.3.1",
47
- "@vitest-agent/reporter": "2.1.1",
48
- "@vitest-agent/sdk": "2.4.1",
44
+ "@effected/workspaces": "^0.16.0",
45
+ "@vitest-agent/cli": "2.2.3",
46
+ "@vitest-agent/mcp": "2.3.3",
47
+ "@vitest-agent/reporter": "2.1.3",
48
+ "@vitest-agent/sdk": "2.4.3",
49
49
  "effect": "4.0.0-rc.109",
50
- "magic-string": "^1.2.0"
50
+ "magic-string": "^1.2.1"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@vitest/coverage-istanbul": "^4.1.0",
package/plugin.js CHANGED
@@ -100,7 +100,7 @@ const aggregatedReporterByVitest = /* @__PURE__ */ new WeakSet();
100
100
  *
101
101
  * @public
102
102
  */
103
- const CURRENT_PLUGIN_VERSION = "2.3.1";
103
+ const CURRENT_PLUGIN_VERSION = "2.4.0";
104
104
  const TEST_FILE_DIR_RE = new RegExp(`/(?:${SRC_DIR}|${TEST_DIR})/`);
105
105
  const isTestFile = (id) => isTestFileName(id) && TEST_FILE_DIR_RE.test(id);
106
106
  /**
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.12"
8
+ "packageVersion": "7.58.13"
9
9
  }
10
10
  ]
11
11
  }
@@ -1,22 +1,22 @@
1
1
  import { toPosixPath } from "./to-posix-path.js";
2
+ import { nodeWalkerFs } from "./walker-fs.js";
2
3
  import { DefaultDiscoverStrategy } from "./discover-strategy.js";
3
4
  import { isTestShapedPackage } from "./is-test-shaped-package.js";
4
5
  import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR } from "@vitest-agent/sdk";
5
6
  import { isAbsolute, join, normalize, relative } from "node:path";
6
- import { readdir, stat } from "node:fs/promises";
7
7
  import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
8
8
  import { nodeSyncOps } from "@effected/workspaces/node-sync";
9
9
 
10
10
  //#region src/utils/discover-projects.ts
11
11
  const DISCOVERY_LAST_SCAN_SYMBOL = Symbol.for("vitest-agent:discovery:last-scan-at");
12
12
  const warnedDeclinedPackagePaths = /* @__PURE__ */ new Set();
13
- async function warnIfDeclinedPackageIsTestShaped(pkg) {
13
+ async function warnIfDeclinedPackageIsTestShaped(pkg, fs) {
14
14
  const normPath = normalize(pkg.path);
15
15
  if (warnedDeclinedPackagePaths.has(normPath)) return;
16
16
  warnedDeclinedPackagePaths.add(normPath);
17
17
  let testShaped;
18
18
  try {
19
- testShaped = await isTestShapedPackage(pkg.path);
19
+ testShaped = await isTestShapedPackage(pkg.path, fs);
20
20
  } catch (error) {
21
21
  warnedDeclinedPackagePaths.delete(normPath);
22
22
  throw error;
@@ -44,16 +44,16 @@ const _cache = /* @__PURE__ */ new Map();
44
44
  * subprocess-e2e fixture that installs deps), and Node's recursive `readdir`
45
45
  * follows symlinked directories, so an unguarded call would walk into it.
46
46
  */
47
- async function computeDirSignature(dirPath) {
47
+ async function computeDirSignature(dirPath, fs) {
48
48
  const parts = [];
49
- await walkDirSignature(dirPath, dirPath, parts);
49
+ await walkDirSignature(dirPath, dirPath, parts, fs);
50
50
  parts.sort();
51
51
  return parts.join("|");
52
52
  }
53
- async function walkDirSignature(root, dir, parts) {
53
+ async function walkDirSignature(root, dir, parts, fs) {
54
54
  let entries;
55
55
  try {
56
- entries = await readdir(dir, { withFileTypes: true });
56
+ entries = await fs.readDirectory(dir);
57
57
  } catch {
58
58
  return;
59
59
  }
@@ -61,14 +61,11 @@ async function walkDirSignature(root, dir, parts) {
61
61
  const fullPath = join(dir, ent.name);
62
62
  if (ent.isDirectory()) {
63
63
  if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
64
- await walkDirSignature(root, fullPath, parts);
64
+ await walkDirSignature(root, fullPath, parts, fs);
65
65
  } else if (ent.isFile()) {
66
- let mtimeMs;
67
- try {
68
- mtimeMs = (await stat(fullPath)).mtimeMs;
69
- } catch {
70
- continue;
71
- }
66
+ const info = await fs.statEntry(fullPath);
67
+ if (info === null) continue;
68
+ const mtimeMs = info.mtimeMs;
72
69
  const relPath = toPosixPath(relative(root, fullPath));
73
70
  parts.push(`${relPath}:${mtimeMs}`);
74
71
  }
@@ -85,11 +82,11 @@ async function walkDirSignature(root, dir, parts) {
85
82
  * test file anywhere else, so nothing else can change the emitted project list
86
83
  * (issue #227).
87
84
  */
88
- async function computeWorkspaceSignature(packages) {
85
+ async function computeWorkspaceSignature(packages, fs) {
89
86
  const parts = [];
90
87
  for (const pkg of packages) {
91
- const srcSig = await computeDirSignature(join(pkg.path, SRC_DIR));
92
- const testSig = await computeDirSignature(join(pkg.path, TEST_DIR));
88
+ const srcSig = await computeDirSignature(join(pkg.path, SRC_DIR), fs);
89
+ const testSig = await computeDirSignature(join(pkg.path, TEST_DIR), fs);
93
90
  parts.push(`${pkg.path}::src=${srcSig}::__test__=${testSig}`);
94
91
  }
95
92
  return parts.join("\n");
@@ -104,14 +101,16 @@ async function discoverProjects(options) {
104
101
  const strategy = options?.strategy;
105
102
  const cwd = options?.cwd;
106
103
  const additionalEntries = options?.additionalEntries ?? [];
107
- const root = findWorkspaceRootSync(cwd ?? process.cwd(), nodeSyncOps);
104
+ const fs = options?.fs ?? nodeWalkerFs;
105
+ const syncOps = options?.syncOps ?? nodeSyncOps;
106
+ const root = findWorkspaceRootSync(cwd ?? process.cwd(), syncOps);
108
107
  if (!root) throw new Error(`[vitest-agent] Could not find workspace root from ${cwd ?? process.cwd()}. Ensure a pnpm-workspace.yaml or package.json with "workspaces" exists.`);
109
108
  const useCache = strategy === void 0 && additionalEntries.length === 0;
110
109
  const resolvedStrategy = strategy ?? new DefaultDiscoverStrategy();
111
- const packages = getWorkspacePackagesSync(root, nodeSyncOps);
110
+ const packages = getWorkspacePackagesSync(root, syncOps);
112
111
  let signature;
113
112
  if (useCache) {
114
- signature = await computeWorkspaceSignature(packages);
113
+ signature = await computeWorkspaceSignature(packages, fs);
115
114
  const cached = _cache.get(root);
116
115
  if (cached && cached.signature === signature) return cached.result;
117
116
  }
@@ -123,10 +122,11 @@ async function discoverProjects(options) {
123
122
  name: pkg.name,
124
123
  path: pkg.path,
125
124
  relativePath: toPosixPath(pkg.relativePath),
126
- workspaceRoot: root
125
+ workspaceRoot: root,
126
+ fs
127
127
  });
128
128
  if (config !== null) configs.push(config);
129
- else await warnIfDeclinedPackageIsTestShaped(pkg);
129
+ else await warnIfDeclinedPackageIsTestShaped(pkg, fs);
130
130
  workspaceNames.add(pkg.name);
131
131
  workspacePaths.add(normalize(pkg.path));
132
132
  }
@@ -140,7 +140,8 @@ async function discoverProjects(options) {
140
140
  name: entry.name,
141
141
  path: normPath,
142
142
  relativePath,
143
- workspaceRoot: root
143
+ workspaceRoot: root,
144
+ fs
144
145
  });
145
146
  if (config === null) {
146
147
  const strategyName = resolvedStrategy.constructor.name;
@@ -1,8 +1,8 @@
1
+ import { nodeWalkerFs } from "./walker-fs.js";
1
2
  import { findTestFiles } from "./find-test-files.js";
2
3
  import { Tag } from "./tag.js";
3
4
  import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS } from "@vitest-agent/sdk";
4
5
  import { join, sep } from "node:path";
5
- import { stat } from "node:fs/promises";
6
6
  import { configDefaults } from "vitest/config";
7
7
 
8
8
  //#region src/utils/discover-strategy.ts
@@ -12,15 +12,11 @@ const SETUP_EXTS = [
12
12
  "js",
13
13
  "jsx"
14
14
  ];
15
- async function isFile(p) {
16
- try {
17
- return (await stat(p)).isFile();
18
- } catch {
19
- return false;
15
+ async function detectSetupFile(pkgPath, fs) {
16
+ for (const ext of SETUP_EXTS) {
17
+ const candidate = join(pkgPath, `vitest.setup.${ext}`);
18
+ if ((await fs.statEntry(candidate))?.isFile === true) return `vitest.setup.${ext}`;
20
19
  }
21
- }
22
- async function detectSetupFile(pkgPath) {
23
- for (const ext of SETUP_EXTS) if (await isFile(join(pkgPath, `vitest.setup.${ext}`))) return `vitest.setup.${ext}`;
24
20
  return null;
25
21
  }
26
22
  /**
@@ -112,8 +108,9 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
112
108
  return ["unit"];
113
109
  }
114
110
  async buildProject(input) {
111
+ const fs = input.fs ?? nodeWalkerFs;
115
112
  const srcPrefix = join(input.path, SRC_DIR);
116
- const allFiles = await findTestFiles(input.path, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`, `${TEST_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`]);
113
+ const allFiles = await findTestFiles(input.path, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`, `${TEST_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`], fs);
117
114
  if (allFiles.length === 0) return null;
118
115
  const hasSrcTests = allFiles.some((f) => f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix);
119
116
  const hasTestDirTests = allFiles.some((f) => !(f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix));
@@ -125,7 +122,7 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
125
122
  ...[SRC_DIR, TEST_DIR].flatMap((root) => [...NON_DISCOVERABLE_DIRS].map((d) => join(input.path, root, "**", d, "**"))),
126
123
  ...hasTestDirTests ? TEST_HELPER_DIRS.map((d) => join(input.path, TEST_DIR, "**", d, "**")) : []
127
124
  ];
128
- const setupFile = await detectSetupFile(input.path);
125
+ const setupFile = await detectSetupFile(input.path, fs);
129
126
  return {
130
127
  extends: true,
131
128
  test: {
@@ -1,7 +1,7 @@
1
1
  import { toPosixPath } from "./to-posix-path.js";
2
+ import { nodeWalkerFs } from "./walker-fs.js";
2
3
  import { NON_DISCOVERABLE_DIRS } from "@vitest-agent/sdk";
3
4
  import { join, relative } from "node:path";
4
- import { readdir } from "node:fs/promises";
5
5
 
6
6
  //#region src/utils/find-test-files.ts
7
7
  function globToRegex(pattern) {
@@ -70,17 +70,17 @@ function toRegexFragment(glob) {
70
70
  * @returns Absolute paths of matched test files
71
71
  * @public
72
72
  */
73
- async function findTestFiles(dir, patterns) {
73
+ async function findTestFiles(dir, patterns, fs = nodeWalkerFs) {
74
74
  if (patterns.length === 0) return [];
75
75
  const matchers = patterns.map(globToRegex);
76
76
  const results = [];
77
- await walkDir(dir, dir, matchers, results);
77
+ await walkDir(dir, dir, matchers, results, fs);
78
78
  return results;
79
79
  }
80
- async function walkDir(root, dir, matchers, results) {
80
+ async function walkDir(root, dir, matchers, results, fs) {
81
81
  let entries;
82
82
  try {
83
- entries = await readdir(dir, { withFileTypes: true });
83
+ entries = await fs.readDirectory(dir);
84
84
  } catch {
85
85
  return;
86
86
  }
@@ -88,7 +88,7 @@ async function walkDir(root, dir, matchers, results) {
88
88
  for (const ent of entries) {
89
89
  if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
90
90
  const fullPath = join(dir, ent.name);
91
- if (ent.isDirectory()) await walkDir(root, fullPath, matchers, results);
91
+ if (ent.isDirectory()) await walkDir(root, fullPath, matchers, results, fs);
92
92
  else if (ent.isFile()) {
93
93
  const rel = toPosixPath(relative(root, fullPath));
94
94
  if (matchers.some((re) => re.test(rel))) results.push(fullPath);
@@ -1,16 +1,9 @@
1
+ import { nodeWalkerFs } from "./walker-fs.js";
1
2
  import { findTestFiles } from "./find-test-files.js";
2
3
  import { SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX } from "@vitest-agent/sdk";
3
4
  import { join } from "node:path";
4
- import { stat } from "node:fs/promises";
5
5
 
6
6
  //#region src/utils/is-test-shaped-package.ts
7
- async function isDirectory(p) {
8
- try {
9
- return (await stat(p)).isDirectory();
10
- } catch {
11
- return false;
12
- }
13
- }
14
7
  /**
15
8
  * True when `pkgPath` looks like it was meant to hold tests: a `__test__/`
16
9
  * directory exists at the package root (regardless of what's inside it), or
@@ -31,9 +24,9 @@ async function isDirectory(p) {
31
24
  * it needs to catch.
32
25
  * @internal
33
26
  */
34
- async function isTestShapedPackage(pkgPath) {
35
- if (await isDirectory(join(pkgPath, TEST_DIR))) return true;
36
- return (await findTestFiles(pkgPath, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`])).length > 0;
27
+ async function isTestShapedPackage(pkgPath, fs = nodeWalkerFs) {
28
+ if ((await fs.statEntry(join(pkgPath, TEST_DIR)))?.isDirectory === true) return true;
29
+ return (await findTestFiles(pkgPath, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`], fs)).length > 0;
37
30
  }
38
31
 
39
32
  //#endregion
@@ -0,0 +1,27 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+
3
+ //#region src/utils/walker-fs.ts
4
+ /**
5
+ * {@link WalkerFileSystem} over `node:fs/promises` — the binding every
6
+ * production call site uses, and the default when a walker is called without
7
+ * an explicit port.
8
+ * @public
9
+ */
10
+ const nodeWalkerFs = {
11
+ readDirectory: async (dir) => await readdir(dir, { withFileTypes: true }),
12
+ statEntry: async (path) => {
13
+ try {
14
+ const info = await stat(path);
15
+ return {
16
+ isFile: info.isFile(),
17
+ isDirectory: info.isDirectory(),
18
+ mtimeMs: info.mtimeMs
19
+ };
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ };
25
+
26
+ //#endregion
27
+ export { nodeWalkerFs };