@vitest-agent/plugin 5.2.2 → 5.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
@@ -1,6 +1,6 @@
1
1
  import "@effected/env";
2
2
  import { EnvironmentDetector, PlatformLiveError, PlatformOptions, PlatformServices, SettingsInput, StackFrameInput } from "@vitest-agent/engine";
3
- import { AgentPluginOptions, AgentReporterOptions, ConsoleMode, CoverageBaselines, CoverageInput, CoverageLevel, CoverageLevelName, CoverageLevelName as CoverageLevelName$1, CoverageReport, OutputFormat, ResolvedReporterConfig, ResolvedThresholds, RunEvent, Transport, VitestAgentReporterFactory, resolveCoverageInput, validateCoverageConfig } from "@vitest-agent/sdk";
3
+ import { AgentPluginOptions, AgentReporterOptions, ConsoleMode, CoverageBaselines, CoverageInput, CoverageLevel, CoverageLevelName, CoverageLevelName as CoverageLevelName$1, CoverageReport, ResolvedReporterConfig, ResolvedThresholds, RunEvent, Transport, VitestAgentReporterFactory, resolveCoverageInput, validateCoverageConfig } from "@vitest-agent/sdk";
4
4
  import { Context, Effect, Layer, Option } from "effect";
5
5
  import { TestProjectInlineConfiguration, TestTagDefinition } from "vitest/config";
6
6
  import { ResolvedConfig, VitestPluginContext } from "vitest/node";
@@ -561,7 +561,6 @@ interface AgentReporterConstructorOptions extends AgentReporterOptions {
561
561
  */
562
562
  coverageMode?: "full" | "ui-only";
563
563
  consoleMode?: ConsoleMode;
564
- format?: OutputFormat;
565
564
  mcp?: boolean;
566
565
  githubActions?: boolean;
567
566
  /**
@@ -699,6 +698,14 @@ export declare class AgentReporter {
699
698
  * @internal
700
699
  */
701
700
  private closing;
701
+ /**
702
+ * The stray-output capture the plugin installed on this Vitest
703
+ * instance's Logger (`configureVitest`, only when the plugin owns the
704
+ * console), found at `onInit`. Undefined otherwise.
705
+ *
706
+ * @internal
707
+ */
708
+ private strayCapture;
702
709
  /**
703
710
  * Whether the user supplied a custom `reporter` factory. A custom
704
711
  * reporter may subscribe to the run-event channel in any console mode,
@@ -795,6 +802,15 @@ export declare class AgentReporter {
795
802
  * @internal
796
803
  */
797
804
  private closeReporters;
805
+ /**
806
+ * Find the stray-output capture on `vitest.logger` and, when a reporter
807
+ * resolved at `onInit` prints stray lines (a live view), route whole
808
+ * lines to it so they print above its drawing. The first reporter that
809
+ * takes a line wins.
810
+ *
811
+ * @internal
812
+ */
813
+ private routeStrayOutput;
798
814
  /**
799
815
  * Resolve the reporter factory at run start.
800
816
  *
@@ -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.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": [
@@ -43,19 +43,19 @@
43
43
  "vitest-agent-mcp": "bin/vitest-agent-mcp.js"
44
44
  },
45
45
  "dependencies": {
46
- "@effect/platform-node": "^4.0.0",
47
- "@effected/cli": "^0.12.0",
46
+ "@effect/platform-node": "^4.0.2",
47
+ "@effected/cli": "^0.15.0",
48
48
  "@effected/env": "^0.1.0",
49
49
  "@effected/glob": "^0.10.0",
50
50
  "@effected/walker": "^0.15.0",
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",
57
- "effect": "^4.0.0",
58
- "magic-string": "^1.4.2"
51
+ "@effected/workspaces": "^0.32.0",
52
+ "@vitest-agent/cli": "4.0.4",
53
+ "@vitest-agent/engine": "1.0.0",
54
+ "@vitest-agent/mcp": "5.2.0",
55
+ "@vitest-agent/reporter": "5.0.0",
56
+ "@vitest-agent/sdk": "7.0.0",
57
+ "effect": "^4.0.2",
58
+ "magic-string": "^1.4.3"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@vitest/coverage-istanbul": "^5.0.0",
package/plugin.js CHANGED
@@ -3,6 +3,7 @@ import { resolveThresholds } from "./utils/resolve-thresholds.js";
3
3
  import { ConfigValidationLive } from "./layers/ConfigValidationLive.js";
4
4
  import { ConfigurationError } from "./utils/configuration-error.js";
5
5
  import { assertFlatScope } from "./utils/report-writer.js";
6
+ import { installStrayOutputCapture } from "./utils/stray-output-capture.js";
6
7
  import { AgentReporter } from "./reporter.js";
7
8
  import { buildModuleInfo } from "./utils/build-module-info.js";
8
9
  import { DefaultDiscoverStrategy } from "./utils/discover-strategy.js";
@@ -101,24 +102,6 @@ function ownsStdout(mode) {
101
102
  return mode !== "passthrough";
102
103
  }
103
104
  /**
104
- * Map the resolved {@link ConsoleMode} to the legacy {@link OutputFormat}
105
- * the existing reporter factories switch on. The new event-sourced renderer
106
- * does not consult this — it dispatches directly on `kit.config.consoleMode`
107
- * — but the bundled markdown/terminal/silent/ci-annotations reporters still
108
- * need a format value to pick which formatter to invoke.
109
- *
110
- * @internal
111
- */
112
- function resolveFormat(mode) {
113
- switch (mode) {
114
- case "stream":
115
- case "agent": return "terminal";
116
- case "ci-annotations": return "ci-annotations";
117
- case "silent": return "silent";
118
- case "passthrough": return "vitest-bypass";
119
- }
120
- }
121
- /**
122
105
  * Set to the Vitest object reference once we've pushed an aggregating
123
106
  * reporter for that Vitest instance. The flag is module-scoped (rather
124
107
  * than closure-scoped on the plugin) because Vitest can construct the
@@ -253,9 +236,8 @@ function AgentPlugin(options = {}, _layer) {
253
236
  process.stderr.write(line);
254
237
  };
255
238
  const consoleMode = resolveConsoleMode(options, executor, env, reportOnce);
256
- const format = resolveFormat(consoleMode);
257
239
  const mcp = executor === "agent";
258
- log("env:", env, "| executor:", executor, "| consoleMode:", consoleMode, "| format:", format, "| mcp (auto):", mcp);
240
+ log("env:", env, "| executor:", executor, "| consoleMode:", consoleMode, "| mcp (auto):", mcp);
259
241
  if (ownsStdout(consoleMode)) {
260
242
  log("stripping console reporters (consoleMode owns stdout)");
261
243
  const stripped = stripConsoleReporters(vitest.config.reporters);
@@ -265,6 +247,11 @@ function AgentPlugin(options = {}, _layer) {
265
247
  log("suppressing native coverage text reporter");
266
248
  coverageCfg.reporter = [];
267
249
  }
250
+ const logger = vitest.logger;
251
+ if (logger?.outputStream !== void 0 && logger.errorStream !== void 0) installStrayOutputCapture(logger, {
252
+ stdout: process.stdout,
253
+ stderr: process.stderr
254
+ });
268
255
  }
269
256
  if (env === "ci-github" && consoleMode !== "silent") {
270
257
  log("normalizing the github-actions reporter entry");
@@ -326,7 +313,6 @@ function AgentPlugin(options = {}, _layer) {
326
313
  ...coverageThresholds !== void 0 ? { coverageThresholds } : {},
327
314
  ...coverageTargets !== void 0 ? { coverageTargets } : {},
328
315
  coverageMode,
329
- format,
330
316
  consoleMode,
331
317
  mcp,
332
318
  githubActions,
package/reporter.js CHANGED
@@ -9,10 +9,11 @@ import { processFailure } from "./utils/process-failure.js";
9
9
  import { assertReportCapable, createReportWriter } from "./utils/report-writer.js";
10
10
  import { resolveNoColor } from "./utils/resolve-no-color.js";
11
11
  import { routeRenderedOutput } from "./utils/route-rendered-output.js";
12
+ import { strayOutputCaptureOf } from "./utils/stray-output-capture.js";
12
13
  import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
13
14
  import { mkdirSync, statSync } from "node:fs";
14
15
  import { dirname } from "node:path";
15
- import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, FormatSelector, HistoryTracker, OutputPipelineLive, PathResolutionLive, ensureMigrated, historyKey, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/engine";
16
+ import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, HistoryTracker, OutputPipelineLive, PathResolutionLive, ensureMigrated, historyKey, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/engine";
16
17
  import { buildAgentReport, coerceErrorField, computeTrend, formatFatalError, isTimeoutError, probeHostMetadataFromEnv } from "@vitest-agent/sdk";
17
18
  import { Effect, Option, PubSub } from "effect";
18
19
  import { randomUUID } from "node:crypto";
@@ -41,6 +42,14 @@ import { DefaultVitestAgentReporter } from "@vitest-agent/reporter";
41
42
  */
42
43
  const dbPathCache = /* @__PURE__ */ new Map();
43
44
  /**
45
+ * The run's stray output on every project report: it arrives on one stream
46
+ * shared by every project, so no report owns it more than another.
47
+ */
48
+ const withStrayOutput = (reports, strayOutput) => strayOutput === void 0 ? reports : reports.map((report) => ({
49
+ ...report,
50
+ strayOutput
51
+ }));
52
+ /**
44
53
  * Safely read a raw Vitest error object's `stacks` array. The property
45
54
  * access itself can throw on hostile shapes (live getters — the same
46
55
  * ConfigError class of failure `coerceErrorField` guards); frames are
@@ -347,6 +356,14 @@ var AgentReporter = class {
347
356
  */
348
357
  closing;
349
358
  /**
359
+ * The stray-output capture the plugin installed on this Vitest
360
+ * instance's Logger (`configureVitest`, only when the plugin owns the
361
+ * console), found at `onInit`. Undefined otherwise.
362
+ *
363
+ * @internal
364
+ */
365
+ strayCapture;
366
+ /**
350
367
  * Whether the user supplied a custom `reporter` factory. A custom
351
368
  * reporter may subscribe to the run-event channel in any console mode,
352
369
  * so the streaming hooks publish events whenever this is true.
@@ -423,7 +440,6 @@ var AgentReporter = class {
423
440
  const resolvedTargets = rawTargets ? "global" in rawTargets ? rawTargets : resolveThresholds(rawTargets) : void 0;
424
441
  const consoleMode = options.consoleMode ?? "passthrough";
425
442
  const consoleOutput = consoleMode === "silent" ? "silent" : "failures";
426
- const derivedFormat = options.format ?? (consoleMode === "silent" ? "silent" : consoleMode === "stream" || consoleMode === "agent" ? "terminal" : consoleMode === "ci-annotations" ? "ci-annotations" : void 0);
427
443
  const githubActions = options.githubActions ?? false;
428
444
  const base = {
429
445
  ...options.cacheDir !== void 0 ? { cacheDir: options.cacheDir } : {},
@@ -437,7 +453,6 @@ var AgentReporter = class {
437
453
  githubSummary: githubActions,
438
454
  githubSummaryFile: void 0,
439
455
  ...options.reportScope !== void 0 ? { reportScope: options.reportScope } : {},
440
- ...derivedFormat !== void 0 ? { format: derivedFormat } : {},
441
456
  consoleMode,
442
457
  ...options.mcp !== void 0 ? { mcp: options.mcp } : {},
443
458
  ...options.projectFilter !== void 0 ? { projectFilter: options.projectFilter } : {},
@@ -470,7 +485,6 @@ var AgentReporter = class {
470
485
  includeBareZero: opts.includeBareZero,
471
486
  githubActions: opts.githubActions,
472
487
  githubSummary: opts.githubSummary,
473
- format: opts.format ?? "vitest-bypass",
474
488
  detail: opts.detail ?? "standard",
475
489
  noColor: false,
476
490
  coverageMode: opts.coverageMode,
@@ -500,6 +514,7 @@ var AgentReporter = class {
500
514
  await this.ensureDbPath();
501
515
  } catch {}
502
516
  await this.initReporters();
517
+ this.routeStrayOutput(vitest);
503
518
  const onClose = vitest?.onClose;
504
519
  if (typeof onClose === "function") onClose.call(vitest, () => this.closeReporters());
505
520
  }
@@ -516,6 +531,7 @@ var AgentReporter = class {
516
531
  */
517
532
  closeReporters() {
518
533
  this.closing ??= (async () => {
534
+ this.strayCapture?.route(void 0);
519
535
  await Promise.all((this.reporters ?? []).map(async (reporter) => {
520
536
  try {
521
537
  await reporter.close?.();
@@ -532,6 +548,25 @@ var AgentReporter = class {
532
548
  return this.closing;
533
549
  }
534
550
  /**
551
+ * Find the stray-output capture on `vitest.logger` and, when a reporter
552
+ * resolved at `onInit` prints stray lines (a live view), route whole
553
+ * lines to it so they print above its drawing. The first reporter that
554
+ * takes a line wins.
555
+ *
556
+ * @internal
557
+ */
558
+ routeStrayOutput(vitest) {
559
+ this.strayCapture = strayOutputCaptureOf(vitest?.logger);
560
+ const printers = (this.reporters ?? []).flatMap((reporter) => reporter.printStrayLine !== void 0 ? [reporter.printStrayLine] : []);
561
+ if (this.strayCapture === void 0 || printers.length === 0) return;
562
+ this.strayCapture.route((stream, line) => {
563
+ for (const print of printers) try {
564
+ if (print(stream, line)) return true;
565
+ } catch {}
566
+ return false;
567
+ });
568
+ }
569
+ /**
535
570
  * Resolve the reporter factory at run start.
536
571
  *
537
572
  * The factory is invoked here — before any streaming hook fires — so a
@@ -553,14 +588,12 @@ var AgentReporter = class {
553
588
  const initProgram = Effect.gen(function* () {
554
589
  const detector = yield* EnvironmentDetector;
555
590
  const executorResolver = yield* ExecutorResolver;
556
- const formatSelector = yield* FormatSelector;
557
591
  const detailResolver = yield* DetailResolver;
558
592
  const env = yield* detector.detect();
559
593
  const executor = yield* executorResolver.resolve(env);
560
594
  return {
561
595
  env,
562
596
  executor,
563
- format: yield* formatSelector.select(executor, opts.format, env),
564
597
  detail: yield* detailResolver.resolve(executor, {
565
598
  hasFailures: false,
566
599
  belowTargets: false,
@@ -569,11 +602,10 @@ var AgentReporter = class {
569
602
  noColor: yield* resolveNoColor
570
603
  };
571
604
  });
572
- const { env, executor, format, detail, noColor } = await Effect.runPromise(initProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer)));
605
+ const { env, executor, detail, noColor } = await Effect.runPromise(initProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer)));
573
606
  const kit = buildReporterKit({
574
607
  env,
575
608
  executor,
576
- format,
577
609
  detail,
578
610
  noColor,
579
611
  consoleMode: opts.consoleMode ?? "passthrough",
@@ -647,6 +679,7 @@ var AgentReporter = class {
647
679
  */
648
680
  onTestRunStart(_specifications) {
649
681
  this.startedSpecCount = _specifications.length;
682
+ this.strayCapture?.reset();
650
683
  if (this.neutralizedThresholdSnapshot.size > 0) {
651
684
  try {
652
685
  const thresholds = (this._vitest?.coverageProvider)?.options?.thresholds;
@@ -1058,6 +1091,8 @@ var AgentReporter = class {
1058
1091
  if (this.rendered) return;
1059
1092
  this.rendered = true;
1060
1093
  const errors = unhandledErrors;
1094
+ this.strayCapture?.flush();
1095
+ const strayOutput = this.strayCapture?.snapshot();
1061
1096
  if (this.wantsRunEvents() && this.currentRunId !== null) {
1062
1097
  let pass = 0;
1063
1098
  let fail = 0;
@@ -1088,7 +1123,8 @@ var AgentReporter = class {
1088
1123
  durationMs: totalDuration,
1089
1124
  // @vitest-agent/ui, which both populate `collectedModules` on their
1090
1125
  collectedModules: testModules.length,
1091
- ...errors.length > 0 && { unhandledErrors: errors }
1126
+ ...errors.length > 0 && { unhandledErrors: errors },
1127
+ ...strayOutput !== void 0 && { strayOutput }
1092
1128
  });
1093
1129
  }
1094
1130
  const modules = testModules;
@@ -1161,11 +1197,9 @@ var AgentReporter = class {
1161
1197
  const uiProgram = Effect.gen(function* () {
1162
1198
  const detector = yield* EnvironmentDetector;
1163
1199
  const executorResolver = yield* ExecutorResolver;
1164
- const formatSelector = yield* FormatSelector;
1165
1200
  const detailResolver = yield* DetailResolver;
1166
1201
  const env = yield* detector.detect();
1167
1202
  const executor = yield* executorResolver.resolve(env);
1168
- const format = yield* formatSelector.select(executor, opts.format, env);
1169
1203
  const health = {
1170
1204
  hasFailures: uiReports.some((r) => r.failedFiles.length > 0 || r.unhandledErrors.length > 0),
1171
1205
  belowTargets: false,
@@ -1176,7 +1210,6 @@ var AgentReporter = class {
1176
1210
  const kit = buildReporterKit({
1177
1211
  env,
1178
1212
  executor,
1179
- format,
1180
1213
  detail,
1181
1214
  noColor: yield* resolveNoColor,
1182
1215
  consoleMode: opts.consoleMode ?? "passthrough",
@@ -1193,7 +1226,7 @@ var AgentReporter = class {
1193
1226
  });
1194
1227
  const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
1195
1228
  const renderInput = {
1196
- reports: uiReports,
1229
+ reports: withStrayOutput(uiReports, strayOutput),
1197
1230
  classifications: /* @__PURE__ */ new Map()
1198
1231
  };
1199
1232
  const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
@@ -1287,7 +1320,8 @@ var AgentReporter = class {
1287
1320
  })),
1288
1321
  ...coverageReport.scoped ? { scoped: coverageReport.scoped } : {},
1289
1322
  ...coverageReport.scopedFiles !== void 0 ? { scopedFiles: coverageReport.scopedFiles.length } : {},
1290
- ...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {}
1323
+ ...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {},
1324
+ ...coverageReport.globShortfalls !== void 0 ? { globShortfalls: coverageReport.globShortfalls } : {}
1291
1325
  });
1292
1326
  if (!coverageReport.scoped) for (const metric of [
1293
1327
  "lines",
@@ -1705,11 +1739,9 @@ var AgentReporter = class {
1705
1739
  const { reports, classifications, trendSummary } = renderInputData;
1706
1740
  const detector = yield* EnvironmentDetector;
1707
1741
  const executorResolver = yield* ExecutorResolver;
1708
- const formatSelector = yield* FormatSelector;
1709
1742
  const detailResolver = yield* DetailResolver;
1710
1743
  const env = yield* detector.detect();
1711
1744
  const executor = yield* executorResolver.resolve(env);
1712
- const format = yield* formatSelector.select(executor, opts.format, env);
1713
1745
  const health = {
1714
1746
  hasFailures: reports.some((r) => r.failedFiles.length > 0 || r.unhandledErrors.length > 0),
1715
1747
  belowTargets: reports.some((r) => {
@@ -1721,14 +1753,12 @@ var AgentReporter = class {
1721
1753
  yield* Effect.logDebug("pipeline resolved").pipe(Effect.annotateLogs({
1722
1754
  env,
1723
1755
  executor,
1724
- format,
1725
1756
  detail
1726
1757
  }));
1727
1758
  const githubSummaryFile = process.env.GITHUB_STEP_SUMMARY;
1728
1759
  const kit = buildReporterKit({
1729
1760
  env,
1730
1761
  executor,
1731
- format,
1732
1762
  detail,
1733
1763
  noColor: yield* resolveNoColor,
1734
1764
  consoleMode: opts.consoleMode ?? "passthrough",
@@ -1745,7 +1775,7 @@ var AgentReporter = class {
1745
1775
  });
1746
1776
  const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
1747
1777
  const renderInput = {
1748
- reports,
1778
+ reports: withStrayOutput(reports, strayOutput),
1749
1779
  classifications,
1750
1780
  ...trendSummary !== void 0 && { trendSummary }
1751
1781
  };
@@ -15,7 +15,6 @@ const buildReporterKit = (input) => {
15
15
  includeBareZero: false,
16
16
  githubActions: input.githubActions,
17
17
  githubSummary,
18
- format: input.format,
19
18
  detail: input.detail,
20
19
  noColor: input.noColor,
21
20
  coverageMode: input.coverageMode,
@@ -0,0 +1,177 @@
1
+ import { STRAY_OUTPUT_SOURCE, isEscapeOnly, makeStrayOutputRecorder } from "@vitest-agent/sdk";
2
+ import { Writable } from "node:stream";
3
+ import { StringDecoder } from "node:string_decoder";
4
+
5
+ //#region src/utils/stray-output-capture.ts
6
+ /**
7
+ * Stray-output capture: a pass-through over Vitest's `Logger.outputStream` /
8
+ * `errorStream`.
9
+ *
10
+ * Why those two streams (vitest\@5.0.0, `src/node/pools/workers/`): the forks
11
+ * and vmForks pools fork each worker with `stdio: "pipe"` and pipe its stdout
12
+ * and stderr into `project.vitest.logger.outputStream` / `errorStream`,
13
+ * captured when the worker is constructed, so every raw byte a worker writes
14
+ * (a grandchild process that inherits the worker's fds included, such as an
15
+ * `execFileSync("git", …)` with default stdio) arrives on them. The threads
16
+ * and vmThreads pools pipe a worker thread's JS-level `process.stdout` /
17
+ * `stderr` the same way, but a child process spawned from a thread inherits
18
+ * the main process's real fds 1/2, so its bytes reach the terminal directly
19
+ * and nothing here can see them. Workers are constructed lazily by the pool,
20
+ * at run time, long after `configureVitest`, so installing there is early
21
+ * enough. The `Logger`'s own `console` was built from the ORIGINAL streams in
22
+ * its constructor, so Vitest's logging bypasses the wrapper; its few direct
23
+ * `outputStream.write` calls are escape-only (cursor show, clear screen),
24
+ * which pass straight through unrecorded.
25
+ *
26
+ * Every byte is recorded (bounded: counts, a few sample lines). While a
27
+ * printer is routed (the reporter's live view is up) and the stream's
28
+ * original destination is the process terminal, bytes are line-buffered and
29
+ * each whole line goes to the printer, which prints it above the live frame;
30
+ * otherwise bytes pass through to the original stream unchanged. The
31
+ * original is whatever stream object the Logger held: under MCP's
32
+ * `run_tests` that is the server's null sink, so worker output still never
33
+ * reaches the JSON-RPC stdout, and it is never routed.
34
+ */
35
+ /**
36
+ * The most text a stream holds back waiting for a newline. Past it, the held
37
+ * text is routed as a line of its own, so a child that never writes a newline
38
+ * cannot grow the buffer without bound.
39
+ *
40
+ * @internal
41
+ */
42
+ const MAX_HELD_CHARS = 8192;
43
+ const isCapture = (value) => typeof value === "object" && value !== null && "route" in value && "snapshot" in value;
44
+ /**
45
+ * The capture installed on `logger`, if any.
46
+ *
47
+ * @internal
48
+ */
49
+ const strayOutputCaptureOf = (logger) => {
50
+ if (typeof logger !== "object" || logger === null) return void 0;
51
+ const value = logger[STRAY_OUTPUT_SOURCE];
52
+ return isCapture(value) ? value : void 0;
53
+ };
54
+ const DELEGATED = [
55
+ "isTTY",
56
+ "columns",
57
+ "rows",
58
+ "getColorDepth",
59
+ "hasColors",
60
+ "getWindowSize"
61
+ ];
62
+ var StrayStream = class extends Writable {
63
+ original;
64
+ take;
65
+ constructor(original, take) {
66
+ super({ decodeStrings: false });
67
+ this.original = original;
68
+ this.take = take;
69
+ for (const key of DELEGATED) {
70
+ if (!(key in original)) continue;
71
+ Object.defineProperty(this, key, {
72
+ configurable: true,
73
+ enumerable: true,
74
+ get: () => {
75
+ const value = original[key];
76
+ return typeof value === "function" ? value.bind(original) : value;
77
+ }
78
+ });
79
+ }
80
+ }
81
+ _write(chunk, encoding, callback) {
82
+ try {
83
+ this.take(chunk, encoding);
84
+ callback();
85
+ } catch (err) {
86
+ callback(err instanceof Error ? err : new Error(String(err)));
87
+ }
88
+ }
89
+ };
90
+ /**
91
+ * Wrap `logger`'s output and error streams with the stray-output capture, once
92
+ * per Logger (a second call returns the first capture). The capture is
93
+ * published on the Logger under `STRAY_OUTPUT_SOURCE`, so MCP's `run_tests`
94
+ * (which cannot import the plugin) reads the run's stray output with the
95
+ * sdk's `readStrayOutput`.
96
+ *
97
+ * @param logger - the Vitest instance's `logger`
98
+ * @param terminal - the process's own stdout and stderr
99
+ *
100
+ * @internal
101
+ */
102
+ const installStrayOutputCapture = (logger, terminal) => {
103
+ const existing = strayOutputCaptureOf(logger);
104
+ if (existing !== void 0) return existing;
105
+ const recorder = makeStrayOutputRecorder();
106
+ const originals = {
107
+ stdout: logger.outputStream,
108
+ stderr: logger.errorStream
109
+ };
110
+ const decoders = {
111
+ stdout: new StringDecoder("utf8"),
112
+ stderr: new StringDecoder("utf8")
113
+ };
114
+ const held = {
115
+ stdout: "",
116
+ stderr: ""
117
+ };
118
+ let printer;
119
+ const flushHeld = (stream) => {
120
+ const text = held[stream];
121
+ if (text === "") return;
122
+ held[stream] = "";
123
+ originals[stream].write(text);
124
+ };
125
+ const take = (stream) => (chunk, encoding) => {
126
+ const original = originals[stream];
127
+ if (typeof chunk === "string" && isEscapeOnly(chunk)) {
128
+ original.write(chunk, encoding);
129
+ return;
130
+ }
131
+ const text = typeof chunk === "string" ? chunk : decoders[stream].write(chunk);
132
+ recorder.write(stream, text);
133
+ const route = printer !== void 0 && original === terminal[stream] ? printer : void 0;
134
+ if (route === void 0) {
135
+ original.write(chunk, encoding);
136
+ return;
137
+ }
138
+ const lines = (held[stream] + text).split("\n");
139
+ const rest = lines.pop() ?? "";
140
+ if (rest.length > 8192) {
141
+ lines.push(rest);
142
+ held[stream] = "";
143
+ } else held[stream] = rest;
144
+ for (const line of lines) if (!route(stream, line)) original.write(`${line}\n`);
145
+ };
146
+ const flushHeldThrough = (stream) => {
147
+ const text = held[stream];
148
+ if (text === "") return;
149
+ held[stream] = "";
150
+ if (printer === void 0 || !printer(stream, text)) originals[stream].write(text);
151
+ };
152
+ logger.outputStream = new StrayStream(originals.stdout, take("stdout"));
153
+ logger.errorStream = new StrayStream(originals.stderr, take("stderr"));
154
+ const capture = {
155
+ snapshot: recorder.snapshot,
156
+ reset: recorder.reset,
157
+ route: (next) => {
158
+ if (next === void 0) {
159
+ flushHeld("stdout");
160
+ flushHeld("stderr");
161
+ }
162
+ printer = next;
163
+ },
164
+ flush: () => {
165
+ flushHeldThrough("stdout");
166
+ flushHeldThrough("stderr");
167
+ }
168
+ };
169
+ Object.defineProperty(logger, STRAY_OUTPUT_SOURCE, {
170
+ value: capture,
171
+ configurable: true
172
+ });
173
+ return capture;
174
+ };
175
+
176
+ //#endregion
177
+ export { MAX_HELD_CHARS, installStrayOutputCapture, strayOutputCaptureOf };
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.4.0";
12
12
 
13
13
  //#endregion
14
14
  export { CURRENT_PLUGIN_VERSION };