@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 +18 -2
- package/layers/CoverageAnalyzerLive.js +108 -1
- package/package.json +11 -11
- package/plugin.js +7 -21
- package/reporter.js +49 -19
- package/utils/build-reporter-kit.js +0 -1
- package/utils/stray-output-capture.js +177 -0
- package/version.js +1 -1
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,
|
|
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.
|
|
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.
|
|
47
|
-
"@effected/cli": "^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.
|
|
52
|
-
"@vitest-agent/cli": "4.0.
|
|
53
|
-
"@vitest-agent/engine": "0.
|
|
54
|
-
"@vitest-agent/mcp": "5.
|
|
55
|
-
"@vitest-agent/reporter": "
|
|
56
|
-
"@vitest-agent/sdk": "
|
|
57
|
-
"effect": "^4.0.
|
|
58
|
-
"magic-string": "^1.4.
|
|
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, "|
|
|
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,
|
|
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,
|
|
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
|
};
|
|
@@ -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 };
|