@vitest-agent/plugin 1.0.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/LICENSE +21 -0
- package/README.md +50 -0
- package/index.d.ts +1216 -0
- package/index.js +31 -0
- package/layers/ConfigValidationLive.js +148 -0
- package/layers/ConfigValidationTest.js +16 -0
- package/layers/CoverageAnalyzerLive.js +138 -0
- package/layers/CoverageAnalyzerTest.js +15 -0
- package/layers/ReporterLive.js +21 -0
- package/package.json +65 -0
- package/plugin.js +329 -0
- package/reporter.js +1349 -0
- package/services/ConfigValidation.js +11 -0
- package/services/CoverageAnalyzer.js +11 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/build-module-info.js +62 -0
- package/utils/build-reporter-kit.js +49 -0
- package/utils/capture-env.js +23 -0
- package/utils/capture-settings.js +54 -0
- package/utils/classify-helpers.js +72 -0
- package/utils/discover-projects.js +68 -0
- package/utils/discover-strategy.js +158 -0
- package/utils/find-test-files.js +94 -0
- package/utils/inject-tags.js +94 -0
- package/utils/process-failure.js +89 -0
- package/utils/resolve-thresholds.js +65 -0
- package/utils/route-rendered-output.js +45 -0
- package/utils/stringify-failure-value.js +36 -0
- package/utils/strip-console-reporters.js +45 -0
- package/utils/tag.js +42 -0
- package/utils/to-posix-path.js +35 -0
package/reporter.js
ADDED
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
import { resolveThresholds } from "./utils/resolve-thresholds.js";
|
|
2
|
+
import { CoverageAnalyzer } from "./services/CoverageAnalyzer.js";
|
|
3
|
+
import { ReporterLive } from "./layers/ReporterLive.js";
|
|
4
|
+
import { buildReporterKit, normalizeReporters } from "./utils/build-reporter-kit.js";
|
|
5
|
+
import { captureEnvVars } from "./utils/capture-env.js";
|
|
6
|
+
import { captureSettings, hashSettings } from "./utils/capture-settings.js";
|
|
7
|
+
import { processFailure } from "./utils/process-failure.js";
|
|
8
|
+
import { routeRenderedOutput } from "./utils/route-rendered-output.js";
|
|
9
|
+
import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
|
|
10
|
+
import { DefaultVitestAgentReporter } from "@vitest-agent/reporter";
|
|
11
|
+
import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, FormatSelector, HistoryTracker, OutputPipelineLive, PathResolutionLive, buildAgentReport, computeTrend, ensureMigrated, formatFatalError, isTimeoutError, probeHostMetadataFromEnv, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
|
|
12
|
+
import { Effect, Option, PubSub } from "effect";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { NodeContext } from "@effect/platform-node";
|
|
17
|
+
|
|
18
|
+
//#region src/reporter.ts
|
|
19
|
+
/**
|
|
20
|
+
* Cache of in-flight `resolveDataPath` promises, keyed by `projectDir`.
|
|
21
|
+
*
|
|
22
|
+
* Multi-project Vitest configs construct one `AgentReporter` instance per
|
|
23
|
+
* project (typical: 4-5 reporters per repo). Each instance's `ensureDbPath`
|
|
24
|
+
* runs `resolveDataPath(projectDir)` under `PathResolutionLive`, which
|
|
25
|
+
* pulls in `WorkspacesLive` — a layer that eagerly scans lockfiles and
|
|
26
|
+
* walks the workspace package graph. Without this cache, that scan
|
|
27
|
+
* happens N times in serial during reporter init, adding seconds of
|
|
28
|
+
* overhead to every `vitest run`.
|
|
29
|
+
*
|
|
30
|
+
* The cache is module-local: one entry per `projectDir` (in practice,
|
|
31
|
+
* always `process.cwd()` for a single Vitest invocation), shared across
|
|
32
|
+
* all reporter instances within the same Node process. Sufficient
|
|
33
|
+
* because reporter instances are created in the main Vitest process,
|
|
34
|
+
* not in worker forks.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
const dbPathCache = /* @__PURE__ */ new Map();
|
|
39
|
+
function resolveDataPathCached(projectDir) {
|
|
40
|
+
const cached = dbPathCache.get(projectDir);
|
|
41
|
+
if (cached !== void 0) return cached;
|
|
42
|
+
const promise = Effect.runPromise(resolveDataPath(projectDir).pipe(Effect.provide(PathResolutionLive(projectDir)), Effect.provide(NodeContext.layer)));
|
|
43
|
+
dbPathCache.set(projectDir, promise);
|
|
44
|
+
promise.catch(() => void 0);
|
|
45
|
+
return promise;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute updated baselines using ratchet logic: take the max of actual vs previous,
|
|
49
|
+
* capped by targets if set.
|
|
50
|
+
*
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
function computeUpdatedBaselines(existing, actual, targets) {
|
|
54
|
+
const prev = existing?.global ?? {};
|
|
55
|
+
const cap = targets?.global ?? {};
|
|
56
|
+
const ratchet = (metric) => {
|
|
57
|
+
const actualVal = actual[metric];
|
|
58
|
+
const prevVal = prev[metric] ?? 0;
|
|
59
|
+
const targetVal = cap[metric];
|
|
60
|
+
const newVal = Math.max(actualVal, prevVal);
|
|
61
|
+
if (targetVal !== void 0 && newVal > targetVal) return targetVal;
|
|
62
|
+
return newVal;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
66
|
+
global: {
|
|
67
|
+
lines: ratchet("lines"),
|
|
68
|
+
functions: ratchet("functions"),
|
|
69
|
+
branches: ratchet("branches"),
|
|
70
|
+
statements: ratchet("statements")
|
|
71
|
+
},
|
|
72
|
+
patterns: existing?.patterns ?? []
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Vitest Reporter that produces structured output for LLM coding agents.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* `AgentReporter` implements three Vitest Reporter lifecycle hooks:
|
|
80
|
+
*
|
|
81
|
+
* - {@link AgentReporter.onInit | onInit} -- stores the Vitest instance
|
|
82
|
+
* for project enumeration (used in Phase 2 overview generation)
|
|
83
|
+
* - {@link AgentReporter.onCoverage | onCoverage} -- stashes the istanbul
|
|
84
|
+
* CoverageMap for merging into reports
|
|
85
|
+
* - {@link AgentReporter.onTestRunEnd | onTestRunEnd} -- groups test
|
|
86
|
+
* modules by project, builds reports, writes data to SQLite,
|
|
87
|
+
* updates the manifest, and emits console/GFM output
|
|
88
|
+
*
|
|
89
|
+
* The reporter handles both single-package repos and monorepos by grouping
|
|
90
|
+
* results via Vitest's native `TestProject` API. In single-project mode,
|
|
91
|
+
* results are written with project name "default".
|
|
92
|
+
*
|
|
93
|
+
* @privateRemarks
|
|
94
|
+
* The `onCoverage` hook fires **before** `onTestRunEnd` in Vitest's lifecycle.
|
|
95
|
+
* Coverage data must be stashed as instance state and merged during
|
|
96
|
+
* `onTestRunEnd`. This ordering is a Vitest design constraint, not a bug.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import { AgentReporter } from "@vitest-agent/plugin";
|
|
101
|
+
* import { defineConfig } from "vitest/config";
|
|
102
|
+
*
|
|
103
|
+
* export default defineConfig({
|
|
104
|
+
* test: {
|
|
105
|
+
* reporters: [
|
|
106
|
+
* new AgentReporter({
|
|
107
|
+
* cacheDir: ".vitest-agent",
|
|
108
|
+
* consoleOutput: "failures",
|
|
109
|
+
* coverageThresholds: { global: { lines: 80 } },
|
|
110
|
+
* }),
|
|
111
|
+
* ],
|
|
112
|
+
* },
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @see `AgentPlugin` for the convenience plugin wrapper
|
|
117
|
+
* @see {@link https://vitest.dev/api/advanced/reporters.html | Vitest Reporter API}
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
var AgentReporter = class {
|
|
121
|
+
options;
|
|
122
|
+
dbPath = null;
|
|
123
|
+
/**
|
|
124
|
+
* Set to `true` after the first `onTestRunEnd` invocation completes.
|
|
125
|
+
*
|
|
126
|
+
* Vitest can fire `onTestRunEnd` more than once per `vitest run` in
|
|
127
|
+
* some multi-project configurations (e.g., once per project group, with
|
|
128
|
+
* the same `testModules` array each time). The plugin pushes only ONE
|
|
129
|
+
* AgentReporter per run, so a fresh instance always starts with this
|
|
130
|
+
* flag `false`; the first call does the work and the flag prevents
|
|
131
|
+
* subsequent invocations from re-rendering and re-writing to the DB.
|
|
132
|
+
*/
|
|
133
|
+
rendered = false;
|
|
134
|
+
/**
|
|
135
|
+
* Stored Vitest instance from {@link AgentReporter.onInit | onInit}.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* Available for Phase 2 overview generation. Exposed as a public
|
|
139
|
+
* property (prefixed with `_`) for testing and extension purposes.
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
_vitest = null;
|
|
144
|
+
coverage = null;
|
|
145
|
+
logLevel;
|
|
146
|
+
logFile;
|
|
147
|
+
onRunEvent;
|
|
148
|
+
/**
|
|
149
|
+
* Run-event channel. The plugin publishes one `RunEvent` per
|
|
150
|
+
* Vitest streaming callback onto this `PubSub`; the reporter factory
|
|
151
|
+
* receives it on the `ReporterKit` and a live-painting reporter
|
|
152
|
+
* (the default reporter in `stream` mode) subscribes to drive its mount.
|
|
153
|
+
* The plugin no longer owns any render mount itself.
|
|
154
|
+
*
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
157
|
+
runEvents;
|
|
158
|
+
/**
|
|
159
|
+
* Reporters resolved from the factory at run start (`onInit`). Reused
|
|
160
|
+
* by `onTestRunEnd` for the `render` call. Undefined when `onInit` did
|
|
161
|
+
* not run (tests invoking `onTestRunEnd` directly) — `onTestRunEnd`
|
|
162
|
+
* then resolves the factory itself.
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
reporters;
|
|
167
|
+
/**
|
|
168
|
+
* Whether the user supplied a custom `reporter` factory. A custom
|
|
169
|
+
* reporter may subscribe to the run-event channel in any console mode,
|
|
170
|
+
* so the streaming hooks publish events whenever this is true.
|
|
171
|
+
*
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
hasCustomReporter;
|
|
175
|
+
currentRunId = null;
|
|
176
|
+
moduleStartedAt = /* @__PURE__ */ new Map();
|
|
177
|
+
/**
|
|
178
|
+
* Wall-clock start stamp (`Date.now()`) for an in-flight Vitest
|
|
179
|
+
* setup/teardown hook, keyed `modulePath::hookType::scopeName`.
|
|
180
|
+
* `onHookStart` writes it, `onHookEnd` reads and clears it to derive
|
|
181
|
+
* `HookFinished.durationMs` — `ReportedHookContext` carries no
|
|
182
|
+
* duration of its own.
|
|
183
|
+
*
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
hookStartedAt = /* @__PURE__ */ new Map();
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
this.logLevel = resolveLogLevel();
|
|
189
|
+
this.logFile = resolveLogFile();
|
|
190
|
+
this.onRunEvent = options.onRunEvent;
|
|
191
|
+
this.hasCustomReporter = options.reporter !== void 0;
|
|
192
|
+
this.runEvents = Effect.runSync(PubSub.unbounded());
|
|
193
|
+
const rawThresholds = options.coverageThresholds;
|
|
194
|
+
const resolvedThresholds = rawThresholds && "global" in rawThresholds ? rawThresholds : resolveThresholds(rawThresholds);
|
|
195
|
+
const rawTargets = options.coverageTargets;
|
|
196
|
+
const resolvedTargets = rawTargets ? "global" in rawTargets ? rawTargets : resolveThresholds(rawTargets) : void 0;
|
|
197
|
+
const consoleMode = options.consoleMode ?? "passthrough";
|
|
198
|
+
const consoleOutput = consoleMode === "silent" ? "silent" : "failures";
|
|
199
|
+
const derivedFormat = options.format ?? (consoleMode === "silent" ? "silent" : consoleMode === "stream" || consoleMode === "agent" ? "terminal" : consoleMode === "ci-annotations" ? "ci-annotations" : void 0);
|
|
200
|
+
const githubActions = options.githubActions ?? false;
|
|
201
|
+
const base = {
|
|
202
|
+
...options.cacheDir !== void 0 ? { cacheDir: options.cacheDir } : {},
|
|
203
|
+
consoleOutput,
|
|
204
|
+
omitPassingTests: true,
|
|
205
|
+
coverageThresholds: resolvedThresholds,
|
|
206
|
+
autoUpdate: true,
|
|
207
|
+
coverageConsoleLimit: 10,
|
|
208
|
+
includeBareZero: false,
|
|
209
|
+
githubActions,
|
|
210
|
+
githubSummary: githubActions,
|
|
211
|
+
githubSummaryFile: void 0,
|
|
212
|
+
...derivedFormat !== void 0 ? { format: derivedFormat } : {},
|
|
213
|
+
consoleMode,
|
|
214
|
+
...options.mcp !== void 0 ? { mcp: options.mcp } : {},
|
|
215
|
+
...options.projectFilter !== void 0 ? { projectFilter: options.projectFilter } : {},
|
|
216
|
+
reporter: options.reporter ?? DefaultVitestAgentReporter,
|
|
217
|
+
coverageMode: options.coverageMode ?? "full",
|
|
218
|
+
transport: options.transport ?? { kind: "local" },
|
|
219
|
+
...options.passWithNoTests !== void 0 ? { passWithNoTests: options.passWithNoTests } : {}
|
|
220
|
+
};
|
|
221
|
+
this.options = resolvedTargets ? {
|
|
222
|
+
...base,
|
|
223
|
+
coverageTargets: resolvedTargets
|
|
224
|
+
} : base;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The resolved reporter config built at construction time. Exposed for
|
|
228
|
+
* test inspection and Phase 5 short-circuit logic.
|
|
229
|
+
*
|
|
230
|
+
* This getter is @internal — do not reference in user-facing docs.
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
get resolvedConfig() {
|
|
234
|
+
const opts = this.options;
|
|
235
|
+
return {
|
|
236
|
+
executor: "ci",
|
|
237
|
+
consoleMode: opts.consoleMode,
|
|
238
|
+
mcp: opts.mcp ?? false,
|
|
239
|
+
consoleOutput: opts.consoleOutput,
|
|
240
|
+
omitPassingTests: opts.omitPassingTests,
|
|
241
|
+
coverageConsoleLimit: opts.coverageConsoleLimit,
|
|
242
|
+
includeBareZero: opts.includeBareZero,
|
|
243
|
+
githubActions: opts.githubActions,
|
|
244
|
+
githubSummary: opts.githubSummary,
|
|
245
|
+
format: opts.format ?? "vitest-bypass",
|
|
246
|
+
detail: opts.detail ?? "standard",
|
|
247
|
+
noColor: false,
|
|
248
|
+
coverageMode: opts.coverageMode,
|
|
249
|
+
transport: opts.transport,
|
|
250
|
+
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
251
|
+
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
252
|
+
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Store the Vitest instance for project enumeration.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* Called once at the start of the test run. The instance is stored
|
|
260
|
+
* for Phase 2 overview generation via `vitest.projects`.
|
|
261
|
+
*
|
|
262
|
+
* @param vitest - The Vitest instance
|
|
263
|
+
*/
|
|
264
|
+
async onInit(vitest) {
|
|
265
|
+
this._vitest = vitest;
|
|
266
|
+
await this.ensureDbPath();
|
|
267
|
+
await this.initReporters();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resolve the reporter factory at run start.
|
|
271
|
+
*
|
|
272
|
+
* The factory is invoked here — before any streaming hook fires — so a
|
|
273
|
+
* reporter that paints live (the default reporter's Ink mount in `stream`
|
|
274
|
+
* mode) can subscribe to {@link AgentReporter.runEvents | the run-event
|
|
275
|
+
* channel} before the first `RunStarted` event. The kit built here
|
|
276
|
+
* carries neutral run health; `detail` is resolved health-aware again
|
|
277
|
+
* for the render kit at `onTestRunEnd`, and that kit is the one handed
|
|
278
|
+
* to `render`.
|
|
279
|
+
*
|
|
280
|
+
* A resolution failure degrades silently: `this.reporters` stays unset
|
|
281
|
+
* and `onTestRunEnd` resolves the factory itself from the render kit.
|
|
282
|
+
*
|
|
283
|
+
* @internal
|
|
284
|
+
*/
|
|
285
|
+
async initReporters() {
|
|
286
|
+
const opts = this.options;
|
|
287
|
+
try {
|
|
288
|
+
const initProgram = Effect.gen(function* () {
|
|
289
|
+
const detector = yield* EnvironmentDetector;
|
|
290
|
+
const executorResolver = yield* ExecutorResolver;
|
|
291
|
+
const formatSelector = yield* FormatSelector;
|
|
292
|
+
const detailResolver = yield* DetailResolver;
|
|
293
|
+
const env = yield* detector.detect();
|
|
294
|
+
const executor = yield* executorResolver.resolve(env);
|
|
295
|
+
return {
|
|
296
|
+
env,
|
|
297
|
+
executor,
|
|
298
|
+
format: yield* formatSelector.select(executor, opts.format, env),
|
|
299
|
+
detail: yield* detailResolver.resolve(executor, {
|
|
300
|
+
hasFailures: false,
|
|
301
|
+
belowTargets: false,
|
|
302
|
+
hasTargets: !!opts.coverageTargets
|
|
303
|
+
}, opts.detail)
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
const { env, executor, format, detail } = await Effect.runPromise(initProgram.pipe(Effect.provide(OutputPipelineLive), Effect.provide(NodeContext.layer)));
|
|
307
|
+
const kit = buildReporterKit({
|
|
308
|
+
env,
|
|
309
|
+
executor,
|
|
310
|
+
format,
|
|
311
|
+
detail,
|
|
312
|
+
noColor: !!process.env.NO_COLOR,
|
|
313
|
+
consoleMode: opts.consoleMode ?? "passthrough",
|
|
314
|
+
mcp: opts.mcp ?? false,
|
|
315
|
+
githubActions: opts.githubActions,
|
|
316
|
+
transport: opts.transport,
|
|
317
|
+
runEvents: this.runEvents,
|
|
318
|
+
...this.dbPath !== null && { dbPath: this.dbPath },
|
|
319
|
+
...opts.projectFilter !== void 0 && { projectFilter: opts.projectFilter },
|
|
320
|
+
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
321
|
+
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
322
|
+
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
323
|
+
coverageMode: opts.coverageMode
|
|
324
|
+
});
|
|
325
|
+
this.reporters = normalizeReporters(opts.reporter(kit));
|
|
326
|
+
} catch (err) {
|
|
327
|
+
process.stderr.write(`vitest-agent: reporter init failed; retrying at run end (${formatFatalError(err)})\n`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Should the streaming Vitest hooks publish `RunEvent`s? The
|
|
332
|
+
* hooks short-circuit when this is false to skip constructing event
|
|
333
|
+
* objects nothing will read: the default reporter consumes the channel
|
|
334
|
+
* only in `stream` mode, a custom reporter may consume it in any mode,
|
|
335
|
+
* and the user `onRunEvent` tap is fed off the same stream.
|
|
336
|
+
*
|
|
337
|
+
* @internal
|
|
338
|
+
*/
|
|
339
|
+
wantsRunEvents() {
|
|
340
|
+
return this.onRunEvent !== void 0 || this.options.consoleMode === "stream" || this.hasCustomReporter;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Publish a `RunEvent` onto the run-event channel and the
|
|
344
|
+
* user-supplied tap. A throwing publish or tap is caught and logged to
|
|
345
|
+
* stderr — a live-render bug must not break persistence.
|
|
346
|
+
*
|
|
347
|
+
* @internal
|
|
348
|
+
*/
|
|
349
|
+
emit(event) {
|
|
350
|
+
try {
|
|
351
|
+
Effect.runSync(PubSub.publish(this.runEvents, event));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
process.stderr.write(`vitest-agent: run-event publish threw: ${formatFatalError(err)}\n`);
|
|
354
|
+
}
|
|
355
|
+
const tap = this.onRunEvent;
|
|
356
|
+
if (tap !== void 0) try {
|
|
357
|
+
tap(event);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
process.stderr.write(`vitest-agent: onRunEvent tap threw: ${formatFatalError(err)}\n`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Walk the suite parent chain to produce the suite-path array used
|
|
364
|
+
* in `RunEvent.TestStarted` / `RunEvent.TestFinished`.
|
|
365
|
+
*
|
|
366
|
+
* @internal
|
|
367
|
+
*/
|
|
368
|
+
collectSuitePath(testCase) {
|
|
369
|
+
const path = [];
|
|
370
|
+
let cursor = testCase.parent;
|
|
371
|
+
while (cursor !== void 0 && cursor.type === "suite") {
|
|
372
|
+
path.unshift(cursor.name);
|
|
373
|
+
cursor = cursor.parent;
|
|
374
|
+
}
|
|
375
|
+
return path;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Vitest streaming hook: a test run is about to start. Mints a
|
|
379
|
+
* synthetic `runId` for the duration of this run and emits a
|
|
380
|
+
* `RunStarted` event for live subscribers.
|
|
381
|
+
*/
|
|
382
|
+
onTestRunStart(_specifications) {
|
|
383
|
+
if (!this.wantsRunEvents()) return;
|
|
384
|
+
this.currentRunId = randomUUID();
|
|
385
|
+
this.moduleStartedAt.clear();
|
|
386
|
+
this.emit({
|
|
387
|
+
_tag: "RunStarted",
|
|
388
|
+
runId: this.currentRunId,
|
|
389
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
390
|
+
configHash: "live"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Read the owning Vitest project name off a `TestModule`. Vitest 4.x
|
|
395
|
+
* attaches `project` to every module; an empty name (the unnamed
|
|
396
|
+
* default project) collapses to `undefined` so the renderer treats it
|
|
397
|
+
* as a single anonymous project.
|
|
398
|
+
*
|
|
399
|
+
* @internal
|
|
400
|
+
*/
|
|
401
|
+
moduleProjectName(testModule) {
|
|
402
|
+
const name = testModule.project?.name;
|
|
403
|
+
return name !== void 0 && name.length > 0 ? name : void 0;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Vitest streaming hook: a test module has been queued.
|
|
407
|
+
*/
|
|
408
|
+
onTestModuleQueued(testModule) {
|
|
409
|
+
if (!this.wantsRunEvents()) return;
|
|
410
|
+
const projectName = this.moduleProjectName(testModule);
|
|
411
|
+
this.emit({
|
|
412
|
+
_tag: "ModuleQueued",
|
|
413
|
+
modulePath: testModule.relativeModuleId,
|
|
414
|
+
...projectName !== void 0 && { projectName }
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Vitest streaming hook: a test module is starting execution.
|
|
419
|
+
*/
|
|
420
|
+
onTestModuleStart(testModule) {
|
|
421
|
+
if (!this.wantsRunEvents()) return;
|
|
422
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
423
|
+
this.moduleStartedAt.set(testModule.relativeModuleId, startedAt);
|
|
424
|
+
const projectName = this.moduleProjectName(testModule);
|
|
425
|
+
this.emit({
|
|
426
|
+
_tag: "ModuleStarted",
|
|
427
|
+
modulePath: testModule.relativeModuleId,
|
|
428
|
+
startedAt,
|
|
429
|
+
...projectName !== void 0 && { projectName }
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Vitest streaming hook: a test module has finished collecting its
|
|
434
|
+
* tests and suites, before execution begins.
|
|
435
|
+
*/
|
|
436
|
+
onTestModuleCollected(testModule) {
|
|
437
|
+
if (!this.wantsRunEvents()) return;
|
|
438
|
+
let testCount = 0;
|
|
439
|
+
for (const _ of testModule.children.allTests()) testCount++;
|
|
440
|
+
let suiteCount = 0;
|
|
441
|
+
for (const _ of testModule.children.allSuites()) suiteCount++;
|
|
442
|
+
this.emit({
|
|
443
|
+
_tag: "ModuleCollected",
|
|
444
|
+
modulePath: testModule.relativeModuleId,
|
|
445
|
+
testCount,
|
|
446
|
+
suiteCount
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Vitest streaming hook: a test case is about to run.
|
|
451
|
+
*
|
|
452
|
+
* Emits a standalone `TestStarted` so the renderer gets a render
|
|
453
|
+
* frame for the transient "running" state. `onTestCaseReady` fires
|
|
454
|
+
* before `onTestCaseResult`; emitting here — rather than synthesizing
|
|
455
|
+
* `TestStarted` back-to-back with `TestFinished` inside
|
|
456
|
+
* `onTestCaseResult` — gives the running state a real lifetime in the
|
|
457
|
+
* event stream and is the prerequisite for any future per-test
|
|
458
|
+
* animation.
|
|
459
|
+
*/
|
|
460
|
+
onTestCaseReady(testCase) {
|
|
461
|
+
if (!this.wantsRunEvents()) return;
|
|
462
|
+
const modulePath = testCase.module?.relativeModuleId ?? "";
|
|
463
|
+
if (modulePath === "") return;
|
|
464
|
+
this.emit({
|
|
465
|
+
_tag: "TestStarted",
|
|
466
|
+
modulePath,
|
|
467
|
+
testName: testCase.name,
|
|
468
|
+
suitePath: this.collectSuitePath(testCase)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Vitest streaming hook: a test case has produced a result.
|
|
473
|
+
*
|
|
474
|
+
* Emits only `TestFinished` — the matching `TestStarted` is emitted
|
|
475
|
+
* by `onTestCaseReady`, which fires earlier in the lifecycle.
|
|
476
|
+
*/
|
|
477
|
+
onTestCaseResult(testCase) {
|
|
478
|
+
if (!this.wantsRunEvents()) return;
|
|
479
|
+
const modulePath = testCase.module?.relativeModuleId ?? "";
|
|
480
|
+
if (modulePath === "") return;
|
|
481
|
+
const suitePath = this.collectSuitePath(testCase);
|
|
482
|
+
const result = testCase.result();
|
|
483
|
+
const diag = testCase.diagnostic();
|
|
484
|
+
const status = result?.state === "passed" || result?.state === "failed" || result?.state === "skipped" ? result.state : "pending";
|
|
485
|
+
const firstError = result?.errors?.[0];
|
|
486
|
+
const expectedStr = stringifyFailureValue(firstError?.expected);
|
|
487
|
+
const receivedStr = stringifyFailureValue(firstError?.actual);
|
|
488
|
+
const error = firstError !== void 0 ? {
|
|
489
|
+
message: firstError.message,
|
|
490
|
+
...firstError.diff !== void 0 && { diff: firstError.diff },
|
|
491
|
+
...expectedStr !== void 0 && { expected: expectedStr },
|
|
492
|
+
...receivedStr !== void 0 && { received: receivedStr }
|
|
493
|
+
} : void 0;
|
|
494
|
+
const timedOut = result?.state === "failed" && firstError !== void 0 && isTimeoutError(firstError);
|
|
495
|
+
this.emit({
|
|
496
|
+
_tag: "TestFinished",
|
|
497
|
+
modulePath,
|
|
498
|
+
testName: testCase.name,
|
|
499
|
+
suitePath,
|
|
500
|
+
status,
|
|
501
|
+
durationMs: diag?.duration ?? 0,
|
|
502
|
+
...error !== void 0 && { error },
|
|
503
|
+
...timedOut && { timedOut: true }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Vitest streaming hook: a test module has finished. Emits a
|
|
508
|
+
* `ModuleFinished` carrying the tallied counts plus a duration
|
|
509
|
+
* derived from the module's diagnostic.
|
|
510
|
+
*/
|
|
511
|
+
onTestModuleEnd(testModule) {
|
|
512
|
+
if (!this.wantsRunEvents()) return;
|
|
513
|
+
let pass = 0;
|
|
514
|
+
let fail = 0;
|
|
515
|
+
let skip = 0;
|
|
516
|
+
let timeout = 0;
|
|
517
|
+
for (const test of testModule.children.allTests()) {
|
|
518
|
+
const result = test.result();
|
|
519
|
+
const state = result?.state;
|
|
520
|
+
if (state === "passed") pass++;
|
|
521
|
+
else if (state === "failed") {
|
|
522
|
+
const firstError = result?.errors?.[0];
|
|
523
|
+
if (firstError !== void 0 && isTimeoutError(firstError)) timeout++;
|
|
524
|
+
else fail++;
|
|
525
|
+
} else skip++;
|
|
526
|
+
}
|
|
527
|
+
const tagCounts = {};
|
|
528
|
+
for (const test of testModule.children.allTests()) for (const tag of test.tags ?? []) tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
|
|
529
|
+
const projectName = this.moduleProjectName(testModule);
|
|
530
|
+
this.emit({
|
|
531
|
+
_tag: "ModuleFinished",
|
|
532
|
+
modulePath: testModule.relativeModuleId,
|
|
533
|
+
passCount: pass,
|
|
534
|
+
failCount: fail,
|
|
535
|
+
skipCount: skip,
|
|
536
|
+
timeoutCount: timeout,
|
|
537
|
+
durationMs: testModule.diagnostic()?.duration ?? 0,
|
|
538
|
+
...projectName !== void 0 && { projectName },
|
|
539
|
+
...Object.keys(tagCounts).length > 0 && { tagCounts }
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Walk a suite/test entity's parent chain to its owning module path.
|
|
544
|
+
*
|
|
545
|
+
* @internal
|
|
546
|
+
*/
|
|
547
|
+
entityModulePath(entity) {
|
|
548
|
+
return entity.relativeModuleId ?? entity.module?.relativeModuleId ?? "";
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Vitest streaming hook: a test suite is about to run.
|
|
552
|
+
*/
|
|
553
|
+
onTestSuiteReady(testSuite) {
|
|
554
|
+
if (!this.wantsRunEvents()) return;
|
|
555
|
+
const modulePath = this.entityModulePath(testSuite);
|
|
556
|
+
if (modulePath === "") return;
|
|
557
|
+
this.emit({
|
|
558
|
+
_tag: "SuiteStarted",
|
|
559
|
+
modulePath,
|
|
560
|
+
suitePath: this.collectSuitePath(testSuite),
|
|
561
|
+
suiteName: testSuite.name
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Vitest streaming hook: a test suite has finished running.
|
|
566
|
+
*/
|
|
567
|
+
onTestSuiteResult(testSuite) {
|
|
568
|
+
if (!this.wantsRunEvents()) return;
|
|
569
|
+
const modulePath = this.entityModulePath(testSuite);
|
|
570
|
+
if (modulePath === "") return;
|
|
571
|
+
let pass = 0;
|
|
572
|
+
let fail = 0;
|
|
573
|
+
let skip = 0;
|
|
574
|
+
for (const test of testSuite.children.allTests()) {
|
|
575
|
+
const state = test.result()?.state;
|
|
576
|
+
if (state === "passed") pass++;
|
|
577
|
+
else if (state === "failed") fail++;
|
|
578
|
+
else skip++;
|
|
579
|
+
}
|
|
580
|
+
this.emit({
|
|
581
|
+
_tag: "SuiteFinished",
|
|
582
|
+
modulePath,
|
|
583
|
+
suitePath: this.collectSuitePath(testSuite),
|
|
584
|
+
suiteName: testSuite.name,
|
|
585
|
+
passCount: pass,
|
|
586
|
+
failCount: fail,
|
|
587
|
+
skipCount: skip
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Derive `modulePath` / `scopeName` / a stable key for a Vitest
|
|
592
|
+
* setup/teardown hook from its `ReportedHookContext` entity.
|
|
593
|
+
*
|
|
594
|
+
* @internal
|
|
595
|
+
*/
|
|
596
|
+
hookScope(hook) {
|
|
597
|
+
const entity = hook.entity ?? {};
|
|
598
|
+
return {
|
|
599
|
+
modulePath: entity.relativeModuleId ?? entity.module?.relativeModuleId ?? "",
|
|
600
|
+
scopeName: entity.name ?? entity.relativeModuleId ?? ""
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Vitest streaming hook: a `beforeAll` / `afterAll` / `beforeEach` /
|
|
605
|
+
* `afterEach` hook is starting.
|
|
606
|
+
*/
|
|
607
|
+
onHookStart(hook) {
|
|
608
|
+
if (!this.wantsRunEvents()) return;
|
|
609
|
+
if (hook.name !== "beforeAll" && hook.name !== "afterAll" && hook.name !== "beforeEach" && hook.name !== "afterEach") return;
|
|
610
|
+
const { modulePath, scopeName } = this.hookScope(hook);
|
|
611
|
+
this.hookStartedAt.set(`${modulePath}::${hook.name}::${scopeName}`, Date.now());
|
|
612
|
+
this.emit({
|
|
613
|
+
_tag: "HookStarted",
|
|
614
|
+
modulePath,
|
|
615
|
+
hookType: hook.name,
|
|
616
|
+
scopeName
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Vitest streaming hook: a setup/teardown hook has finished. Duration
|
|
621
|
+
* is derived from the stamp `onHookStart` recorded — `ReportedHookContext`
|
|
622
|
+
* carries none of its own. Status is a best-effort read of the
|
|
623
|
+
* entity's collected errors.
|
|
624
|
+
*/
|
|
625
|
+
onHookEnd(hook) {
|
|
626
|
+
if (!this.wantsRunEvents()) return;
|
|
627
|
+
if (hook.name !== "beforeAll" && hook.name !== "afterAll" && hook.name !== "beforeEach" && hook.name !== "afterEach") return;
|
|
628
|
+
const { modulePath, scopeName } = this.hookScope(hook);
|
|
629
|
+
const key = `${modulePath}::${hook.name}::${scopeName}`;
|
|
630
|
+
const startedAt = this.hookStartedAt.get(key);
|
|
631
|
+
this.hookStartedAt.delete(key);
|
|
632
|
+
const durationMs = startedAt !== void 0 ? Date.now() - startedAt : 0;
|
|
633
|
+
const entityErrors = typeof hook.entity?.errors === "function" ? hook.entity.errors() : [];
|
|
634
|
+
const firstError = entityErrors[0];
|
|
635
|
+
this.emit({
|
|
636
|
+
_tag: "HookFinished",
|
|
637
|
+
modulePath,
|
|
638
|
+
hookType: hook.name,
|
|
639
|
+
scopeName,
|
|
640
|
+
durationMs,
|
|
641
|
+
status: entityErrors.length > 0 ? "failed" : "passed",
|
|
642
|
+
...firstError !== void 0 && { error: {
|
|
643
|
+
message: firstError.message,
|
|
644
|
+
...firstError.stack !== void 0 && { stack: firstError.stack },
|
|
645
|
+
...firstError.diff !== void 0 && { diff: firstError.diff }
|
|
646
|
+
} }
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Vitest streaming hook: a captured `console.log` / `console.error`
|
|
651
|
+
* from user test code. The Vitest log carries a `taskId` rather than
|
|
652
|
+
* a module/test path; resolving it back to one would need the task
|
|
653
|
+
* registry, so `modulePath` / `testName` are left unset.
|
|
654
|
+
*/
|
|
655
|
+
onUserConsoleLog(log) {
|
|
656
|
+
if (!this.wantsRunEvents()) return;
|
|
657
|
+
this.emit({
|
|
658
|
+
_tag: "ConsoleLog",
|
|
659
|
+
level: log.type,
|
|
660
|
+
content: log.content,
|
|
661
|
+
time: log.time
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Vitest streaming hook: the run exceeded its configured process
|
|
666
|
+
* timeout. Terminal — moves the renderer to a final frame.
|
|
667
|
+
*/
|
|
668
|
+
onProcessTimeout() {
|
|
669
|
+
if (!this.wantsRunEvents()) return;
|
|
670
|
+
this.emit({
|
|
671
|
+
_tag: "RunTimedOut",
|
|
672
|
+
message: "Test run exceeded the configured process timeout."
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Vitest streaming hook: a test case recorded an annotation.
|
|
677
|
+
*/
|
|
678
|
+
onTestCaseAnnotate(testCase, annotation) {
|
|
679
|
+
if (!this.wantsRunEvents()) return;
|
|
680
|
+
const modulePath = testCase.module?.relativeModuleId ?? "";
|
|
681
|
+
if (modulePath === "") return;
|
|
682
|
+
this.emit({
|
|
683
|
+
_tag: "TestAnnotated",
|
|
684
|
+
modulePath,
|
|
685
|
+
testName: testCase.name,
|
|
686
|
+
suitePath: this.collectSuitePath(testCase),
|
|
687
|
+
annotation: annotation.message
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Vitest streaming hook: a test case recorded an artifact.
|
|
692
|
+
*/
|
|
693
|
+
onTestCaseArtifactRecord(testCase, artifact) {
|
|
694
|
+
if (!this.wantsRunEvents()) return;
|
|
695
|
+
const modulePath = testCase.module?.relativeModuleId ?? "";
|
|
696
|
+
if (modulePath === "") return;
|
|
697
|
+
this.emit({
|
|
698
|
+
_tag: "TestArtifactRecorded",
|
|
699
|
+
modulePath,
|
|
700
|
+
testName: testCase.name,
|
|
701
|
+
suitePath: this.collectSuitePath(testCase),
|
|
702
|
+
artifact: artifact.type ?? "artifact"
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Vitest streaming hook: watch mode has finished its initial run and
|
|
707
|
+
* is waiting for file changes.
|
|
708
|
+
*/
|
|
709
|
+
onWatcherStart() {
|
|
710
|
+
if (!this.wantsRunEvents()) return;
|
|
711
|
+
this.emit({ _tag: "WatcherReady" });
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Vitest streaming hook: watch mode is re-running because tracked
|
|
715
|
+
* files changed.
|
|
716
|
+
*/
|
|
717
|
+
onWatcherRerun(files, trigger) {
|
|
718
|
+
if (!this.wantsRunEvents()) return;
|
|
719
|
+
this.emit({
|
|
720
|
+
_tag: "WatcherRerun",
|
|
721
|
+
triggerFiles: [...files],
|
|
722
|
+
...trigger !== void 0 && { reason: trigger }
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async ensureDbPath() {
|
|
726
|
+
if (this.dbPath) return this.dbPath;
|
|
727
|
+
if (this.options.cacheDir) {
|
|
728
|
+
mkdirSync(this.options.cacheDir, { recursive: true });
|
|
729
|
+
this.dbPath = `${this.options.cacheDir}/data.db`;
|
|
730
|
+
return this.dbPath;
|
|
731
|
+
}
|
|
732
|
+
const projectDir = process.cwd();
|
|
733
|
+
this.dbPath = await resolveDataPathCached(projectDir);
|
|
734
|
+
return this.dbPath;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Stash coverage data for merging into reports.
|
|
738
|
+
*
|
|
739
|
+
* @privateRemarks
|
|
740
|
+
* This hook fires **before** `onTestRunEnd` in Vitest's lifecycle.
|
|
741
|
+
* The coverage value is an istanbul CoverageMap that will be duck-typed
|
|
742
|
+
* and processed during `onTestRunEnd`.
|
|
743
|
+
*
|
|
744
|
+
* @param coverage - Istanbul CoverageMap (duck-typed at processing time)
|
|
745
|
+
*
|
|
746
|
+
* @see `processCoverage` for the duck-typing logic
|
|
747
|
+
*/
|
|
748
|
+
onCoverage(coverage) {
|
|
749
|
+
this.coverage = coverage;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Process test results, write reports, and emit formatted output.
|
|
753
|
+
*
|
|
754
|
+
* @remarks
|
|
755
|
+
* This is the main lifecycle hook where all output is generated.
|
|
756
|
+
* Processing steps:
|
|
757
|
+
*
|
|
758
|
+
* 1. Group test modules by `testModule.project.name`
|
|
759
|
+
* 2. Process stashed coverage data (if available)
|
|
760
|
+
* 3. Build per-project `AgentReport` objects
|
|
761
|
+
* 4. Classify tests via HistoryTracker and attach classifications
|
|
762
|
+
* 5. Write settings, run, modules, test cases, and errors to SQLite
|
|
763
|
+
* 6. Write per-test history entries
|
|
764
|
+
* 7. Write baselines and trends
|
|
765
|
+
* 8. Emit console markdown (unless `"silent"`)
|
|
766
|
+
* 9. Write GFM summary to `GITHUB_STEP_SUMMARY` (if GitHub Actions)
|
|
767
|
+
*
|
|
768
|
+
* File write failures are logged to stderr but do not crash the test run.
|
|
769
|
+
*
|
|
770
|
+
* @param testModules - All test modules from the completed run
|
|
771
|
+
* @param unhandledErrors - Any unhandled errors during the run
|
|
772
|
+
* @param reason - Overall outcome: `"passed"`, `"failed"`, or `"interrupted"`
|
|
773
|
+
*/
|
|
774
|
+
async onTestRunEnd(testModules, unhandledErrors, reason) {
|
|
775
|
+
if (this.rendered) return;
|
|
776
|
+
this.rendered = true;
|
|
777
|
+
if (this.wantsRunEvents() && this.currentRunId !== null) {
|
|
778
|
+
let pass = 0;
|
|
779
|
+
let fail = 0;
|
|
780
|
+
let skip = 0;
|
|
781
|
+
let timeout = 0;
|
|
782
|
+
let totalDuration = 0;
|
|
783
|
+
for (const mod of testModules) {
|
|
784
|
+
totalDuration += mod.diagnostic()?.duration ?? 0;
|
|
785
|
+
for (const test of mod.children.allTests()) {
|
|
786
|
+
const result = test.result();
|
|
787
|
+
const state = result?.state;
|
|
788
|
+
if (state === "passed") pass++;
|
|
789
|
+
else if (state === "failed") {
|
|
790
|
+
const firstError = result?.errors?.[0];
|
|
791
|
+
if (firstError !== void 0 && isTimeoutError(firstError)) timeout++;
|
|
792
|
+
else fail++;
|
|
793
|
+
} else skip++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.emit({
|
|
797
|
+
_tag: "RunFinished",
|
|
798
|
+
runId: this.currentRunId,
|
|
799
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
passCount: pass,
|
|
801
|
+
failCount: fail,
|
|
802
|
+
skipCount: skip,
|
|
803
|
+
timeoutCount: timeout,
|
|
804
|
+
durationMs: totalDuration
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
const modules = testModules;
|
|
808
|
+
const errors = unhandledErrors;
|
|
809
|
+
const opts = this.options;
|
|
810
|
+
const stashedCoverage = this.coverage;
|
|
811
|
+
const stashedVitest = this._vitest;
|
|
812
|
+
const logLevel = this.logLevel;
|
|
813
|
+
const logFile = this.logFile;
|
|
814
|
+
const runEvents = this.runEvents;
|
|
815
|
+
const preBuiltReporters = this.reporters;
|
|
816
|
+
const emitEvent = (event) => {
|
|
817
|
+
this.emit(event);
|
|
818
|
+
};
|
|
819
|
+
const wantsRunEvents = this.wantsRunEvents();
|
|
820
|
+
let dbPath;
|
|
821
|
+
try {
|
|
822
|
+
dbPath = await this.ensureDbPath();
|
|
823
|
+
} catch (err) {
|
|
824
|
+
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const filteredModules = opts.projectFilter ? modules.filter((m) => (m.project.name || "default") === opts.projectFilter) : modules;
|
|
828
|
+
if (filteredModules.length === 0 && opts.projectFilter) return;
|
|
829
|
+
if (opts.coverageMode === "ui-only") {
|
|
830
|
+
const uiProjectGroups = /* @__PURE__ */ new Map();
|
|
831
|
+
for (const mod of filteredModules) {
|
|
832
|
+
const key = mod.project.name || "default";
|
|
833
|
+
const existing = uiProjectGroups.get(key);
|
|
834
|
+
if (existing) existing.push(mod);
|
|
835
|
+
else uiProjectGroups.set(key, [mod]);
|
|
836
|
+
}
|
|
837
|
+
const isMultiProject = uiProjectGroups.size > 1 || !!opts.projectFilter;
|
|
838
|
+
const uiReports = [];
|
|
839
|
+
for (const [projectName, projectModules] of uiProjectGroups) {
|
|
840
|
+
const report = buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProject ? projectName : void 0);
|
|
841
|
+
uiReports.push(report);
|
|
842
|
+
}
|
|
843
|
+
const uiProgram = Effect.gen(function* () {
|
|
844
|
+
const detector = yield* EnvironmentDetector;
|
|
845
|
+
const executorResolver = yield* ExecutorResolver;
|
|
846
|
+
const formatSelector = yield* FormatSelector;
|
|
847
|
+
const detailResolver = yield* DetailResolver;
|
|
848
|
+
const env = yield* detector.detect();
|
|
849
|
+
const executor = yield* executorResolver.resolve(env);
|
|
850
|
+
const format = yield* formatSelector.select(executor, opts.format, env);
|
|
851
|
+
const health = {
|
|
852
|
+
hasFailures: uiReports.some((r) => r.summary.failed > 0 || r.unhandledErrors.length > 0),
|
|
853
|
+
belowTargets: false,
|
|
854
|
+
hasTargets: !!opts.coverageTargets
|
|
855
|
+
};
|
|
856
|
+
const detail = yield* detailResolver.resolve(executor, health, opts.detail);
|
|
857
|
+
const githubSummaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
858
|
+
const kit = buildReporterKit({
|
|
859
|
+
env,
|
|
860
|
+
executor,
|
|
861
|
+
format,
|
|
862
|
+
detail,
|
|
863
|
+
noColor: !!process.env.NO_COLOR,
|
|
864
|
+
consoleMode: opts.consoleMode ?? "passthrough",
|
|
865
|
+
mcp: opts.mcp ?? false,
|
|
866
|
+
githubActions: opts.githubActions,
|
|
867
|
+
transport: opts.transport,
|
|
868
|
+
runEvents,
|
|
869
|
+
...dbPath !== void 0 && { dbPath },
|
|
870
|
+
...opts.projectFilter !== void 0 && { projectFilter: opts.projectFilter },
|
|
871
|
+
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
872
|
+
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
873
|
+
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
874
|
+
coverageMode: opts.coverageMode
|
|
875
|
+
});
|
|
876
|
+
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
877
|
+
const renderInput = {
|
|
878
|
+
reports: uiReports,
|
|
879
|
+
classifications: /* @__PURE__ */ new Map()
|
|
880
|
+
};
|
|
881
|
+
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
882
|
+
for (const output of allOutputs) routeRenderedOutput(output, { ...githubSummaryFile !== void 0 && { githubSummaryFile } });
|
|
883
|
+
});
|
|
884
|
+
await Effect.runPromise(uiProgram.pipe(Effect.provide(OutputPipelineLive), Effect.provide(NodeContext.layer))).catch((err) => {
|
|
885
|
+
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
886
|
+
});
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
890
|
+
try {
|
|
891
|
+
await ensureMigrated(dbPath, logLevel, logFile);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const program = Effect.gen(function* () {
|
|
897
|
+
const store = yield* DataStore;
|
|
898
|
+
const reader = yield* DataReader;
|
|
899
|
+
const analyzer = yield* CoverageAnalyzer;
|
|
900
|
+
const tracker = yield* HistoryTracker;
|
|
901
|
+
const invocationId = randomUUID();
|
|
902
|
+
const vitest = stashedVitest;
|
|
903
|
+
const settings = captureSettings(vitest?.config ?? {}, vitest?.version ?? "unknown");
|
|
904
|
+
const settingsHash = hashSettings(settings);
|
|
905
|
+
const envVars = captureEnvVars(process.env);
|
|
906
|
+
yield* store.writeSettings(settingsHash, settings, envVars);
|
|
907
|
+
const projectGroups = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const mod of filteredModules) {
|
|
909
|
+
const key = mod.project.name || "default";
|
|
910
|
+
const existing = projectGroups.get(key);
|
|
911
|
+
if (existing) existing.push(mod);
|
|
912
|
+
else projectGroups.set(key, [mod]);
|
|
913
|
+
}
|
|
914
|
+
const baselinesOpt = yield* reader.getBaselines("__global__").pipe(Effect.catchAll(() => Effect.succeed(Option.none())));
|
|
915
|
+
const baselines = Option.getOrUndefined(baselinesOpt);
|
|
916
|
+
const projectModuleCounts = /* @__PURE__ */ new Map();
|
|
917
|
+
for (const m of modules) {
|
|
918
|
+
const key = m.project.name || "default";
|
|
919
|
+
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
920
|
+
}
|
|
921
|
+
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
922
|
+
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
923
|
+
const coverageOpts = {
|
|
924
|
+
thresholds: opts.coverageThresholds,
|
|
925
|
+
includeBareZero: opts.includeBareZero,
|
|
926
|
+
...opts.coverageTargets ? { targets: opts.coverageTargets } : {},
|
|
927
|
+
...baselines ? { baselines } : {}
|
|
928
|
+
};
|
|
929
|
+
const coverageResult = stashedCoverage && isFirstProject ? yield* analyzer.process(stashedCoverage, coverageOpts) : Option.none();
|
|
930
|
+
const coverageReport = Option.getOrUndefined(coverageResult);
|
|
931
|
+
if (wantsRunEvents && coverageReport !== void 0) {
|
|
932
|
+
const globalThresholds = coverageReport.thresholds.global;
|
|
933
|
+
emitEvent({
|
|
934
|
+
_tag: "CoverageReady",
|
|
935
|
+
metrics: coverageReport.totals,
|
|
936
|
+
thresholds: globalThresholds,
|
|
937
|
+
gaps: coverageReport.lowCoverage.map((fc) => ({
|
|
938
|
+
file: fc.file,
|
|
939
|
+
missing: fc.summary,
|
|
940
|
+
uncoveredLines: fc.uncoveredLines
|
|
941
|
+
}))
|
|
942
|
+
});
|
|
943
|
+
for (const metric of [
|
|
944
|
+
"lines",
|
|
945
|
+
"branches",
|
|
946
|
+
"functions",
|
|
947
|
+
"statements"
|
|
948
|
+
]) {
|
|
949
|
+
const expected = globalThresholds[metric];
|
|
950
|
+
const actual = coverageReport.totals[metric];
|
|
951
|
+
if (expected !== void 0 && actual < expected) emitEvent({
|
|
952
|
+
_tag: "ThresholdViolation",
|
|
953
|
+
metric,
|
|
954
|
+
expected,
|
|
955
|
+
actual
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const reports = [];
|
|
960
|
+
const isMultiProject = projectGroups.size > 1 || !!opts.projectFilter;
|
|
961
|
+
for (const [projectName, projectModules] of projectGroups) {
|
|
962
|
+
const project = projectName === "default" ? "default" : projectName;
|
|
963
|
+
const baseReport = buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProject ? projectName : void 0);
|
|
964
|
+
let totalDuration = 0;
|
|
965
|
+
for (const mod of projectModules) totalDuration += mod.diagnostic()?.duration ?? 0;
|
|
966
|
+
const envAgentId = process.env.VITEST_AGENT_AGENT_ID;
|
|
967
|
+
const envConversationId = process.env.VITEST_AGENT_CONVERSATION_ID;
|
|
968
|
+
const attribution = envAgentId !== void 0 && envAgentId.length > 0 ? {
|
|
969
|
+
actorType: "agent",
|
|
970
|
+
agentId: envAgentId,
|
|
971
|
+
conversationId: envConversationId ?? null
|
|
972
|
+
} : {
|
|
973
|
+
actorType: "system",
|
|
974
|
+
agentId: null,
|
|
975
|
+
conversationId: null
|
|
976
|
+
};
|
|
977
|
+
const hostProbe = probeHostMetadataFromEnv(process.env);
|
|
978
|
+
const runId = yield* store.writeRun({
|
|
979
|
+
invocationId,
|
|
980
|
+
project,
|
|
981
|
+
settingsHash,
|
|
982
|
+
timestamp: baseReport.timestamp,
|
|
983
|
+
commitSha: process.env.GITHUB_SHA ?? null,
|
|
984
|
+
branch: process.env.GITHUB_REF_NAME ?? null,
|
|
985
|
+
reason,
|
|
986
|
+
duration: totalDuration,
|
|
987
|
+
total: baseReport.summary.total,
|
|
988
|
+
passed: baseReport.summary.passed,
|
|
989
|
+
failed: baseReport.summary.failed,
|
|
990
|
+
skipped: baseReport.summary.skipped,
|
|
991
|
+
scoped: false,
|
|
992
|
+
actorType: attribution.actorType,
|
|
993
|
+
agentId: attribution.agentId,
|
|
994
|
+
conversationId: attribution.conversationId,
|
|
995
|
+
gitBranch: process.env.GITHUB_REF_NAME ?? null,
|
|
996
|
+
gitCommitSha: process.env.GITHUB_SHA ?? null,
|
|
997
|
+
hostSource: hostProbe.source,
|
|
998
|
+
hostValue: hostProbe.value,
|
|
999
|
+
hostMetadata: hostProbe.metadata
|
|
1000
|
+
});
|
|
1001
|
+
for (const mod of projectModules) {
|
|
1002
|
+
const fileId = yield* store.ensureFile(mod.relativeModuleId);
|
|
1003
|
+
const moduleId = (yield* store.writeModules(runId, [{
|
|
1004
|
+
fileId,
|
|
1005
|
+
relativeModuleId: mod.relativeModuleId,
|
|
1006
|
+
state: mod.state(),
|
|
1007
|
+
duration: mod.diagnostic()?.duration ?? 0
|
|
1008
|
+
}]))[0];
|
|
1009
|
+
const sourceFile = mod.relativeModuleId.replace(/\.test\.([^.]+)$/, ".$1").replace(/\.spec\.([^.]+)$/, ".$1");
|
|
1010
|
+
if (sourceFile !== mod.relativeModuleId) yield* store.writeSourceMap(sourceFile, moduleId, "convention");
|
|
1011
|
+
const suiteIdMap = /* @__PURE__ */ new Map();
|
|
1012
|
+
for (const suite of mod.children.allSuites()) {
|
|
1013
|
+
const parentSuiteId = suite.parent && suite.parent.type === "suite" ? suiteIdMap.get(suite.parent.fullName) : void 0;
|
|
1014
|
+
const suiteIds = yield* store.writeSuites(moduleId, [{
|
|
1015
|
+
name: suite.name,
|
|
1016
|
+
fullName: suite.fullName,
|
|
1017
|
+
state: suite.state(),
|
|
1018
|
+
...parentSuiteId !== void 0 && { parentSuiteId },
|
|
1019
|
+
...suite.options?.mode !== void 0 && { mode: suite.options.mode },
|
|
1020
|
+
...suite.options?.concurrent !== void 0 && { concurrent: suite.options.concurrent },
|
|
1021
|
+
...suite.options?.shuffle !== void 0 && { shuffle: suite.options.shuffle },
|
|
1022
|
+
...suite.options?.retry !== void 0 && { retry: suite.options.retry },
|
|
1023
|
+
...suite.options?.repeats !== void 0 && { repeats: suite.options.repeats },
|
|
1024
|
+
...suite.location?.line !== void 0 && { locationLine: suite.location.line },
|
|
1025
|
+
...suite.location?.column !== void 0 && { locationColumn: suite.location.column }
|
|
1026
|
+
}]);
|
|
1027
|
+
suiteIdMap.set(suite.fullName, suiteIds[0]);
|
|
1028
|
+
}
|
|
1029
|
+
const testCases = [];
|
|
1030
|
+
for (const testCase of mod.children.allTests()) {
|
|
1031
|
+
const result = testCase.result();
|
|
1032
|
+
const diag = testCase.diagnostic();
|
|
1033
|
+
const parent = testCase.parent;
|
|
1034
|
+
const parentSuiteId = parent && parent.type === "suite" ? suiteIdMap.get(parent.fullName) : void 0;
|
|
1035
|
+
testCases.push({
|
|
1036
|
+
name: testCase.name,
|
|
1037
|
+
fullName: testCase.fullName,
|
|
1038
|
+
state: result?.state ?? "pending",
|
|
1039
|
+
...diag?.duration !== void 0 && { duration: diag.duration },
|
|
1040
|
+
...diag?.flaky !== void 0 && { flaky: diag.flaky },
|
|
1041
|
+
...diag?.slow !== void 0 && { slow: diag.slow },
|
|
1042
|
+
...testCase.tags.length > 0 && { tags: testCase.tags },
|
|
1043
|
+
...parentSuiteId !== void 0 && { suiteId: parentSuiteId }
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const testCaseIds = yield* store.writeTestCases(moduleId, testCases.map((tc) => ({
|
|
1047
|
+
name: tc.name,
|
|
1048
|
+
fullName: tc.fullName,
|
|
1049
|
+
state: tc.state,
|
|
1050
|
+
...tc.duration !== void 0 && { duration: tc.duration },
|
|
1051
|
+
...tc.flaky !== void 0 && { flaky: tc.flaky },
|
|
1052
|
+
...tc.slow !== void 0 && { slow: tc.slow },
|
|
1053
|
+
...tc.tags !== void 0 && { tags: tc.tags },
|
|
1054
|
+
...tc.suiteId !== void 0 && { suiteId: tc.suiteId }
|
|
1055
|
+
})));
|
|
1056
|
+
let testIdx = 0;
|
|
1057
|
+
for (const testCase of mod.children.allTests()) {
|
|
1058
|
+
const result = testCase.result();
|
|
1059
|
+
if (result?.errors && result.errors.length > 0) {
|
|
1060
|
+
const testCaseId = testCaseIds[testIdx];
|
|
1061
|
+
const inputs = [];
|
|
1062
|
+
for (let ordinal = 0; ordinal < result.errors.length; ordinal++) {
|
|
1063
|
+
const e = result.errors[ordinal];
|
|
1064
|
+
const { frames, signatureHash } = processFailure(e);
|
|
1065
|
+
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1066
|
+
signatureHash,
|
|
1067
|
+
runId,
|
|
1068
|
+
seenAt: baseReport.timestamp
|
|
1069
|
+
});
|
|
1070
|
+
inputs.push({
|
|
1071
|
+
testCaseId,
|
|
1072
|
+
scope: "test",
|
|
1073
|
+
message: e.message,
|
|
1074
|
+
...e.name !== void 0 && { name: e.name },
|
|
1075
|
+
...e.diff !== void 0 && { diff: e.diff },
|
|
1076
|
+
...e.stack !== void 0 && { stack: e.stack },
|
|
1077
|
+
...signatureHash !== null && { signatureHash },
|
|
1078
|
+
...frames.length > 0 && { frames },
|
|
1079
|
+
ordinal
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
yield* store.writeErrors(runId, inputs);
|
|
1083
|
+
}
|
|
1084
|
+
testIdx++;
|
|
1085
|
+
}
|
|
1086
|
+
const modErrors = mod.errors();
|
|
1087
|
+
if (modErrors.length > 0) {
|
|
1088
|
+
const inputs = [];
|
|
1089
|
+
for (let ordinal = 0; ordinal < modErrors.length; ordinal++) {
|
|
1090
|
+
const e = modErrors[ordinal];
|
|
1091
|
+
const { frames, signatureHash } = processFailure(e);
|
|
1092
|
+
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1093
|
+
signatureHash,
|
|
1094
|
+
runId,
|
|
1095
|
+
seenAt: baseReport.timestamp
|
|
1096
|
+
});
|
|
1097
|
+
inputs.push({
|
|
1098
|
+
moduleId,
|
|
1099
|
+
scope: "module",
|
|
1100
|
+
message: e.message,
|
|
1101
|
+
...e.name !== void 0 && { name: e.name },
|
|
1102
|
+
...e.stack !== void 0 && { stack: e.stack },
|
|
1103
|
+
...signatureHash !== null && { signatureHash },
|
|
1104
|
+
...frames.length > 0 && { frames },
|
|
1105
|
+
ordinal
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
yield* store.writeErrors(runId, inputs);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (errors.length > 0) {
|
|
1112
|
+
const inputs = [];
|
|
1113
|
+
for (let ordinal = 0; ordinal < errors.length; ordinal++) {
|
|
1114
|
+
const e = errors[ordinal];
|
|
1115
|
+
const { frames, signatureHash } = processFailure(e);
|
|
1116
|
+
if (signatureHash !== null) yield* store.writeFailureSignature({
|
|
1117
|
+
signatureHash,
|
|
1118
|
+
runId,
|
|
1119
|
+
seenAt: baseReport.timestamp
|
|
1120
|
+
});
|
|
1121
|
+
inputs.push({
|
|
1122
|
+
scope: "unhandled",
|
|
1123
|
+
message: e.message,
|
|
1124
|
+
...e.name !== void 0 && { name: e.name },
|
|
1125
|
+
...e.stack !== void 0 && { stack: e.stack },
|
|
1126
|
+
...signatureHash !== null && { signatureHash },
|
|
1127
|
+
...frames.length > 0 && { frames },
|
|
1128
|
+
ordinal
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
yield* store.writeErrors(runId, inputs);
|
|
1132
|
+
}
|
|
1133
|
+
const testOutcomes = [];
|
|
1134
|
+
for (const mod of projectModules) for (const testCase of mod.children.allTests()) {
|
|
1135
|
+
const state = testCase.result()?.state;
|
|
1136
|
+
if (state === "passed" || state === "failed") testOutcomes.push({
|
|
1137
|
+
fullName: testCase.fullName,
|
|
1138
|
+
state
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
const { classifications } = yield* tracker.classify(project, testOutcomes, baseReport.timestamp);
|
|
1142
|
+
const diagMap = /* @__PURE__ */ new Map();
|
|
1143
|
+
const errorMap = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (const mod of projectModules) for (const tc of mod.children.allTests()) {
|
|
1145
|
+
diagMap.set(tc.fullName, tc.diagnostic() ?? {});
|
|
1146
|
+
const tcResult = tc.result();
|
|
1147
|
+
if (tcResult?.state === "failed") {
|
|
1148
|
+
const errors = tcResult.errors;
|
|
1149
|
+
errorMap.set(tc.fullName, errors?.[0]?.message ?? null);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
for (const outcome of testOutcomes) {
|
|
1153
|
+
const diag = diagMap.get(outcome.fullName);
|
|
1154
|
+
const errorMessage = outcome.state === "failed" ? errorMap.get(outcome.fullName) ?? null : null;
|
|
1155
|
+
yield* store.writeHistory(project, outcome.fullName, runId, baseReport.timestamp, outcome.state, diag?.duration ?? null, diag?.flaky ?? false, 0, errorMessage);
|
|
1156
|
+
}
|
|
1157
|
+
const failedWithClassifications = baseReport.failed.map((mod) => ({
|
|
1158
|
+
...mod,
|
|
1159
|
+
tests: mod.tests.map((test) => {
|
|
1160
|
+
const cls = classifications.get(test.fullName);
|
|
1161
|
+
return cls ? {
|
|
1162
|
+
...test,
|
|
1163
|
+
classification: cls
|
|
1164
|
+
} : test;
|
|
1165
|
+
})
|
|
1166
|
+
}));
|
|
1167
|
+
const classifiedReport = {
|
|
1168
|
+
...baseReport,
|
|
1169
|
+
failed: failedWithClassifications
|
|
1170
|
+
};
|
|
1171
|
+
const tagCounts = {};
|
|
1172
|
+
for (const mod of projectModules) for (const tc of mod.children.allTests()) {
|
|
1173
|
+
const state = tc.result()?.state ?? "pending";
|
|
1174
|
+
const tags = tc.tags ?? [];
|
|
1175
|
+
for (const tag of tags) {
|
|
1176
|
+
tagCounts[tag] ??= {
|
|
1177
|
+
passed: 0,
|
|
1178
|
+
failed: 0,
|
|
1179
|
+
skipped: 0
|
|
1180
|
+
};
|
|
1181
|
+
if (state === "passed") tagCounts[tag].passed++;
|
|
1182
|
+
else if (state === "failed") tagCounts[tag].failed++;
|
|
1183
|
+
else if (state === "skipped") tagCounts[tag].skipped++;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const reportWithTags = Object.keys(tagCounts).length > 0 ? {
|
|
1187
|
+
...classifiedReport,
|
|
1188
|
+
tagCounts
|
|
1189
|
+
} : classifiedReport;
|
|
1190
|
+
const report = coverageReport ? {
|
|
1191
|
+
...reportWithTags,
|
|
1192
|
+
coverage: coverageReport
|
|
1193
|
+
} : reportWithTags;
|
|
1194
|
+
reports.push(report);
|
|
1195
|
+
if (coverageReport) {
|
|
1196
|
+
const tierByFile = /* @__PURE__ */ new Map();
|
|
1197
|
+
const sourceByFile = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const fc of coverageReport.belowTarget ?? []) {
|
|
1199
|
+
tierByFile.set(fc.file, "below_target");
|
|
1200
|
+
sourceByFile.set(fc.file, fc);
|
|
1201
|
+
}
|
|
1202
|
+
for (const fc of coverageReport.lowCoverage) {
|
|
1203
|
+
tierByFile.set(fc.file, "below_threshold");
|
|
1204
|
+
sourceByFile.set(fc.file, fc);
|
|
1205
|
+
}
|
|
1206
|
+
if (tierByFile.size > 0) {
|
|
1207
|
+
const coverageInputs = [];
|
|
1208
|
+
for (const [file, tier] of tierByFile) {
|
|
1209
|
+
const fc = sourceByFile.get(file);
|
|
1210
|
+
if (!fc) continue;
|
|
1211
|
+
const fileId = yield* store.ensureFile(fc.file);
|
|
1212
|
+
coverageInputs.push({
|
|
1213
|
+
fileId,
|
|
1214
|
+
statements: fc.summary.statements,
|
|
1215
|
+
branches: fc.summary.branches,
|
|
1216
|
+
functions: fc.summary.functions,
|
|
1217
|
+
lines: fc.summary.lines,
|
|
1218
|
+
uncoveredLines: fc.uncoveredLines,
|
|
1219
|
+
tier
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
yield* store.writeCoverage(runId, coverageInputs);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (coverageReport && !coverageReport.scoped) {
|
|
1226
|
+
const existingTrends = yield* reader.getTrends(project).pipe(Effect.map((opt) => Option.getOrUndefined(opt)), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
1227
|
+
const updatedTrends = computeTrend(coverageReport.totals, existingTrends, opts.coverageTargets);
|
|
1228
|
+
const latestEntry = updatedTrends.entries[updatedTrends.entries.length - 1];
|
|
1229
|
+
if (latestEntry) yield* store.writeTrends(project, runId, latestEntry);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (opts.autoUpdate && coverageReport) {
|
|
1233
|
+
const newBaselines = computeUpdatedBaselines(baselines, coverageReport.totals, opts.coverageTargets);
|
|
1234
|
+
yield* store.writeBaselines(newBaselines);
|
|
1235
|
+
}
|
|
1236
|
+
let trendSummary;
|
|
1237
|
+
if (coverageReport && !coverageReport.scoped) {
|
|
1238
|
+
const firstProjectKey = Array.from(projectGroups.keys())[0];
|
|
1239
|
+
if (firstProjectKey) {
|
|
1240
|
+
const tp = firstProjectKey === "default" ? "default" : firstProjectKey;
|
|
1241
|
+
const trendsOpt = yield* reader.getTrends(tp).pipe(Effect.catchAll(() => Effect.succeed(Option.none())));
|
|
1242
|
+
if (Option.isSome(trendsOpt)) {
|
|
1243
|
+
const entries = trendsOpt.value.entries;
|
|
1244
|
+
if (entries.length >= 2) {
|
|
1245
|
+
const latest = entries[entries.length - 1];
|
|
1246
|
+
const prev = entries[entries.length - 2];
|
|
1247
|
+
const direction = latest.direction;
|
|
1248
|
+
const metrics = [
|
|
1249
|
+
"lines",
|
|
1250
|
+
"functions",
|
|
1251
|
+
"branches",
|
|
1252
|
+
"statements"
|
|
1253
|
+
];
|
|
1254
|
+
let firstMetric;
|
|
1255
|
+
for (const m of metrics) {
|
|
1256
|
+
const from = prev.coverage[m];
|
|
1257
|
+
const to = latest.coverage[m];
|
|
1258
|
+
if (from !== to) {
|
|
1259
|
+
const target = opts.coverageTargets?.global?.[m];
|
|
1260
|
+
firstMetric = {
|
|
1261
|
+
name: m,
|
|
1262
|
+
from,
|
|
1263
|
+
to,
|
|
1264
|
+
...target !== void 0 ? { target } : {}
|
|
1265
|
+
};
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
trendSummary = {
|
|
1270
|
+
direction,
|
|
1271
|
+
runCount: entries.length,
|
|
1272
|
+
...firstMetric ? { firstMetric } : {}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (wantsRunEvents && trendSummary !== void 0) emitEvent({
|
|
1279
|
+
_tag: "TrendComputed",
|
|
1280
|
+
direction: trendSummary.direction,
|
|
1281
|
+
runCount: trendSummary.runCount
|
|
1282
|
+
});
|
|
1283
|
+
yield* Effect.logInfo("reports built").pipe(Effect.annotateLogs({
|
|
1284
|
+
count: reports.length,
|
|
1285
|
+
projects: Array.from(projectGroups.keys()).join(", ")
|
|
1286
|
+
}));
|
|
1287
|
+
const detector = yield* EnvironmentDetector;
|
|
1288
|
+
const executorResolver = yield* ExecutorResolver;
|
|
1289
|
+
const formatSelector = yield* FormatSelector;
|
|
1290
|
+
const detailResolver = yield* DetailResolver;
|
|
1291
|
+
const env = yield* detector.detect();
|
|
1292
|
+
const executor = yield* executorResolver.resolve(env);
|
|
1293
|
+
const format = yield* formatSelector.select(executor, opts.format, env);
|
|
1294
|
+
const health = {
|
|
1295
|
+
hasFailures: reports.some((r) => r.summary.failed > 0 || r.unhandledErrors.length > 0),
|
|
1296
|
+
belowTargets: reports.some((r) => {
|
|
1297
|
+
return (r.coverage?.belowTarget?.length ?? 0) > 0;
|
|
1298
|
+
}),
|
|
1299
|
+
hasTargets: !!opts.coverageTargets
|
|
1300
|
+
};
|
|
1301
|
+
const detail = yield* detailResolver.resolve(executor, health, opts.detail);
|
|
1302
|
+
yield* Effect.logDebug("pipeline resolved").pipe(Effect.annotateLogs({
|
|
1303
|
+
env,
|
|
1304
|
+
executor,
|
|
1305
|
+
format,
|
|
1306
|
+
detail
|
|
1307
|
+
}));
|
|
1308
|
+
const classifications = /* @__PURE__ */ new Map();
|
|
1309
|
+
for (const report of reports) for (const mod of report.failed) for (const test of mod.tests) if (test.classification) classifications.set(test.fullName, test.classification);
|
|
1310
|
+
const githubSummaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
1311
|
+
const kit = buildReporterKit({
|
|
1312
|
+
env,
|
|
1313
|
+
executor,
|
|
1314
|
+
format,
|
|
1315
|
+
detail,
|
|
1316
|
+
noColor: !!process.env.NO_COLOR,
|
|
1317
|
+
consoleMode: opts.consoleMode ?? "passthrough",
|
|
1318
|
+
mcp: opts.mcp ?? false,
|
|
1319
|
+
githubActions: opts.githubActions,
|
|
1320
|
+
transport: opts.transport,
|
|
1321
|
+
runEvents,
|
|
1322
|
+
...dbPath !== void 0 && { dbPath },
|
|
1323
|
+
...opts.projectFilter !== void 0 && { projectFilter: opts.projectFilter },
|
|
1324
|
+
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
1325
|
+
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
1326
|
+
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
1327
|
+
coverageMode: opts.coverageMode
|
|
1328
|
+
});
|
|
1329
|
+
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1330
|
+
const renderInput = {
|
|
1331
|
+
reports,
|
|
1332
|
+
classifications,
|
|
1333
|
+
...trendSummary !== void 0 && { trendSummary }
|
|
1334
|
+
};
|
|
1335
|
+
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
1336
|
+
yield* Effect.logDebug("reporters rendered").pipe(Effect.annotateLogs({
|
|
1337
|
+
reporterCount: reporters.length,
|
|
1338
|
+
outputs: allOutputs.length
|
|
1339
|
+
}));
|
|
1340
|
+
for (const output of allOutputs) routeRenderedOutput(output, { ...githubSummaryFile !== void 0 && { githubSummaryFile } });
|
|
1341
|
+
});
|
|
1342
|
+
await Effect.runPromise(program.pipe(Effect.annotateLogs("service", "reporter"), Effect.provide(ReporterLive(dbPath, logLevel, logFile)))).catch((err) => {
|
|
1343
|
+
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
//#endregion
|
|
1349
|
+
export { AgentReporter };
|