@vitest-agent/plugin 5.2.2 → 5.3.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.
@@ -17,6 +17,59 @@ function isBelowMetricThresholds(stats, thresholds) {
17
17
  if (thresholds.statements !== void 0 && stats.statements < thresholds.statements) return true;
18
18
  return false;
19
19
  }
20
+ const METRICS = [
21
+ "statements",
22
+ "branches",
23
+ "functions",
24
+ "lines"
25
+ ];
26
+ /**
27
+ * Istanbul's own percentage: `covered / total` floored to two decimals, and
28
+ * 100 for an empty (`total === 0`) metric. Mirrors `istanbul-lib-coverage`'s
29
+ * `percent()` so an aggregate computed here equals what Vitest compares.
30
+ */
31
+ function istanbulPercent(covered, total) {
32
+ return total > 0 ? Math.floor(1e4 * covered / total) / 100 : 100;
33
+ }
34
+ /**
35
+ * Aggregate a glob's matched files the way Vitest builds a glob coverage map:
36
+ * sum `covered` and `total` per metric across the files' summaries, then take
37
+ * the percentage of the sums (NOT a mean of per-file percentages). Returns
38
+ * `undefined` when any file's summary lacks counts, since no honest aggregate
39
+ * exists then.
40
+ */
41
+ function aggregateSummaries(summaries) {
42
+ const sums = {
43
+ statements: {
44
+ covered: 0,
45
+ total: 0
46
+ },
47
+ branches: {
48
+ covered: 0,
49
+ total: 0
50
+ },
51
+ functions: {
52
+ covered: 0,
53
+ total: 0
54
+ },
55
+ lines: {
56
+ covered: 0,
57
+ total: 0
58
+ }
59
+ };
60
+ for (const s of summaries) for (const m of METRICS) {
61
+ const { covered, total } = s[m];
62
+ if (typeof covered !== "number" || typeof total !== "number") return void 0;
63
+ sums[m].covered += covered;
64
+ sums[m].total += total;
65
+ }
66
+ return {
67
+ statements: istanbulPercent(sums.statements.covered, sums.statements.total),
68
+ branches: istanbulPercent(sums.branches.covered, sums.branches.total),
69
+ functions: istanbulPercent(sums.functions.covered, sums.functions.total),
70
+ lines: istanbulPercent(sums.lines.covered, sums.lines.total)
71
+ };
72
+ }
20
73
  /**
21
74
  * Runtime duck-type check for istanbul CoverageMap.
22
75
  */
@@ -86,6 +139,53 @@ function resolveEffectivePerFileThresholds(filePath, resolved) {
86
139
  return setting;
87
140
  }
88
141
  /**
142
+ * Find the threshold globs whose aggregate coverage is below their own metric
143
+ * numbers (issue #391), matching Vitest 5: a glob's coverage map holds every
144
+ * file in the run that matches it (a file may belong to several globs), the
145
+ * aggregate is `sum(covered) / sum(total)` per metric, and it is enforced in
146
+ * addition to any object `perFile`. A glob with `perFile: true` is checked per
147
+ * file only, so it has no aggregate to fail.
148
+ *
149
+ * Choices (document before changing):
150
+ * - Bare-zero files COUNT toward the aggregate, regardless of
151
+ * `includeBareZero`. That option only decides whether such files are listed
152
+ * per file; Vitest's aggregate includes them, and dropping them would hide
153
+ * exactly the shortfall they cause.
154
+ * - Scoped runs never produce shortfalls (the caller skips this): a partial
155
+ * coverage map says nothing about a glob's full aggregate, mirroring how
156
+ * scoped runs never flag threshold violations.
157
+ * - Only non-negative numbers are compared (minimum percentages). Vitest's
158
+ * negative "max uncovered count" form is not evaluated here.
159
+ * - A glob that matches no file, or whose matched summaries lack
160
+ * covered/total counts, yields nothing.
161
+ */
162
+ function computeGlobShortfalls(files, resolved) {
163
+ const shortfalls = [];
164
+ for (const [pattern, metrics] of resolved.patterns ?? []) {
165
+ if (metrics.perFile === true) continue;
166
+ const thresholds = {
167
+ ...metrics.lines !== void 0 ? { lines: metrics.lines } : {},
168
+ ...metrics.functions !== void 0 ? { functions: metrics.functions } : {},
169
+ ...metrics.branches !== void 0 ? { branches: metrics.branches } : {},
170
+ ...metrics.statements !== void 0 ? { statements: metrics.statements } : {}
171
+ };
172
+ if (METRICS.every((m) => thresholds[m] === void 0)) continue;
173
+ const matched = files.filter((f) => matchGlob(f.matchPath, pattern));
174
+ if (matched.length === 0) continue;
175
+ const aggregate = aggregateSummaries(matched.map((f) => f.summary));
176
+ if (aggregate === void 0) continue;
177
+ if (METRICS.some((m) => {
178
+ const min = thresholds[m];
179
+ return min !== void 0 && min >= 0 && aggregate[m] < min;
180
+ })) shortfalls.push({
181
+ pattern,
182
+ summary: aggregate,
183
+ thresholds
184
+ });
185
+ }
186
+ return shortfalls;
187
+ }
188
+ /**
89
189
  * Internal coverage processing logic. Shared by both `process` and `processScoped`.
90
190
  *
91
191
  * @param coverageMap - The value received by `onCoverage`; duck-typed at runtime
@@ -110,10 +210,15 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
110
210
  const belowTarget = [];
111
211
  const { root } = options;
112
212
  const matchKey = (filePath) => toPosixPath(root === void 0 ? filePath : relative(root, filePath));
213
+ const mapSummaries = [];
113
214
  for (const filePath of coverageMap.files()) {
114
215
  const matchPath = matchKey(filePath);
115
216
  const fileCoverage = coverageMap.fileCoverageFor(filePath);
116
217
  const fileSummary = fileCoverage.toSummary();
218
+ mapSummaries.push({
219
+ matchPath,
220
+ summary: fileSummary
221
+ });
117
222
  const fileStats = {
118
223
  statements: fileSummary.statements.pct,
119
224
  branches: fileSummary.branches.pct,
@@ -134,7 +239,7 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
134
239
  continue;
135
240
  }
136
241
  if (options.targets) {
137
- if (isBelowMetricThresholds(fileStats, resolveEffectiveThresholds(matchPath, options.targets))) {
242
+ if (isBelowMetricThresholds(fileStats, resolveEffectivePerFileThresholds(matchPath, options.targets) ?? resolveEffectiveThresholds(matchPath, options.targets))) {
138
243
  const uncoveredLines = compressLines(fileCoverage.getUncoveredLines());
139
244
  belowTarget.push({
140
245
  file: filePath,
@@ -144,6 +249,7 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
144
249
  }
145
250
  }
146
251
  }
252
+ const globShortfalls = scoped ? [] : computeGlobShortfalls(mapSummaries, options.thresholds);
147
253
  lowCoverage.sort((a, b) => a.summary.lines - b.summary.lines);
148
254
  belowTarget.sort((a, b) => a.summary.lines - b.summary.lines);
149
255
  return {
@@ -165,6 +271,7 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
165
271
  ...scoped && options.totalFiles !== void 0 ? { totalFiles: options.totalFiles } : {},
166
272
  lowCoverage,
167
273
  lowCoverageFiles: lowCoverage.map((f) => f.file),
274
+ ...globShortfalls.length > 0 ? { globShortfalls } : {},
168
275
  ...options.targets ? {
169
276
  belowTarget,
170
277
  belowTargetFiles: belowTarget.map((f) => f.file)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/plugin",
3
- "version": "5.2.2",
3
+ "version": "5.3.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": [
@@ -49,11 +49,11 @@
49
49
  "@effected/glob": "^0.10.0",
50
50
  "@effected/walker": "^0.15.0",
51
51
  "@effected/workspaces": "^0.31.0",
52
- "@vitest-agent/cli": "4.0.2",
53
- "@vitest-agent/engine": "0.6.0",
54
- "@vitest-agent/mcp": "5.1.8",
55
- "@vitest-agent/reporter": "4.1.0",
56
- "@vitest-agent/sdk": "6.0.0",
52
+ "@vitest-agent/cli": "4.0.3",
53
+ "@vitest-agent/engine": "0.6.1",
54
+ "@vitest-agent/mcp": "5.1.9",
55
+ "@vitest-agent/reporter": "4.1.1",
56
+ "@vitest-agent/sdk": "6.1.0",
57
57
  "effect": "^4.0.0",
58
58
  "magic-string": "^1.4.2"
59
59
  },
package/reporter.js CHANGED
@@ -1287,7 +1287,8 @@ var AgentReporter = class {
1287
1287
  })),
1288
1288
  ...coverageReport.scoped ? { scoped: coverageReport.scoped } : {},
1289
1289
  ...coverageReport.scopedFiles !== void 0 ? { scopedFiles: coverageReport.scopedFiles.length } : {},
1290
- ...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {}
1290
+ ...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {},
1291
+ ...coverageReport.globShortfalls !== void 0 ? { globShortfalls: coverageReport.globShortfalls } : {}
1291
1292
  });
1292
1293
  if (!coverageReport.scoped) for (const metric of [
1293
1294
  "lines",
package/version.js CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_PLUGIN_VERSION = "5.2.2";
11
+ const CURRENT_PLUGIN_VERSION = "5.3.0";
12
12
 
13
13
  //#endregion
14
14
  export { CURRENT_PLUGIN_VERSION };