@vitest-agent/plugin 4.0.2 → 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 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 4.x
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
- * Match a file path against a glob pattern using basic matching.
27
- * Supports `*` (any segment chars) and `**` (any path segments).
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
- const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, "[^/]");
31
- return new RegExp(`^${regexStr}$`).test(filePath);
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(filePath, options.thresholds) ?? resolveEffectiveThresholds(filePath, options.thresholds));
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(filePath, options.targets))) {
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.2",
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,15 +44,15 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@effect/platform-node": "4.0.0-rc.115",
47
- "@effect/sql-sqlite-node": "4.0.0-rc.115",
47
+ "@effected/glob": "^0.6.0",
48
48
  "@effected/workspaces": "^0.22.0",
49
- "@vitest-agent/cli": "3.0.2",
50
- "@vitest-agent/engine": "0.1.2",
51
- "@vitest-agent/mcp": "4.0.2",
52
- "@vitest-agent/reporter": "3.0.5",
53
- "@vitest-agent/sdk": "5.0.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
- "magic-string": "^1.3.1"
55
+ "magic-string": "^1.4.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@vitest/coverage-istanbul": "^5.0.0",
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
- const accepted = executor === "human" ? HumanConsoleMode.literals : executor === "agent" ? AgentConsoleMode.literals : CiConsoleMode.literals;
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.2";
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
- const consoleMode = resolveConsoleMode(options, executor, env);
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) process.stderr.write(`[vitest-agent:plugin] warning ${w.code}: ${w.message}` + (w.remediation ? `\n ${w.remediation}` : "") + "\n");
238
- for (const i of validation.info) process.stderr.write(`[vitest-agent:plugin] info ${i.code}: ${i.message}\n`);
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 4.x
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.relativeModuleId.replace(/\.test\.([^.]+)$/, ".$1").replace(/\.spec\.([^.]+)$/, ".$1")))) : void 0;
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);
@@ -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
- * - An explicit `--project` filter was supplied.
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 };