@vitest-agent/plugin 2.0.16 → 2.2.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 +10 -2
- package/package.json +5 -5
- package/plugin.js +4 -5
- package/reporter.js +112 -26
- package/utils/discover-projects.js +40 -26
- package/utils/discover-strategy.js +12 -21
- package/utils/find-test-files.js +13 -8
- package/utils/stringify-failure-value.js +5 -1
package/index.d.ts
CHANGED
|
@@ -976,8 +976,16 @@ declare function combineClassifiers(...fns: ReadonlyArray<ClassifyFn>): Classify
|
|
|
976
976
|
* Async file walker that returns matched absolute paths.
|
|
977
977
|
*
|
|
978
978
|
* Walks `dir` recursively via `node:fs/promises`. Skips `node_modules`, `.git`,
|
|
979
|
-
* and `dist` directories
|
|
980
|
-
*
|
|
979
|
+
* and `dist` directories, and does not descend past a nested `package.json`
|
|
980
|
+
* boundary (any directory other than `dir` itself that has its own
|
|
981
|
+
* `package.json` is treated as an independent unit — its files belong to a
|
|
982
|
+
* separate discovery pass, not this one). The package-boundary rule applies
|
|
983
|
+
* to every supplied pattern, not just an unanchored `**\/` one — an anchored
|
|
984
|
+
* pattern like `"src/**\/*.test.ts"` will not match a test file under a
|
|
985
|
+
* nested package.json even though the pattern itself never reaches past
|
|
986
|
+
* `src/`, because the boundary check runs once per directory, independent of
|
|
987
|
+
* which pattern is being matched. Matches files against the supplied glob
|
|
988
|
+
* patterns relative to `dir` (e.g. `"src/**\/*.test.ts"`).
|
|
981
989
|
*
|
|
982
990
|
* Returns an empty array if `dir` does not exist or no files match.
|
|
983
991
|
* @param dir - Absolute path to the directory to walk
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/plugin",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.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": [
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@effect/platform-node": "4.0.0-beta.107",
|
|
43
43
|
"@effect/sql-sqlite-node": "4.0.0-beta.107",
|
|
44
44
|
"@effected/workspaces": "^0.11.1",
|
|
45
|
-
"@vitest-agent/cli": "2.0
|
|
46
|
-
"@vitest-agent/mcp": "2.1.
|
|
47
|
-
"@vitest-agent/reporter": "2.0.
|
|
48
|
-
"@vitest-agent/sdk": "2.0
|
|
45
|
+
"@vitest-agent/cli": "2.1.0",
|
|
46
|
+
"@vitest-agent/mcp": "2.1.3",
|
|
47
|
+
"@vitest-agent/reporter": "2.0.18",
|
|
48
|
+
"@vitest-agent/sdk": "2.2.0",
|
|
49
49
|
"effect": "4.0.0-beta.107",
|
|
50
50
|
"magic-string": "^1.1.0"
|
|
51
51
|
},
|
package/plugin.js
CHANGED
|
@@ -9,7 +9,7 @@ import { injectTags } from "./utils/inject-tags.js";
|
|
|
9
9
|
import { isBenignViteSourceMapWarning } from "./utils/is-benign-vite-source-map-warning.js";
|
|
10
10
|
import { stripConsoleReporters } from "./utils/strip-console-reporters.js";
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
|
-
import { AgentConsoleMode, CiConsoleMode, CoverageLevel, EnvironmentDetector, EnvironmentDetectorLive, HumanConsoleMode, formatFatalError, resolveLogLevel } from "@vitest-agent/sdk";
|
|
12
|
+
import { AgentConsoleMode, CiConsoleMode, CoverageLevel, EnvironmentDetector, EnvironmentDetectorLive, HumanConsoleMode, SRC_DIR, TEST_DIR, formatFatalError, isTestFileName, resolveLogLevel } from "@vitest-agent/sdk";
|
|
13
13
|
import { Effect, Schema } from "effect";
|
|
14
14
|
|
|
15
15
|
//#region src/plugin.ts
|
|
@@ -98,10 +98,9 @@ const aggregatedReporterByVitest = /* @__PURE__ */ new WeakSet();
|
|
|
98
98
|
*
|
|
99
99
|
* @public
|
|
100
100
|
*/
|
|
101
|
-
const CURRENT_PLUGIN_VERSION = "2.0
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const isTestFile = (id) => TEST_FILE_SUFFIX_RE.test(id) && TEST_FILE_DIR_RE.test(id);
|
|
101
|
+
const CURRENT_PLUGIN_VERSION = "2.2.0";
|
|
102
|
+
const TEST_FILE_DIR_RE = new RegExp(`/(?:${SRC_DIR}|${TEST_DIR})/`);
|
|
103
|
+
const isTestFile = (id) => isTestFileName(id) && TEST_FILE_DIR_RE.test(id);
|
|
105
104
|
/**
|
|
106
105
|
* Map a detected {@link Environment} to its {@link Executor}. Inline copy
|
|
107
106
|
* of the `ExecutorResolverLive` mapping so the plugin can compute it
|
package/reporter.js
CHANGED
|
@@ -7,7 +7,7 @@ import { captureSettings, hashSettings } from "./utils/capture-settings.js";
|
|
|
7
7
|
import { processFailure } from "./utils/process-failure.js";
|
|
8
8
|
import { routeRenderedOutput } from "./utils/route-rendered-output.js";
|
|
9
9
|
import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
|
|
10
|
-
import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, FormatSelector, HistoryTracker, OutputPipelineLive, PathResolutionLive, buildAgentReport, computeTrend, ensureMigrated, formatFatalError, historyKey, isTimeoutError, probeHostMetadataFromEnv, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
|
|
10
|
+
import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, FormatSelector, HistoryTracker, OutputPipelineLive, PathResolutionLive, buildAgentReport, coerceErrorField, computeTrend, ensureMigrated, formatFatalError, historyKey, isTimeoutError, probeHostMetadataFromEnv, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
|
|
11
11
|
import { Effect, Option, PubSub } from "effect";
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { mkdirSync } from "node:fs";
|
|
@@ -36,6 +36,21 @@ import { DefaultVitestAgentReporter } from "@vitest-agent/reporter";
|
|
|
36
36
|
* @internal
|
|
37
37
|
*/
|
|
38
38
|
const dbPathCache = /* @__PURE__ */ new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Safely read a raw Vitest error object's `stacks` array. The property
|
|
41
|
+
* access itself can throw on hostile shapes (live getters — the same
|
|
42
|
+
* ConfigError class of failure `coerceErrorField` guards); frames are
|
|
43
|
+
* dropped rather than crashing the persistence loop.
|
|
44
|
+
*/
|
|
45
|
+
function readErrorStacks(source) {
|
|
46
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
47
|
+
try {
|
|
48
|
+
const value = source.stacks;
|
|
49
|
+
return Array.isArray(value) ? value : void 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
function resolveDataPathCached(projectDir) {
|
|
40
55
|
const cached = dbPathCache.get(projectDir);
|
|
41
56
|
if (cached !== void 0) return cached;
|
|
@@ -263,7 +278,9 @@ var AgentReporter = class {
|
|
|
263
278
|
*/
|
|
264
279
|
async onInit(vitest) {
|
|
265
280
|
this._vitest = vitest;
|
|
266
|
-
|
|
281
|
+
try {
|
|
282
|
+
await this.ensureDbPath();
|
|
283
|
+
} catch {}
|
|
267
284
|
await this.initReporters();
|
|
268
285
|
}
|
|
269
286
|
/**
|
|
@@ -801,7 +818,9 @@ var AgentReporter = class {
|
|
|
801
818
|
failCount: fail,
|
|
802
819
|
skipCount: skip,
|
|
803
820
|
timeoutCount: timeout,
|
|
804
|
-
durationMs: totalDuration
|
|
821
|
+
durationMs: totalDuration,
|
|
822
|
+
// @vitest-agent/ui, which both populate `collectedModules` on their
|
|
823
|
+
collectedModules: testModules.length
|
|
805
824
|
});
|
|
806
825
|
}
|
|
807
826
|
const modules = testModules;
|
|
@@ -818,11 +837,11 @@ var AgentReporter = class {
|
|
|
818
837
|
};
|
|
819
838
|
const wantsRunEvents = this.wantsRunEvents();
|
|
820
839
|
let dbPath;
|
|
840
|
+
let persistDisabled;
|
|
821
841
|
try {
|
|
822
842
|
dbPath = await this.ensureDbPath();
|
|
823
843
|
} catch (err) {
|
|
824
|
-
|
|
825
|
-
return;
|
|
844
|
+
persistDisabled = formatFatalError(err);
|
|
826
845
|
}
|
|
827
846
|
const filteredModules = opts.projectFilter ? modules.filter((m) => (m.project.name || "default") === opts.projectFilter) : modules;
|
|
828
847
|
if (filteredModules.length === 0 && opts.projectFilter) return;
|
|
@@ -886,14 +905,33 @@ var AgentReporter = class {
|
|
|
886
905
|
});
|
|
887
906
|
return;
|
|
888
907
|
}
|
|
889
|
-
|
|
908
|
+
if (dbPath !== void 0 && persistDisabled === void 0) try {
|
|
909
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
910
|
+
} catch (err) {
|
|
911
|
+
persistDisabled = formatFatalError(err);
|
|
912
|
+
}
|
|
913
|
+
let fallbackReports;
|
|
890
914
|
try {
|
|
891
|
-
|
|
915
|
+
const fallbackProjectGroups = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const mod of filteredModules) {
|
|
917
|
+
const key = mod.project.name || "default";
|
|
918
|
+
const existing = fallbackProjectGroups.get(key);
|
|
919
|
+
if (existing) existing.push(mod);
|
|
920
|
+
else fallbackProjectGroups.set(key, [mod]);
|
|
921
|
+
}
|
|
922
|
+
const isMultiProjectFallback = fallbackProjectGroups.size > 1 || !!opts.projectFilter;
|
|
923
|
+
fallbackReports = [];
|
|
924
|
+
for (const [projectName, projectModules] of fallbackProjectGroups) fallbackReports.push(buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProjectFallback ? projectName : void 0));
|
|
892
925
|
} catch (err) {
|
|
893
926
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
894
927
|
return;
|
|
895
928
|
}
|
|
896
|
-
|
|
929
|
+
if (dbPath !== void 0 && persistDisabled === void 0) try {
|
|
930
|
+
await ensureMigrated(dbPath, logLevel, logFile);
|
|
931
|
+
} catch (err) {
|
|
932
|
+
persistDisabled = formatFatalError(err);
|
|
933
|
+
}
|
|
934
|
+
const persistProgram = Effect.gen(function* () {
|
|
897
935
|
const store = yield* DataStore;
|
|
898
936
|
const reader = yield* DataReader;
|
|
899
937
|
const analyzer = yield* CoverageAnalyzer;
|
|
@@ -977,6 +1015,7 @@ var AgentReporter = class {
|
|
|
977
1015
|
conversationId: null
|
|
978
1016
|
};
|
|
979
1017
|
const hostProbe = probeHostMetadataFromEnv(process.env);
|
|
1018
|
+
const projectReason = reason === "interrupted" ? "interrupted" : baseReport.summary.failed > 0 || baseReport.failedFiles.length > 0 ? "failed" : "passed";
|
|
980
1019
|
const runId = yield* store.writeRun({
|
|
981
1020
|
invocationId,
|
|
982
1021
|
project,
|
|
@@ -984,7 +1023,7 @@ var AgentReporter = class {
|
|
|
984
1023
|
timestamp: baseReport.timestamp,
|
|
985
1024
|
commitSha: process.env.GITHUB_SHA ?? null,
|
|
986
1025
|
branch: process.env.GITHUB_REF_NAME ?? null,
|
|
987
|
-
reason,
|
|
1026
|
+
reason: projectReason,
|
|
988
1027
|
duration: totalDuration,
|
|
989
1028
|
total: baseReport.summary.total,
|
|
990
1029
|
passed: baseReport.summary.passed,
|
|
@@ -1063,7 +1102,17 @@ var AgentReporter = class {
|
|
|
1063
1102
|
const inputs = [];
|
|
1064
1103
|
for (let ordinal = 0; ordinal < result.errors.length; ordinal++) {
|
|
1065
1104
|
const e = result.errors[ordinal];
|
|
1066
|
-
const
|
|
1105
|
+
const messageText = coerceErrorField(e, "message") ?? "<missing message>";
|
|
1106
|
+
const nameText = coerceErrorField(e, "name");
|
|
1107
|
+
const diffText = coerceErrorField(e, "diff");
|
|
1108
|
+
const stackText = coerceErrorField(e, "stack");
|
|
1109
|
+
const stacksValue = readErrorStacks(e);
|
|
1110
|
+
const { frames, signatureHash } = processFailure({
|
|
1111
|
+
message: messageText,
|
|
1112
|
+
...nameText !== void 0 && { name: nameText },
|
|
1113
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1114
|
+
...stacksValue !== void 0 && { stacks: stacksValue }
|
|
1115
|
+
});
|
|
1067
1116
|
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1068
1117
|
signatureHash,
|
|
1069
1118
|
runId,
|
|
@@ -1072,10 +1121,10 @@ var AgentReporter = class {
|
|
|
1072
1121
|
inputs.push({
|
|
1073
1122
|
testCaseId,
|
|
1074
1123
|
scope: "test",
|
|
1075
|
-
message:
|
|
1076
|
-
...
|
|
1077
|
-
...
|
|
1078
|
-
...
|
|
1124
|
+
message: messageText,
|
|
1125
|
+
...nameText !== void 0 && { name: nameText },
|
|
1126
|
+
...diffText !== void 0 && { diff: diffText },
|
|
1127
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1079
1128
|
...signatureHash !== null && { signatureHash },
|
|
1080
1129
|
...frames.length > 0 && { frames },
|
|
1081
1130
|
ordinal
|
|
@@ -1090,7 +1139,16 @@ var AgentReporter = class {
|
|
|
1090
1139
|
const inputs = [];
|
|
1091
1140
|
for (let ordinal = 0; ordinal < modErrors.length; ordinal++) {
|
|
1092
1141
|
const e = modErrors[ordinal];
|
|
1093
|
-
const
|
|
1142
|
+
const messageText = coerceErrorField(e, "message") ?? "<missing message>";
|
|
1143
|
+
const nameText = coerceErrorField(e, "name");
|
|
1144
|
+
const stackText = coerceErrorField(e, "stack");
|
|
1145
|
+
const stacksValue = readErrorStacks(e);
|
|
1146
|
+
const { frames, signatureHash } = processFailure({
|
|
1147
|
+
message: messageText,
|
|
1148
|
+
...nameText !== void 0 && { name: nameText },
|
|
1149
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1150
|
+
...stacksValue !== void 0 && { stacks: stacksValue }
|
|
1151
|
+
});
|
|
1094
1152
|
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1095
1153
|
signatureHash,
|
|
1096
1154
|
runId,
|
|
@@ -1099,9 +1157,9 @@ var AgentReporter = class {
|
|
|
1099
1157
|
inputs.push({
|
|
1100
1158
|
moduleId,
|
|
1101
1159
|
scope: "module",
|
|
1102
|
-
message:
|
|
1103
|
-
...
|
|
1104
|
-
...
|
|
1160
|
+
message: messageText,
|
|
1161
|
+
...nameText !== void 0 && { name: nameText },
|
|
1162
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1105
1163
|
...signatureHash !== null && { signatureHash },
|
|
1106
1164
|
...frames.length > 0 && { frames },
|
|
1107
1165
|
ordinal
|
|
@@ -1114,7 +1172,16 @@ var AgentReporter = class {
|
|
|
1114
1172
|
const inputs = [];
|
|
1115
1173
|
for (let ordinal = 0; ordinal < errors.length; ordinal++) {
|
|
1116
1174
|
const e = errors[ordinal];
|
|
1117
|
-
const
|
|
1175
|
+
const messageText = coerceErrorField(e, "message") ?? "<missing message>";
|
|
1176
|
+
const nameText = coerceErrorField(e, "name");
|
|
1177
|
+
const stackText = coerceErrorField(e, "stack");
|
|
1178
|
+
const stacksValue = readErrorStacks(e);
|
|
1179
|
+
const { frames, signatureHash } = processFailure({
|
|
1180
|
+
message: messageText,
|
|
1181
|
+
...nameText !== void 0 && { name: nameText },
|
|
1182
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1183
|
+
...stacksValue !== void 0 && { stacks: stacksValue }
|
|
1184
|
+
});
|
|
1118
1185
|
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1119
1186
|
signatureHash,
|
|
1120
1187
|
runId,
|
|
@@ -1122,9 +1189,9 @@ var AgentReporter = class {
|
|
|
1122
1189
|
});
|
|
1123
1190
|
inputs.push({
|
|
1124
1191
|
scope: "unhandled",
|
|
1125
|
-
message:
|
|
1126
|
-
...
|
|
1127
|
-
...
|
|
1192
|
+
message: messageText,
|
|
1193
|
+
...nameText !== void 0 && { name: nameText },
|
|
1194
|
+
...stackText !== void 0 && { stack: stackText },
|
|
1128
1195
|
...signatureHash !== null && { signatureHash },
|
|
1129
1196
|
...frames.length > 0 && { frames },
|
|
1130
1197
|
ordinal
|
|
@@ -1155,7 +1222,7 @@ var AgentReporter = class {
|
|
|
1155
1222
|
const tcResult = tc.result();
|
|
1156
1223
|
if (tcResult?.state === "failed") {
|
|
1157
1224
|
const errors = tcResult.errors;
|
|
1158
|
-
errorMap.set(key, errors?.[0]
|
|
1225
|
+
errorMap.set(key, coerceErrorField(errors?.[0], "message") ?? null);
|
|
1159
1226
|
}
|
|
1160
1227
|
}
|
|
1161
1228
|
}
|
|
@@ -1291,10 +1358,29 @@ var AgentReporter = class {
|
|
|
1291
1358
|
direction: trendSummary.direction,
|
|
1292
1359
|
runCount: trendSummary.runCount
|
|
1293
1360
|
});
|
|
1361
|
+
const classifications = /* @__PURE__ */ new Map();
|
|
1362
|
+
for (const report of reports) for (const mod of report.failed) for (const test of mod.tests) if (test.classification) classifications.set(test.fullName, test.classification);
|
|
1294
1363
|
yield* Effect.logInfo("reports built").pipe(Effect.annotateLogs({
|
|
1295
1364
|
count: reports.length,
|
|
1296
1365
|
projects: Array.from(projectGroups.keys()).join(", ")
|
|
1297
1366
|
}));
|
|
1367
|
+
return {
|
|
1368
|
+
reports,
|
|
1369
|
+
classifications,
|
|
1370
|
+
...trendSummary !== void 0 && { trendSummary }
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
let persistFailure;
|
|
1374
|
+
let persistResult;
|
|
1375
|
+
if (persistDisabled === void 0 && dbPath !== void 0) persistResult = await Effect.runPromise(persistProgram.pipe(Effect.annotateLogs("service", "reporter"), Effect.provide(ReporterLive(dbPath, logLevel, logFile)))).catch((err) => {
|
|
1376
|
+
persistFailure = formatFatalError(err);
|
|
1377
|
+
});
|
|
1378
|
+
const renderInputData = persistResult ?? {
|
|
1379
|
+
reports: fallbackReports,
|
|
1380
|
+
classifications: /* @__PURE__ */ new Map()
|
|
1381
|
+
};
|
|
1382
|
+
const renderProgram = Effect.gen(function* () {
|
|
1383
|
+
const { reports, classifications, trendSummary } = renderInputData;
|
|
1298
1384
|
const detector = yield* EnvironmentDetector;
|
|
1299
1385
|
const executorResolver = yield* ExecutorResolver;
|
|
1300
1386
|
const formatSelector = yield* FormatSelector;
|
|
@@ -1316,8 +1402,6 @@ var AgentReporter = class {
|
|
|
1316
1402
|
format,
|
|
1317
1403
|
detail
|
|
1318
1404
|
}));
|
|
1319
|
-
const classifications = /* @__PURE__ */ new Map();
|
|
1320
|
-
for (const report of reports) for (const mod of report.failed) for (const test of mod.tests) if (test.classification) classifications.set(test.fullName, test.classification);
|
|
1321
1405
|
const githubSummaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
1322
1406
|
const kit = buildReporterKit({
|
|
1323
1407
|
env,
|
|
@@ -1350,9 +1434,11 @@ var AgentReporter = class {
|
|
|
1350
1434
|
}));
|
|
1351
1435
|
for (const output of allOutputs) routeRenderedOutput(output, { ...githubSummaryFile !== void 0 && { githubSummaryFile } });
|
|
1352
1436
|
});
|
|
1353
|
-
await Effect.runPromise(
|
|
1437
|
+
await Effect.runPromise(renderProgram.pipe(Effect.annotateLogs("service", "reporter"), Effect.provide(OutputPipelineLive), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1354
1438
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1355
1439
|
});
|
|
1440
|
+
const persistError = persistDisabled ?? persistFailure;
|
|
1441
|
+
if (persistError !== void 0) process.stderr.write(`vitest-agent: persistence failed — results above were rendered but NOT recorded: ${persistError}\n`);
|
|
1356
1442
|
}
|
|
1357
1443
|
};
|
|
1358
1444
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toPosixPath } from "./to-posix-path.js";
|
|
2
2
|
import { DefaultDiscoverStrategy } from "./discover-strategy.js";
|
|
3
|
+
import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR } from "@vitest-agent/sdk";
|
|
3
4
|
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
4
5
|
import { readdir, stat } from "node:fs/promises";
|
|
5
6
|
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
@@ -17,47 +18,60 @@ const _cache = /* @__PURE__ */ new Map();
|
|
|
17
18
|
* files between `discoverProjects` calls without re-walking test-file globs.
|
|
18
19
|
* Returns an empty string when `dirPath` does not exist — this still produces
|
|
19
20
|
* a stable, comparable signature contribution.
|
|
21
|
+
*
|
|
22
|
+
* Manual per-directory walk (not a single `readdir({ recursive: true })`
|
|
23
|
+
* call) pruning `NON_DISCOVERABLE_DIRS` *before* recursing: a `src/` or
|
|
24
|
+
* `__test__/` dir can itself contain a fixture `node_modules` (e.g. a
|
|
25
|
+
* subprocess-e2e fixture that installs deps), and Node's recursive `readdir`
|
|
26
|
+
* follows symlinked directories, so an unguarded call would walk into it.
|
|
20
27
|
*/
|
|
21
28
|
async function computeDirSignature(dirPath) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
await walkDirSignature(dirPath, dirPath, parts);
|
|
31
|
+
parts.sort();
|
|
32
|
+
return parts.join("|");
|
|
33
|
+
}
|
|
34
|
+
async function walkDirSignature(root, dir, parts) {
|
|
22
35
|
let entries;
|
|
23
36
|
try {
|
|
24
|
-
entries = await readdir(
|
|
25
|
-
withFileTypes: true,
|
|
26
|
-
recursive: true
|
|
27
|
-
});
|
|
37
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
28
38
|
} catch {
|
|
29
|
-
return
|
|
39
|
+
return;
|
|
30
40
|
}
|
|
31
|
-
const parts = [];
|
|
32
41
|
for (const ent of entries) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const fullPath = join(dir, ent.name);
|
|
43
|
+
if (ent.isDirectory()) {
|
|
44
|
+
if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
|
|
45
|
+
await walkDirSignature(root, fullPath, parts);
|
|
46
|
+
} else if (ent.isFile()) {
|
|
47
|
+
let mtimeMs;
|
|
48
|
+
try {
|
|
49
|
+
mtimeMs = (await stat(fullPath)).mtimeMs;
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const relPath = toPosixPath(relative(root, fullPath));
|
|
54
|
+
parts.push(`${relPath}:${mtimeMs}`);
|
|
41
55
|
}
|
|
42
|
-
const relPath = toPosixPath(relative(dirPath, fullPath));
|
|
43
|
-
parts.push(`${relPath}:${mtimeMs}`);
|
|
44
56
|
}
|
|
45
|
-
parts.sort();
|
|
46
|
-
return parts.join("|");
|
|
47
57
|
}
|
|
48
58
|
/**
|
|
49
|
-
* Computes a cheap whole-workspace directory signature
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
59
|
+
* Computes a cheap whole-workspace directory signature from each package's
|
|
60
|
+
* `src/` and `__test__/` directories (issue #100). Only entries + mtimes are
|
|
61
|
+
* read — no file content — so this stays fast for large monorepos. A changed
|
|
62
|
+
* signature means a test file was added, removed, moved, or renamed since
|
|
63
|
+
* the cached result was computed.
|
|
64
|
+
*
|
|
65
|
+
* Only the two anchored directories are fingerprinted. Discovery cannot see a
|
|
66
|
+
* test file anywhere else, so nothing else can change the emitted project list
|
|
67
|
+
* (issue #227).
|
|
54
68
|
*/
|
|
55
69
|
async function computeWorkspaceSignature(packages) {
|
|
56
70
|
const parts = [];
|
|
57
71
|
for (const pkg of packages) {
|
|
58
|
-
const srcSig = await computeDirSignature(join(pkg.path,
|
|
59
|
-
const
|
|
60
|
-
parts.push(`${pkg.path}::src=${srcSig}::__test__=${
|
|
72
|
+
const srcSig = await computeDirSignature(join(pkg.path, SRC_DIR));
|
|
73
|
+
const testSig = await computeDirSignature(join(pkg.path, TEST_DIR));
|
|
74
|
+
parts.push(`${pkg.path}::src=${srcSig}::__test__=${testSig}`);
|
|
61
75
|
}
|
|
62
76
|
return parts.join("\n");
|
|
63
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findTestFiles } from "./find-test-files.js";
|
|
2
2
|
import { Tag } from "./tag.js";
|
|
3
|
+
import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS } from "@vitest-agent/sdk";
|
|
3
4
|
import { join, sep } from "node:path";
|
|
4
5
|
import { stat } from "node:fs/promises";
|
|
5
6
|
import { configDefaults } from "vitest/config";
|
|
@@ -11,18 +12,6 @@ const SETUP_EXTS = [
|
|
|
11
12
|
"js",
|
|
12
13
|
"jsx"
|
|
13
14
|
];
|
|
14
|
-
const TEST_DIR_HELPER_DIRS = [
|
|
15
|
-
"utils",
|
|
16
|
-
"fixtures",
|
|
17
|
-
"snapshots"
|
|
18
|
-
];
|
|
19
|
-
async function isDir(p) {
|
|
20
|
-
try {
|
|
21
|
-
return (await stat(p)).isDirectory();
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
15
|
async function isFile(p) {
|
|
27
16
|
try {
|
|
28
17
|
return (await stat(p)).isFile();
|
|
@@ -123,17 +112,19 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
123
112
|
return ["unit"];
|
|
124
113
|
}
|
|
125
114
|
async buildProject(input) {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const testPrefix = join(input.path, "__test__");
|
|
129
|
-
const allFiles = await findTestFiles(input.path, ["src/**/*.{test,spec}.{ts,tsx,js,jsx}", "__test__/**/*.{test,spec}.{ts,tsx,js,jsx}"]);
|
|
115
|
+
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}`]);
|
|
130
117
|
if (allFiles.length === 0) return null;
|
|
131
118
|
const hasSrcTests = allFiles.some((f) => f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix);
|
|
132
|
-
const hasTestDirTests = allFiles.some((f) => f.startsWith(`${
|
|
119
|
+
const hasTestDirTests = allFiles.some((f) => !(f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix));
|
|
133
120
|
const include = [];
|
|
134
|
-
if (hasSrcTests) include.push(join(input.path, "
|
|
135
|
-
if (hasTestDirTests) include.push(join(input.path, "
|
|
136
|
-
const exclude =
|
|
121
|
+
if (hasSrcTests) include.push(join(input.path, SRC_DIR, "**", TEST_FILE_GLOB_SUFFIX));
|
|
122
|
+
if (hasTestDirTests) include.push(join(input.path, TEST_DIR, "**", TEST_FILE_GLOB_SUFFIX));
|
|
123
|
+
const exclude = [
|
|
124
|
+
...configDefaults.exclude,
|
|
125
|
+
...[SRC_DIR, TEST_DIR].flatMap((root) => [...NON_DISCOVERABLE_DIRS].map((d) => join(input.path, root, "**", d, "**"))),
|
|
126
|
+
...hasTestDirTests ? TEST_HELPER_DIRS.map((d) => join(input.path, TEST_DIR, "**", d, "**")) : []
|
|
127
|
+
];
|
|
137
128
|
const setupFile = await detectSetupFile(input.path);
|
|
138
129
|
return {
|
|
139
130
|
extends: true,
|
|
@@ -141,7 +132,7 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
141
132
|
name: input.name,
|
|
142
133
|
environment: "node",
|
|
143
134
|
include,
|
|
144
|
-
|
|
135
|
+
exclude,
|
|
145
136
|
...setupFile ? { setupFiles: [join(input.path, setupFile)] } : {}
|
|
146
137
|
}
|
|
147
138
|
};
|
package/utils/find-test-files.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { toPosixPath } from "./to-posix-path.js";
|
|
2
|
+
import { NON_DISCOVERABLE_DIRS } from "@vitest-agent/sdk";
|
|
2
3
|
import { join, relative } from "node:path";
|
|
3
4
|
import { readdir } from "node:fs/promises";
|
|
4
5
|
|
|
5
6
|
//#region src/utils/find-test-files.ts
|
|
6
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7
|
-
"node_modules",
|
|
8
|
-
".git",
|
|
9
|
-
"dist"
|
|
10
|
-
]);
|
|
11
7
|
function globToRegex(pattern) {
|
|
12
8
|
const alts = expandBraces(pattern).map(toRegexFragment);
|
|
13
9
|
return new RegExp(`^(?:${alts.join("|")})$`);
|
|
@@ -57,8 +53,16 @@ function toRegexFragment(glob) {
|
|
|
57
53
|
* Async file walker that returns matched absolute paths.
|
|
58
54
|
*
|
|
59
55
|
* Walks `dir` recursively via `node:fs/promises`. Skips `node_modules`, `.git`,
|
|
60
|
-
* and `dist` directories
|
|
61
|
-
*
|
|
56
|
+
* and `dist` directories, and does not descend past a nested `package.json`
|
|
57
|
+
* boundary (any directory other than `dir` itself that has its own
|
|
58
|
+
* `package.json` is treated as an independent unit — its files belong to a
|
|
59
|
+
* separate discovery pass, not this one). The package-boundary rule applies
|
|
60
|
+
* to every supplied pattern, not just an unanchored `**\/` one — an anchored
|
|
61
|
+
* pattern like `"src/**\/*.test.ts"` will not match a test file under a
|
|
62
|
+
* nested package.json even though the pattern itself never reaches past
|
|
63
|
+
* `src/`, because the boundary check runs once per directory, independent of
|
|
64
|
+
* which pattern is being matched. Matches files against the supplied glob
|
|
65
|
+
* patterns relative to `dir` (e.g. `"src/**\/*.test.ts"`).
|
|
62
66
|
*
|
|
63
67
|
* Returns an empty array if `dir` does not exist or no files match.
|
|
64
68
|
* @param dir - Absolute path to the directory to walk
|
|
@@ -80,8 +84,9 @@ async function walkDir(root, dir, matchers, results) {
|
|
|
80
84
|
} catch {
|
|
81
85
|
return;
|
|
82
86
|
}
|
|
87
|
+
if (dir !== root && entries.some((ent) => ent.isFile() && ent.name === "package.json")) return;
|
|
83
88
|
for (const ent of entries) {
|
|
84
|
-
if (
|
|
89
|
+
if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
|
|
85
90
|
const fullPath = join(dir, ent.name);
|
|
86
91
|
if (ent.isDirectory()) await walkDir(root, fullPath, matchers, results);
|
|
87
92
|
else if (ent.isFile()) {
|