@vitest-agent/plugin 4.0.3 → 4.0.4
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 +32 -2
- package/layers/CoverageAnalyzerLive.js +32 -9
- package/package.json +7 -7
- package/plugin.js +37 -7
- package/reporter.js +30 -5
- package/utils/is-partial-run.js +70 -3
package/index.d.ts
CHANGED
|
@@ -693,6 +693,22 @@ export declare class AgentReporter {
|
|
|
693
693
|
* @internal
|
|
694
694
|
*/
|
|
695
695
|
private neutralizedThresholdSnapshot;
|
|
696
|
+
/**
|
|
697
|
+
* Snapshot of `vitest.configOverride.testNamePattern` taken once, at
|
|
698
|
+
* `onInit`. Vitest's own startup copy of the RESOLVED `testNamePattern`
|
|
699
|
+
* (merging `vitest.config.ts`'s `test.testNamePattern` with any `-t`
|
|
700
|
+
* flag) into `configOverride` runs before any reporter's `onInit`
|
|
701
|
+
* (Vitest 5.0 `Vitest._setServer`), so this snapshot always observes it.
|
|
702
|
+
* Compared against the same field read again at `onTestRunEnd` — a
|
|
703
|
+
* difference means a watch-mode `t` keypress (`Vitest.changeNamePattern`)
|
|
704
|
+
* ran in between; no difference means the pattern, if any, was already
|
|
705
|
+
* baked into the resolved config (issue #401 regression: a project-level
|
|
706
|
+
* `testNamePattern` must not make every run partial). See
|
|
707
|
+
* `hasTestNameFilter` in `utils/is-partial-run.ts` for the decision rule.
|
|
708
|
+
*
|
|
709
|
+
* @internal
|
|
710
|
+
*/
|
|
711
|
+
private initialTestNamePattern;
|
|
696
712
|
constructor(options?: AgentReporterConstructorOptions);
|
|
697
713
|
/**
|
|
698
714
|
* The resolved reporter config built at construction time. Exposed for
|
|
@@ -761,8 +777,8 @@ export declare class AgentReporter {
|
|
|
761
777
|
*/
|
|
762
778
|
onTestRunStart(_specifications: ReadonlyArray<unknown>): void;
|
|
763
779
|
/**
|
|
764
|
-
* Read the owning Vitest project name off a `TestModule`. Vitest
|
|
765
|
-
* attaches `project` to every module; an empty name (the unnamed
|
|
780
|
+
* Read the owning Vitest project name off a `TestModule`. Vitest 5
|
|
781
|
+
* attaches `project` to every module (`reported-tasks.ts`); an empty name (the unnamed
|
|
766
782
|
* default project) collapses to `undefined` so the renderer treats it
|
|
767
783
|
* as a single anonymous project.
|
|
768
784
|
*
|
|
@@ -1236,6 +1252,20 @@ interface CoverageOptions {
|
|
|
1236
1252
|
* "N of M test files" instead of just "N".
|
|
1237
1253
|
*/
|
|
1238
1254
|
readonly totalFiles?: number;
|
|
1255
|
+
/**
|
|
1256
|
+
* The Vitest config root. When set, every coverage-map key is matched
|
|
1257
|
+
* against threshold / target glob patterns as `relative(root, key)` —
|
|
1258
|
+
* the shape Vitest's own threshold evaluator globs on. The v8 and
|
|
1259
|
+
* istanbul providers key the map by ABSOLUTE path, so without this a
|
|
1260
|
+
* relative pattern like `src/**\/*.ts` never matches. Absent, keys are
|
|
1261
|
+
* matched verbatim. Reported `file` fields keep the map's original key.
|
|
1262
|
+
*
|
|
1263
|
+
* `root` does NOT apply to `processScoped`'s `testedFiles`: those are
|
|
1264
|
+
* compared against the map key with only separators normalized, so
|
|
1265
|
+
* callers pass the same shape the provider uses (absolute paths in
|
|
1266
|
+
* production; forward or back slashes both work).
|
|
1267
|
+
*/
|
|
1268
|
+
readonly root?: string;
|
|
1239
1269
|
}
|
|
1240
1270
|
declare const CoverageAnalyzer_base: Context.ServiceClass<CoverageAnalyzer, "vitest-agent/CoverageAnalyzer", {
|
|
1241
1271
|
readonly process: (coverage: unknown, options: CoverageOptions) => Effect.Effect<Option.Option<CoverageReport>>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CoverageAnalyzer } from "../services/CoverageAnalyzer.js";
|
|
2
|
+
import { toPosixPath } from "../utils/to-posix-path.js";
|
|
3
|
+
import { relative } from "node:path";
|
|
2
4
|
import { compressLines } from "@vitest-agent/sdk";
|
|
3
|
-
import { Effect, Layer, Option } from "effect";
|
|
5
|
+
import { Effect, Layer, Option, Result } from "effect";
|
|
6
|
+
import { GlobPattern } from "@effected/glob";
|
|
4
7
|
|
|
5
8
|
//#region src/layers/CoverageAnalyzerLive.ts
|
|
6
9
|
/**
|
|
@@ -23,12 +26,29 @@ function isIstanbulCoverageMap(value) {
|
|
|
23
26
|
return typeof obj.getCoverageSummary === "function" && typeof obj.files === "function" && typeof obj.fileCoverageFor === "function";
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* Compiled matchers keyed by pattern source. Threshold patterns are a
|
|
30
|
+
* small, fixed set per run and `processCoverageInternal` matches every
|
|
31
|
+
* file against every pattern, so compile once and reuse.
|
|
32
|
+
*/
|
|
33
|
+
const globCache = /* @__PURE__ */ new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Match a file path against a coverage-threshold glob with the same
|
|
36
|
+
* semantics Vitest applies to `coverage.thresholds` keys (picomatch under
|
|
37
|
+
* default options): `**` spans zero or more directories, `*` and `?`
|
|
38
|
+
* never cross a slash, brace groups expand, character classes and
|
|
39
|
+
* extglobs work, and dotfiles are not matched by wildcards (issue #381).
|
|
40
|
+
* `@effected/glob` is minimatch-based and agrees with picomatch on every
|
|
41
|
+
* shape a threshold key realistically takes. A pattern that fails to
|
|
42
|
+
* compile (guard trip on an absurd input) matches nothing.
|
|
28
43
|
*/
|
|
29
44
|
function matchGlob(filePath, pattern) {
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
let compiled = globCache.get(pattern);
|
|
46
|
+
if (compiled === void 0) {
|
|
47
|
+
const result = GlobPattern.compileResult(pattern);
|
|
48
|
+
compiled = Result.isSuccess(result) ? result.success : null;
|
|
49
|
+
globCache.set(pattern, compiled);
|
|
50
|
+
}
|
|
51
|
+
return compiled?.matches(filePath) ?? false;
|
|
32
52
|
}
|
|
33
53
|
/**
|
|
34
54
|
* Resolve the effective thresholds for a file path by checking pattern
|
|
@@ -85,10 +105,13 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
|
|
|
85
105
|
functions: summary.functions.pct,
|
|
86
106
|
lines: summary.lines.pct
|
|
87
107
|
};
|
|
88
|
-
const testedFileSet = testedFiles ? new Set(testedFiles) : void 0;
|
|
108
|
+
const testedFileSet = testedFiles ? new Set(testedFiles.map(toPosixPath)) : void 0;
|
|
89
109
|
const lowCoverage = [];
|
|
90
110
|
const belowTarget = [];
|
|
111
|
+
const { root } = options;
|
|
112
|
+
const matchKey = (filePath) => toPosixPath(root === void 0 ? filePath : relative(root, filePath));
|
|
91
113
|
for (const filePath of coverageMap.files()) {
|
|
114
|
+
const matchPath = matchKey(filePath);
|
|
92
115
|
const fileCoverage = coverageMap.fileCoverageFor(filePath);
|
|
93
116
|
const fileSummary = fileCoverage.toSummary();
|
|
94
117
|
const fileStats = {
|
|
@@ -99,8 +122,8 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
|
|
|
99
122
|
};
|
|
100
123
|
const isBareZero = fileStats.statements === 0 && fileStats.branches === 0 && fileStats.functions === 0 && fileStats.lines === 0;
|
|
101
124
|
if (isBareZero && !includeBareZero) continue;
|
|
102
|
-
if (scoped && !testedFileSet?.has(filePath)) continue;
|
|
103
|
-
const isBelowThreshold = isBelowMetricThresholds(fileStats, resolveEffectivePerFileThresholds(
|
|
125
|
+
if (scoped && !testedFileSet?.has(toPosixPath(filePath))) continue;
|
|
126
|
+
const isBelowThreshold = isBelowMetricThresholds(fileStats, resolveEffectivePerFileThresholds(matchPath, options.thresholds) ?? resolveEffectiveThresholds(matchPath, options.thresholds));
|
|
104
127
|
if (isBareZero || isBelowThreshold) {
|
|
105
128
|
const uncoveredLines = compressLines(fileCoverage.getUncoveredLines());
|
|
106
129
|
lowCoverage.push({
|
|
@@ -111,7 +134,7 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
|
|
|
111
134
|
continue;
|
|
112
135
|
}
|
|
113
136
|
if (options.targets) {
|
|
114
|
-
if (isBelowMetricThresholds(fileStats, resolveEffectiveThresholds(
|
|
137
|
+
if (isBelowMetricThresholds(fileStats, resolveEffectiveThresholds(matchPath, options.targets))) {
|
|
115
138
|
const uncoveredLines = compressLines(fileCoverage.getUncoveredLines());
|
|
116
139
|
belowTarget.push({
|
|
117
140
|
file: filePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/plugin",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
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": [
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
47
|
-
"@
|
|
47
|
+
"@effected/glob": "^0.6.0",
|
|
48
48
|
"@effected/workspaces": "^0.22.0",
|
|
49
|
-
"@vitest-agent/cli": "3.0.
|
|
50
|
-
"@vitest-agent/engine": "0.1.
|
|
51
|
-
"@vitest-agent/mcp": "4.0.
|
|
52
|
-
"@vitest-agent/reporter": "3.0.
|
|
53
|
-
"@vitest-agent/sdk": "5.0.
|
|
49
|
+
"@vitest-agent/cli": "3.0.3",
|
|
50
|
+
"@vitest-agent/engine": "0.1.3",
|
|
51
|
+
"@vitest-agent/mcp": "4.0.3",
|
|
52
|
+
"@vitest-agent/reporter": "3.0.6",
|
|
53
|
+
"@vitest-agent/sdk": "5.0.1",
|
|
54
54
|
"effect": "4.0.0-rc.115",
|
|
55
55
|
"magic-string": "^1.4.1"
|
|
56
56
|
},
|
package/plugin.js
CHANGED
|
@@ -40,16 +40,22 @@ import { Effect, Schema } from "effect";
|
|
|
40
40
|
* the dedicated `ci-annotations` reporter is opt-in until the GHA
|
|
41
41
|
* annotations writer ships).
|
|
42
42
|
*
|
|
43
|
+
* An invalid `VITEST_AGENT_CONSOLE` value is ignored with a diagnostic
|
|
44
|
+
* line sent to `report` (default: `process.stderr`). `configureVitest`
|
|
45
|
+
* passes the per-Vitest-instance dedupe sink so the line prints once per
|
|
46
|
+
* run rather than once per project (issue #459).
|
|
47
|
+
*
|
|
43
48
|
* @internal
|
|
44
49
|
*/
|
|
45
|
-
function resolveConsoleMode(options, executor, _env) {
|
|
50
|
+
function resolveConsoleMode(options, executor, _env, report = (line) => {
|
|
51
|
+
process.stderr.write(line);
|
|
52
|
+
}) {
|
|
46
53
|
const override = process.env.VITEST_AGENT_CONSOLE;
|
|
47
54
|
if (override !== void 0 && override !== "") {
|
|
48
55
|
if (executor === "human" && Schema.is(HumanConsoleMode)(override)) return override;
|
|
49
56
|
if (executor === "agent" && Schema.is(AgentConsoleMode)(override)) return override;
|
|
50
57
|
if (executor !== "human" && executor !== "agent" && Schema.is(CiConsoleMode)(override)) return override;
|
|
51
|
-
|
|
52
|
-
process.stderr.write(`[vitest-agent:plugin] ignoring invalid VITEST_AGENT_CONSOLE="${override}" for ${executor} executor; accepted for ${executor}: ${accepted.join(" | ")}\n`);
|
|
58
|
+
report(`[vitest-agent:plugin] ignoring invalid VITEST_AGENT_CONSOLE="${override}" for ${executor} executor; accepted for ${executor}: ${(executor === "human" ? HumanConsoleMode.literals : executor === "agent" ? AgentConsoleMode.literals : CiConsoleMode.literals).join(" | ")}\n`);
|
|
53
59
|
}
|
|
54
60
|
const console = options.console;
|
|
55
61
|
if (executor === "human") return console?.human ?? "passthrough";
|
|
@@ -120,6 +126,20 @@ const cacheKeyGeneratorByVitest = /* @__PURE__ */ new WeakSet();
|
|
|
120
126
|
*/
|
|
121
127
|
const coverageDirDecidedByVitest = /* @__PURE__ */ new WeakSet();
|
|
122
128
|
/**
|
|
129
|
+
* Per-Vitest-instance memory of which `ConfigValidation` diagnostic lines
|
|
130
|
+
* have already been written to stderr (issue #400). `configureVitest` fires
|
|
131
|
+
* once per project, but `ConfigValidation` runs against root-level Vitest
|
|
132
|
+
* config that is identical across every project in the run — without this
|
|
133
|
+
* guard, an N-project run prints N identical copies of every warning/info
|
|
134
|
+
* line. Keyed on the vitest instance (like its `WeakSet` siblings above) so
|
|
135
|
+
* a different Vitest run in the same process gets a fresh, empty `Set` and
|
|
136
|
+
* reports again; the dedupe key is the fully rendered line (code + message
|
|
137
|
+
* + remediation), matching what actually reaches stderr.
|
|
138
|
+
*
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
const reportedConfigDiagnosticsByVitest = /* @__PURE__ */ new WeakMap();
|
|
142
|
+
/**
|
|
123
143
|
* The version of this package, inlined at build time from
|
|
124
144
|
* `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
|
|
125
145
|
* Re-exported from the package barrel as the public symbol; defined here so
|
|
@@ -127,7 +147,7 @@ const coverageDirDecidedByVitest = /* @__PURE__ */ new WeakSet();
|
|
|
127
147
|
*
|
|
128
148
|
* @public
|
|
129
149
|
*/
|
|
130
|
-
const CURRENT_PLUGIN_VERSION = "4.0.
|
|
150
|
+
const CURRENT_PLUGIN_VERSION = "4.0.4";
|
|
131
151
|
const TEST_FILE_DIR_RE = new RegExp(`/(?:${SRC_DIR}|${TEST_DIR})/`);
|
|
132
152
|
const isTestFile = (id) => isTestFileName(id) && TEST_FILE_DIR_RE.test(id);
|
|
133
153
|
/**
|
|
@@ -209,7 +229,17 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
209
229
|
const reportOption = options.report;
|
|
210
230
|
const reportScope = reportOption === false ? void 0 : executor === "human" && reportOption === void 0 ? void 0 : reportOption?.scope ?? "vitest-agent";
|
|
211
231
|
if (reportScope !== void 0) assertFlatScope(reportScope);
|
|
212
|
-
|
|
232
|
+
let reportedDiagnostics = reportedConfigDiagnosticsByVitest.get(vitest);
|
|
233
|
+
if (!reportedDiagnostics) {
|
|
234
|
+
reportedDiagnostics = /* @__PURE__ */ new Set();
|
|
235
|
+
reportedConfigDiagnosticsByVitest.set(vitest, reportedDiagnostics);
|
|
236
|
+
}
|
|
237
|
+
const reportOnce = (line) => {
|
|
238
|
+
if (reportedDiagnostics.has(line)) return;
|
|
239
|
+
reportedDiagnostics.add(line);
|
|
240
|
+
process.stderr.write(line);
|
|
241
|
+
};
|
|
242
|
+
const consoleMode = resolveConsoleMode(options, executor, env, reportOnce);
|
|
213
243
|
const format = resolveFormat(consoleMode);
|
|
214
244
|
const mcp = executor === "agent";
|
|
215
245
|
log("env:", env, "| executor:", executor, "| consoleMode:", consoleMode, "| format:", format, "| mcp (auto):", mcp);
|
|
@@ -234,8 +264,8 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
234
264
|
vitestConfig: vitest.config,
|
|
235
265
|
pluginOptions: options
|
|
236
266
|
})), ConfigValidationLive));
|
|
237
|
-
for (const w of validation.warnings)
|
|
238
|
-
for (const i of validation.info)
|
|
267
|
+
for (const w of validation.warnings) reportOnce(`[vitest-agent:plugin] warning ${w.code}: ${w.message}` + (w.remediation ? `\n ${w.remediation}` : "") + "\n");
|
|
268
|
+
for (const i of validation.info) reportOnce(`[vitest-agent:plugin] info ${i.code}: ${i.message}\n`);
|
|
239
269
|
if (validation.errors.length > 0) {
|
|
240
270
|
const body = validation.errors.map((e) => `${e.code}${e.path ? ` @ ${e.path}` : ""}: ${e.message}` + (e.remediation ? `\n ${e.remediation}` : "")).join("\n");
|
|
241
271
|
throw new Error(body);
|
package/reporter.js
CHANGED
|
@@ -387,6 +387,22 @@ var AgentReporter = class {
|
|
|
387
387
|
* @internal
|
|
388
388
|
*/
|
|
389
389
|
neutralizedThresholdSnapshot = /* @__PURE__ */ new Map();
|
|
390
|
+
/**
|
|
391
|
+
* Snapshot of `vitest.configOverride.testNamePattern` taken once, at
|
|
392
|
+
* `onInit`. Vitest's own startup copy of the RESOLVED `testNamePattern`
|
|
393
|
+
* (merging `vitest.config.ts`'s `test.testNamePattern` with any `-t`
|
|
394
|
+
* flag) into `configOverride` runs before any reporter's `onInit`
|
|
395
|
+
* (Vitest 5.0 `Vitest._setServer`), so this snapshot always observes it.
|
|
396
|
+
* Compared against the same field read again at `onTestRunEnd` — a
|
|
397
|
+
* difference means a watch-mode `t` keypress (`Vitest.changeNamePattern`)
|
|
398
|
+
* ran in between; no difference means the pattern, if any, was already
|
|
399
|
+
* baked into the resolved config (issue #401 regression: a project-level
|
|
400
|
+
* `testNamePattern` must not make every run partial). See
|
|
401
|
+
* `hasTestNameFilter` in `utils/is-partial-run.ts` for the decision rule.
|
|
402
|
+
*
|
|
403
|
+
* @internal
|
|
404
|
+
*/
|
|
405
|
+
initialTestNamePattern;
|
|
390
406
|
constructor(options = {}) {
|
|
391
407
|
this.logLevel = resolveLogLevel(process.env);
|
|
392
408
|
this.logFile = resolveLogFile(process.env);
|
|
@@ -467,6 +483,7 @@ var AgentReporter = class {
|
|
|
467
483
|
*/
|
|
468
484
|
async onInit(vitest) {
|
|
469
485
|
this._vitest = vitest;
|
|
486
|
+
this.initialTestNamePattern = vitest?.configOverride?.testNamePattern;
|
|
470
487
|
if (this.options.reportScope !== void 0) {
|
|
471
488
|
assertReportCapable(vitest);
|
|
472
489
|
this.reportWriter = createReportWriter(vitest, this.options.reportScope);
|
|
@@ -611,8 +628,8 @@ var AgentReporter = class {
|
|
|
611
628
|
});
|
|
612
629
|
}
|
|
613
630
|
/**
|
|
614
|
-
* Read the owning Vitest project name off a `TestModule`. Vitest
|
|
615
|
-
* attaches `project` to every module; an empty name (the unnamed
|
|
631
|
+
* Read the owning Vitest project name off a `TestModule`. Vitest 5
|
|
632
|
+
* attaches `project` to every module (`reported-tasks.ts`); an empty name (the unnamed
|
|
616
633
|
* default project) collapses to `undefined` so the renderer treats it
|
|
617
634
|
* as a single anonymous project.
|
|
618
635
|
*
|
|
@@ -1055,9 +1072,15 @@ var AgentReporter = class {
|
|
|
1055
1072
|
filenamePattern: vitestForPartialCheck?.filenamePattern,
|
|
1056
1073
|
startedSpecCount,
|
|
1057
1074
|
totalSpecCount,
|
|
1058
|
-
projectFilter: opts.projectFilter
|
|
1075
|
+
projectFilter: opts.projectFilter,
|
|
1076
|
+
cliFilters: vitestForPartialCheck?.config?.cliOptions,
|
|
1077
|
+
testNamePattern: {
|
|
1078
|
+
cli: vitestForPartialCheck?.config?.cliOptions?.testNamePattern,
|
|
1079
|
+
initial: this.initialTestNamePattern,
|
|
1080
|
+
current: vitestForPartialCheck?.configOverride?.testNamePattern
|
|
1081
|
+
}
|
|
1059
1082
|
});
|
|
1060
|
-
const testedFiles = isPartial ? Array.from(new Set(modules.map((m) => m.
|
|
1083
|
+
const testedFiles = isPartial ? Array.from(new Set(modules.map((m) => m.moduleId.replace(/\.test\.([^.]+)$/, ".$1").replace(/\.spec\.([^.]+)$/, ".$1")))) : void 0;
|
|
1061
1084
|
if (isPartial) {
|
|
1062
1085
|
this.neutralizedThresholdSnapshot = /* @__PURE__ */ new Map();
|
|
1063
1086
|
try {
|
|
@@ -1201,12 +1224,14 @@ var AgentReporter = class {
|
|
|
1201
1224
|
}
|
|
1202
1225
|
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1203
1226
|
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1227
|
+
const coverageRoot = typeof vitestConfig.root === "string" ? vitestConfig.root : void 0;
|
|
1204
1228
|
const coverageOpts = {
|
|
1205
1229
|
thresholds: opts.coverageThresholds,
|
|
1206
1230
|
includeBareZero: opts.includeBareZero,
|
|
1207
1231
|
...opts.coverageTargets ? { targets: opts.coverageTargets } : {},
|
|
1208
1232
|
...baselines ? { baselines } : {},
|
|
1209
|
-
...isPartial ? { totalFiles: totalSpecCount } : {}
|
|
1233
|
+
...isPartial ? { totalFiles: totalSpecCount } : {},
|
|
1234
|
+
...coverageRoot !== void 0 ? { root: coverageRoot } : {}
|
|
1210
1235
|
};
|
|
1211
1236
|
const coverageResult = stashedCoverage && isFirstProject ? isPartial ? yield* analyzer.processScoped(stashedCoverage, coverageOpts, testedFiles ?? []) : yield* analyzer.process(stashedCoverage, coverageOpts) : Option.none();
|
|
1212
1237
|
const coverageReport = Option.getOrUndefined(coverageResult);
|
package/utils/is-partial-run.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
//#region src/utils/is-partial-run.ts
|
|
2
2
|
/**
|
|
3
|
+
* Stable comparison key for a `configOverride.testNamePattern` value: `""`
|
|
4
|
+
* for `undefined`, otherwise `RegExp#toString()` (`/source/flags`) — two
|
|
5
|
+
* distinct `RegExp` objects constructed from the same pattern compare equal.
|
|
6
|
+
*/
|
|
7
|
+
function testNamePatternKey(pattern) {
|
|
8
|
+
return pattern === void 0 ? "" : pattern.toString();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pure decision function: does the `{ cli, initial, current }` snapshot
|
|
12
|
+
* indicate a per-run test-name filter (as opposed to the project's own
|
|
13
|
+
* `vitest.config.ts`-declared `testNamePattern`)?
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* - When `initial` and `current` differ (by stable key, not identity): the
|
|
17
|
+
* run is partial iff `current` is truthy — a watch-mode filter was just
|
|
18
|
+
* applied (partial) or just cleared (full).
|
|
19
|
+
* - When `initial` and `current` are the same: the run is partial iff `cli`
|
|
20
|
+
* is truthy — a CLI `-t foo` (partial), vs. `-t ""`/unset or a
|
|
21
|
+
* config-file-only pattern where `cli` is `undefined` (full either way).
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
function hasTestNameFilter(input) {
|
|
26
|
+
if (input === void 0) return false;
|
|
27
|
+
const { cli, initial, current } = input;
|
|
28
|
+
if (testNamePatternKey(initial) !== testNamePatternKey(current)) return current !== void 0;
|
|
29
|
+
return cli !== void 0 && cli !== "";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pure predicate: does `cliFilters` carry at least one non-empty CLI/
|
|
33
|
+
* programmatic scope filter?
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Each field is checked independently so a single unset/empty field never
|
|
37
|
+
* masks another that IS set:
|
|
38
|
+
* - `project` — non-empty array, or non-empty string.
|
|
39
|
+
* - `tagsFilter` — non-empty array.
|
|
40
|
+
* - `changed` — `true`, or a non-empty ref string.
|
|
41
|
+
* - `related` — non-empty array, or non-empty string.
|
|
42
|
+
* - `shard` — any non-empty string.
|
|
43
|
+
*
|
|
44
|
+
* `testNamePattern` is NOT part of {@link CliScopeFilters} — it needs the
|
|
45
|
+
* snapshot-diff rule in {@link hasTestNameFilter}, which {@link isPartialRun}
|
|
46
|
+
* applies separately.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
function hasCliScopeFilter(cliFilters) {
|
|
51
|
+
if (cliFilters === void 0) return false;
|
|
52
|
+
const { project, tagsFilter, changed, related, shard } = cliFilters;
|
|
53
|
+
if (project !== void 0 && project.length > 0) return true;
|
|
54
|
+
if (tagsFilter !== void 0 && tagsFilter.length > 0) return true;
|
|
55
|
+
if (changed !== void 0 && changed !== false) return true;
|
|
56
|
+
if (related !== void 0 && related.length > 0) return true;
|
|
57
|
+
if (shard !== void 0 && shard.length > 0) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
3
61
|
* Pure decision function: was this Vitest run scoped to a subset of the
|
|
4
62
|
* project's test files?
|
|
5
63
|
*
|
|
@@ -7,7 +65,14 @@
|
|
|
7
65
|
* A run is partial when any of the following holds:
|
|
8
66
|
* - Vitest's `filenamePattern` was set (a non-empty array) for this run.
|
|
9
67
|
* - Fewer specifications started than exist in total for the same project set.
|
|
10
|
-
* -
|
|
68
|
+
* - `AgentReporter`'s construction-time `projectFilter` option was set (not
|
|
69
|
+
* the CLI `--project` flag — see {@link IsPartialRunInput.projectFilter}).
|
|
70
|
+
* - Any recognized CLI/programmatic scope filter is set on `cliFilters`:
|
|
71
|
+
* `--project`, `--tags-filter`, `--changed`, `--related`, or `--shard`
|
|
72
|
+
* (issue #401).
|
|
73
|
+
* - {@link hasTestNameFilter} finds a per-run test-name filter (`-t`, or a
|
|
74
|
+
* watch-mode `t` change) — NOT a permanent `vitest.config.ts`
|
|
75
|
+
* `testNamePattern`, which is the project's own scope and stays full.
|
|
11
76
|
*
|
|
12
77
|
* Any of these makes coverage's whole-project denominator meaningless for
|
|
13
78
|
* threshold enforcement (issue #160).
|
|
@@ -15,12 +80,14 @@
|
|
|
15
80
|
* @public
|
|
16
81
|
*/
|
|
17
82
|
function isPartialRun(input) {
|
|
18
|
-
const { filenamePattern, startedSpecCount, totalSpecCount, projectFilter } = input;
|
|
83
|
+
const { filenamePattern, startedSpecCount, totalSpecCount, projectFilter, cliFilters, testNamePattern } = input;
|
|
19
84
|
if (filenamePattern !== void 0 && filenamePattern.length > 0) return true;
|
|
20
85
|
if (startedSpecCount < totalSpecCount) return true;
|
|
21
86
|
if (projectFilter !== void 0) return true;
|
|
87
|
+
if (hasCliScopeFilter(cliFilters)) return true;
|
|
88
|
+
if (hasTestNameFilter(testNamePattern)) return true;
|
|
22
89
|
return false;
|
|
23
90
|
}
|
|
24
91
|
|
|
25
92
|
//#endregion
|
|
26
|
-
export { isPartialRun };
|
|
93
|
+
export { hasCliScopeFilter, hasTestNameFilter, isPartialRun };
|