@vitest-agent/plugin 5.4.8 → 5.5.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 +109 -12
- package/index.js +2 -2
- package/layers/CoverageAnalyzerLive.js +24 -1
- package/package.json +8 -8
- package/plugin.js +11 -1
- package/reporter.js +144 -71
- package/utils/build-reporter-kit.js +1 -0
- package/utils/discover-projects.js +41 -3
- package/utils/resolve-persistence.js +61 -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, ResolvedReporterConfig, ResolvedThresholds, RunEvent, Transport, VitestAgentReporterFactory, resolveCoverageInput, validateCoverageConfig } from "@vitest-agent/sdk";
|
|
3
|
+
import { AgentPluginOptions, AgentReporterOptions, ConsoleMode, CoverageBaselines, CoverageInput, CoverageLevel, CoverageLevelName, CoverageLevelName as CoverageLevelName$1, CoverageReport, ResolvedReporterConfig, ResolvedThresholds, RunEvent, RunRetentionPolicy, 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";
|
|
@@ -263,7 +263,51 @@ interface DiscoverProjectsOptions {
|
|
|
263
263
|
* Forwarded to `@effected/workspaces`' `getWorkspacePackagesSync`.
|
|
264
264
|
*/
|
|
265
265
|
readonly maxDepth?: number;
|
|
266
|
+
/**
|
|
267
|
+
* Per-project `test` overrides keyed by project name (the workspace
|
|
268
|
+
* package name, or an `additionalEntries` name). Each value is shallow-merged
|
|
269
|
+
* over the generated project's `test` block after the strategy has built it;
|
|
270
|
+
* `name` is never replaced. A key naming no discovered project throws, so a
|
|
271
|
+
* typo cannot silently leave a project on its defaults. Supplying this
|
|
272
|
+
* bypasses the process-level result cache.
|
|
273
|
+
*/
|
|
274
|
+
readonly configure?: ProjectConfigureMap;
|
|
266
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* The Vitest `test` fields a `configure` entry may set on one generated
|
|
278
|
+
* project — everything Vitest accepts on a project's inline `test` block
|
|
279
|
+
* except `name`, which stays the discovered project's own.
|
|
280
|
+
*
|
|
281
|
+
* @remarks
|
|
282
|
+
* The typical use is pool scheduling per project: `{ isolate: false }` for a
|
|
283
|
+
* package whose suites share no module-level state, while a package that
|
|
284
|
+
* spawns processes or calls `process.chdir()` keeps the root's forks plus
|
|
285
|
+
* isolation (issue #626). The merge is shallow: an `include` or `exclude` here
|
|
286
|
+
* replaces the strategy's list rather than appending to it.
|
|
287
|
+
* @public
|
|
288
|
+
*/
|
|
289
|
+
type ProjectTestOverrides = Omit<NonNullable<TestProjectInlineConfiguration["test"]>, "name">;
|
|
290
|
+
/**
|
|
291
|
+
* Project name to {@link ProjectTestOverrides}, as passed to `configure`.
|
|
292
|
+
* @public
|
|
293
|
+
*/
|
|
294
|
+
type ProjectConfigureMap = Readonly<Record<string, ProjectTestOverrides>>;
|
|
295
|
+
/**
|
|
296
|
+
* Shallow-merges each `overrides` entry into the generated project whose
|
|
297
|
+
* `test.name` equals the entry's key. Pure: returns new project objects for the
|
|
298
|
+
* configured names and the original objects for the rest; the discovered
|
|
299
|
+
* `name` always wins over one carried by the override. Only a string
|
|
300
|
+
* `test.name` can be keyed; a project named through Vitest's object form is
|
|
301
|
+
* left untouched (and a key for it is reported as unknown).
|
|
302
|
+
*
|
|
303
|
+
* Throws when a key names no project in `configs`, listing the discovered
|
|
304
|
+
* names so the typo is visible at config-load time.
|
|
305
|
+
* @param configs - The projects a strategy produced
|
|
306
|
+
* @param overrides - Per-project `test` overrides keyed by project name
|
|
307
|
+
* @returns The projects with overrides applied
|
|
308
|
+
* @public
|
|
309
|
+
*/
|
|
310
|
+
export declare function applyProjectOverrides(configs: ReadonlyArray<TestProjectInlineConfiguration>, overrides: ProjectConfigureMap): TestProjectInlineConfiguration[];
|
|
267
311
|
/**
|
|
268
312
|
* Scan all workspace packages and additional entries through the active strategy and return projects + tags.
|
|
269
313
|
* @param options - Optional strategy, working directory, and extra project entries
|
|
@@ -464,16 +508,28 @@ export declare namespace AgentPlugin {
|
|
|
464
508
|
* };
|
|
465
509
|
* ```
|
|
466
510
|
*
|
|
511
|
+
* Per-project `test` overrides ride the options object's `configure` map,
|
|
512
|
+
* keyed by project name and shallow-merged over the generated project's
|
|
513
|
+
* `test` block (`name` is never replaced); an unknown key throws. The
|
|
514
|
+
* typical use is per-package pool scheduling (issue #626):
|
|
515
|
+
*
|
|
516
|
+
* ```ts
|
|
517
|
+
* const { projects, tags } = await AgentPlugin.discover({
|
|
518
|
+
* configure: { "@acme/pure-lib": { isolate: false } },
|
|
519
|
+
* });
|
|
520
|
+
* ```
|
|
521
|
+
*
|
|
467
522
|
* @param strategy - Optional strategy or options object
|
|
468
|
-
* `{ strategy?, cwd?, maxDepth? }`.
|
|
523
|
+
* `{ strategy?, cwd?, maxDepth?, configure? }`.
|
|
469
524
|
* Pass a {@link DiscoverStrategy} directly, or an options object to also
|
|
470
|
-
* specify a custom workspace root via `cwd
|
|
471
|
-
* `maxDepth`.
|
|
525
|
+
* specify a custom workspace root via `cwd`, deeper package traversal via
|
|
526
|
+
* `maxDepth`, and per-project `test` overrides via `configure`.
|
|
472
527
|
*/
|
|
473
528
|
function discover(strategy?: DiscoverStrategy | {
|
|
474
529
|
strategy?: DiscoverStrategy;
|
|
475
530
|
cwd?: string;
|
|
476
531
|
maxDepth?: number;
|
|
532
|
+
configure?: ProjectConfigureMap;
|
|
477
533
|
}): DiscoverBuilder;
|
|
478
534
|
/**
|
|
479
535
|
* Run a shell command, suppressing all output unless the command fails.
|
|
@@ -545,6 +601,18 @@ interface AgentReporterConstructorOptions extends AgentReporterOptions {
|
|
|
545
601
|
* @internal
|
|
546
602
|
*/
|
|
547
603
|
coverageMode?: "full" | "ui-only";
|
|
604
|
+
/**
|
|
605
|
+
* Whether the run is recorded in SQLite (issue #625). The plugin
|
|
606
|
+
* resolves it from `AgentPlugin({ persist })` and the
|
|
607
|
+
* `VITEST_AGENT_PERSIST` operator override; defaults to `true` when
|
|
608
|
+
* `AgentReporter` is constructed directly. `false` routes
|
|
609
|
+
* `onTestRunEnd` through the same DB-free path UI-only mode uses —
|
|
610
|
+
* every render and `RunEvent` still happens, coverage is still
|
|
611
|
+
* analyzed, and `data.db` is never resolved, created or opened.
|
|
612
|
+
*
|
|
613
|
+
* @internal
|
|
614
|
+
*/
|
|
615
|
+
persist?: boolean;
|
|
548
616
|
consoleMode?: ConsoleMode;
|
|
549
617
|
mcp?: boolean;
|
|
550
618
|
githubActions?: boolean;
|
|
@@ -574,6 +642,17 @@ interface AgentReporterConstructorOptions extends AgentReporterOptions {
|
|
|
574
642
|
* @internal
|
|
575
643
|
*/
|
|
576
644
|
cacheDir?: string;
|
|
645
|
+
/**
|
|
646
|
+
* Run-retention override (issue 623). A `RunRetentionPolicy` is applied
|
|
647
|
+
* as given; `false` disables run pruning for this reporter; absent means
|
|
648
|
+
* the policy the `[retention]` table of `vitest-agent.config.toml`
|
|
649
|
+
* resolves to (the default of `RunRetentionPolicy.DEFAULT_KEEP_RUNS`
|
|
650
|
+
* newest runs per project when the table is absent). Takes precedence
|
|
651
|
+
* over the TOML file, like `cacheDir` does for the database path.
|
|
652
|
+
*
|
|
653
|
+
* @internal
|
|
654
|
+
*/
|
|
655
|
+
retention?: RunRetentionPolicy | false;
|
|
577
656
|
}
|
|
578
657
|
/**
|
|
579
658
|
* Vitest Reporter that produces structured output for LLM coding agents.
|
|
@@ -628,14 +707,17 @@ export declare class AgentReporter {
|
|
|
628
707
|
private options;
|
|
629
708
|
private dbPath;
|
|
630
709
|
/**
|
|
631
|
-
* Set to `true`
|
|
710
|
+
* Set to `true` once `onTestRunEnd` has done this run's work.
|
|
632
711
|
*
|
|
633
|
-
* Vitest can fire `onTestRunEnd` more than once per
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
712
|
+
* Vitest can fire `onTestRunEnd` more than once per run in some
|
|
713
|
+
* multi-project configurations (e.g., once per project group, with the
|
|
714
|
+
* same `testModules` array each time). The first call does the work and
|
|
715
|
+
* the flag prevents subsequent invocations from re-rendering and
|
|
716
|
+
* re-writing to the DB. The flag is PER RUN, not per instance: Vitest
|
|
717
|
+
* keeps one reporter instance alive across watch-mode reruns, so
|
|
718
|
+
* `onTestRunStart` re-arms it — otherwise every rerun after the first
|
|
719
|
+
* would silently skip persistence, rendering and `RunFinished`
|
|
720
|
+
* (issue #576).
|
|
639
721
|
*/
|
|
640
722
|
private rendered;
|
|
641
723
|
/**
|
|
@@ -1270,6 +1352,21 @@ interface CoverageOptions {
|
|
|
1270
1352
|
* production; forward or back slashes both work).
|
|
1271
1353
|
*/
|
|
1272
1354
|
readonly root?: string;
|
|
1355
|
+
/**
|
|
1356
|
+
* Vitest's resolved `coverage.include` globs. When set, a coverage-map
|
|
1357
|
+
* key matching none of them is dropped before any per-file or glob
|
|
1358
|
+
* evaluation (issue #622). Matched like threshold patterns: against
|
|
1359
|
+
* `relative(root, key)` when `root` is set, verbatim otherwise. Absent,
|
|
1360
|
+
* every key is kept. A path under a `node_modules` segment is dropped
|
|
1361
|
+
* regardless of this option.
|
|
1362
|
+
*/
|
|
1363
|
+
readonly include?: ReadonlyArray<string>;
|
|
1364
|
+
/**
|
|
1365
|
+
* Vitest's resolved `coverage.exclude` globs. A coverage-map key matching
|
|
1366
|
+
* any of them is dropped before evaluation (issue #622), with the same
|
|
1367
|
+
* matching rule as `include`.
|
|
1368
|
+
*/
|
|
1369
|
+
readonly exclude?: ReadonlyArray<string>;
|
|
1273
1370
|
}
|
|
1274
1371
|
declare const CoverageAnalyzer_base: Context.ServiceClass<CoverageAnalyzer, "vitest-agent/CoverageAnalyzer", {
|
|
1275
1372
|
readonly process: (coverage: unknown, options: CoverageOptions) => Effect.Effect<Option.Option<CoverageReport>>;
|
|
@@ -1490,5 +1587,5 @@ export declare const COVERAGE_AUTOUPDATE: Readonly<{
|
|
|
1490
1587
|
strict: (next: number, previous: number) => number;
|
|
1491
1588
|
lenient: (next: number, previous: number) => number;
|
|
1492
1589
|
}>;
|
|
1493
|
-
export { type AddProjectInput, type AgentPluginConstructorOptions, type AgentReporterConstructorOptions, type ClassifyContext, type ClassifyFn, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, type WalkerEntry, type WalkerEntryStat, type WalkerFileSystem, resolveCoverageInput, validateCoverageConfig };
|
|
1590
|
+
export { type AddProjectInput, type AgentPluginConstructorOptions, type AgentReporterConstructorOptions, type ClassifyContext, type ClassifyFn, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, type ProjectConfigureMap, type ProjectTestOverrides, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, type WalkerEntry, type WalkerEntryStat, type WalkerFileSystem, resolveCoverageInput, validateCoverageConfig };
|
|
1494
1591
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { nodeWalkerFs } from "./utils/walker-fs.js";
|
|
|
13
13
|
import { findTestFiles } from "./utils/find-test-files.js";
|
|
14
14
|
import { Tag } from "./utils/tag.js";
|
|
15
15
|
import { DefaultDiscoverStrategy, DiscoverStrategy } from "./utils/discover-strategy.js";
|
|
16
|
-
import { discoverProjects } from "./utils/discover-projects.js";
|
|
16
|
+
import { applyProjectOverrides, discoverProjects } from "./utils/discover-projects.js";
|
|
17
17
|
import { CONSOLE_REPORTERS, stripConsoleReporters } from "./utils/strip-console-reporters.js";
|
|
18
18
|
import { CURRENT_PLUGIN_VERSION } from "./version.js";
|
|
19
19
|
import { AgentPlugin } from "./plugin.js";
|
|
@@ -30,4 +30,4 @@ const COVERAGE_LEVELS_PER_FILE = AgentPlugin.COVERAGE_LEVELS_PER_FILE;
|
|
|
30
30
|
/** Auto-update tolerance functions for `coverage.thresholds.autoUpdate`. Mirrors `AgentPlugin.COVERAGE_AUTOUPDATE`. @public */
|
|
31
31
|
const COVERAGE_AUTOUPDATE = AgentPlugin.COVERAGE_AUTOUPDATE;
|
|
32
32
|
|
|
33
|
-
export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, ConfigurationError, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
|
33
|
+
export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, ConfigurationError, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, applyProjectOverrides, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
|
@@ -104,6 +104,28 @@ function matchGlob(filePath, pattern) {
|
|
|
104
104
|
return compiled?.matches(filePath) ?? false;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
+
* True when a path has a `node_modules` directory segment anywhere in it,
|
|
108
|
+
* under either separator. A dependency is never one of the project's own
|
|
109
|
+
* sources, whatever Vitest's untested-files glob swept into the map
|
|
110
|
+
* (issue #622: `rolldown-pnpm-config` under `node_modules` reached
|
|
111
|
+
* `file_coverage` on spencerbeggs/effected).
|
|
112
|
+
*/
|
|
113
|
+
function isNodeModulesPath(filePath) {
|
|
114
|
+
return /(?:^|\/)node_modules(?:\/|$)/.test(toPosixPath(filePath));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Decide whether a coverage-map key is one of the project's sources: not
|
|
118
|
+
* under `node_modules`, inside `include` when given, and outside `exclude`
|
|
119
|
+
* when given. `matchPath` is the glob-matching form of the key (see
|
|
120
|
+
* `CoverageOptions.root`).
|
|
121
|
+
*/
|
|
122
|
+
function isProjectSource(filePath, matchPath, options) {
|
|
123
|
+
if (isNodeModulesPath(filePath)) return false;
|
|
124
|
+
const { include, exclude } = options;
|
|
125
|
+
if (include !== void 0 && include.length > 0 && !include.some((pattern) => matchGlob(matchPath, pattern))) return false;
|
|
126
|
+
return !exclude?.some((pattern) => matchGlob(matchPath, pattern));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
107
129
|
* Resolve the effective thresholds for a file path by checking pattern
|
|
108
130
|
* overrides first, falling back to global thresholds.
|
|
109
131
|
*/
|
|
@@ -213,6 +235,7 @@ function processCoverageInternal(coverageMap, options, testedFiles) {
|
|
|
213
235
|
const mapSummaries = [];
|
|
214
236
|
for (const filePath of coverageMap.files()) {
|
|
215
237
|
const matchPath = matchKey(filePath);
|
|
238
|
+
if (!isProjectSource(filePath, matchPath, options)) continue;
|
|
216
239
|
const fileCoverage = coverageMap.fileCoverageFor(filePath);
|
|
217
240
|
const fileSummary = fileCoverage.toSummary();
|
|
218
241
|
mapSummaries.push({
|
|
@@ -293,4 +316,4 @@ const CoverageAnalyzerLive = Layer.succeed(CoverageAnalyzer, {
|
|
|
293
316
|
})
|
|
294
317
|
});
|
|
295
318
|
|
|
296
|
-
export { CoverageAnalyzerLive };
|
|
319
|
+
export { CoverageAnalyzerLive, isNodeModulesPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/plugin",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.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": [
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@effect/platform-node": "^4.0.6",
|
|
47
|
-
"@effected/cli": "^0.
|
|
47
|
+
"@effected/cli": "^0.22.0",
|
|
48
48
|
"@effected/env": "^0.5.0",
|
|
49
49
|
"@effected/glob": "^0.14.0",
|
|
50
50
|
"@effected/walker": "^0.19.0",
|
|
51
|
-
"@effected/workspaces": "^0.
|
|
52
|
-
"@vitest-agent/cli": "4.0
|
|
53
|
-
"@vitest-agent/engine": "1.
|
|
54
|
-
"@vitest-agent/mcp": "5.2.
|
|
55
|
-
"@vitest-agent/reporter": "5.0.
|
|
56
|
-
"@vitest-agent/sdk": "7.0
|
|
51
|
+
"@effected/workspaces": "^0.38.0",
|
|
52
|
+
"@vitest-agent/cli": "4.1.0",
|
|
53
|
+
"@vitest-agent/engine": "1.2.0",
|
|
54
|
+
"@vitest-agent/mcp": "5.2.10",
|
|
55
|
+
"@vitest-agent/reporter": "5.0.9",
|
|
56
|
+
"@vitest-agent/sdk": "7.1.0",
|
|
57
57
|
"effect": "^4.0.6",
|
|
58
58
|
"magic-string": "^1.4.3"
|
|
59
59
|
},
|
package/plugin.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ensureGithubActionsReporter } from "./utils/ensure-github-reporter.js";
|
|
|
12
12
|
import { injectTags } from "./utils/inject-tags.js";
|
|
13
13
|
import { isBenignViteSourceMapWarning } from "./utils/is-benign-vite-source-map-warning.js";
|
|
14
14
|
import { resolveCoverageDirIsolation } from "./utils/resolve-coverage-dir-isolation.js";
|
|
15
|
+
import { PERSIST_ENV_VAR, resolvePersistence } from "./utils/resolve-persistence.js";
|
|
15
16
|
import { DEFAULT_BUILT_RECENTLY_MS, DEFAULT_LOCK_STALE_MS, DEFAULT_LOCK_WAIT_TIMEOUT_MS, acquireRunScriptLock, markRunScriptDone, parseLockTimingOverride, releaseRunScriptLock } from "./utils/run-script-lock.js";
|
|
16
17
|
import { stripConsoleReporters } from "./utils/strip-console-reporters.js";
|
|
17
18
|
import { makeTagCacheKeyGenerator } from "./utils/tag-cache-key.js";
|
|
@@ -275,6 +276,13 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
275
276
|
const rawTargets = options.coverageTargets;
|
|
276
277
|
const coverageTargets = rawTargets ? resolveThresholds(rawTargets) : void 0;
|
|
277
278
|
const coverageMode = coverageConfig?.enabled === false ? "ui-only" : "full";
|
|
279
|
+
const persistence = resolvePersistence({
|
|
280
|
+
option: options.persist,
|
|
281
|
+
env: process.env
|
|
282
|
+
});
|
|
283
|
+
if (persistence.ignoredEnvValue !== void 0) reportOnce(`[vitest-agent:plugin] ignoring ${PERSIST_ENV_VAR}=${persistence.ignoredEnvValue}: expected 0|false|off or 1|true|on\n`);
|
|
284
|
+
const persist = persistence.persist;
|
|
285
|
+
log("coverageMode:", coverageMode, "| persist:", persist, `(${persistence.source})`);
|
|
278
286
|
if (coverageConfig && !coverageDirDecidedByVitest.has(vitest)) {
|
|
279
287
|
coverageDirDecidedByVitest.add(vitest);
|
|
280
288
|
const dirDecision = resolveCoverageDirIsolation({
|
|
@@ -313,6 +321,7 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
313
321
|
...coverageThresholds !== void 0 ? { coverageThresholds } : {},
|
|
314
322
|
...coverageTargets !== void 0 ? { coverageTargets } : {},
|
|
315
323
|
coverageMode,
|
|
324
|
+
persist,
|
|
316
325
|
consoleMode,
|
|
317
326
|
mcp,
|
|
318
327
|
githubActions,
|
|
@@ -417,7 +426,8 @@ function makeDiscoverBuilder(options) {
|
|
|
417
426
|
return makeDiscoverBuilder({
|
|
418
427
|
...opts.strategy !== void 0 ? { strategy: opts.strategy } : {},
|
|
419
428
|
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
420
|
-
...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {}
|
|
429
|
+
...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {},
|
|
430
|
+
...opts.configure !== void 0 ? { configure: opts.configure } : {}
|
|
421
431
|
});
|
|
422
432
|
}
|
|
423
433
|
_AgentPlugin.discover = discover;
|
package/reporter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveThresholds } from "./utils/resolve-thresholds.js";
|
|
2
2
|
import { CoverageAnalyzer } from "./services/CoverageAnalyzer.js";
|
|
3
|
+
import { CoverageAnalyzerLive } from "./layers/CoverageAnalyzerLive.js";
|
|
3
4
|
import { ReporterLive } from "./layers/ReporterLive.js";
|
|
4
5
|
import { buildReporterKit, normalizeReporters } from "./utils/build-reporter-kit.js";
|
|
5
6
|
import { captureEnvVars } from "./utils/capture-env.js";
|
|
@@ -13,8 +14,8 @@ import { strayOutputCaptureOf } from "./utils/stray-output-capture.js";
|
|
|
13
14
|
import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
|
|
14
15
|
import { mkdirSync, statSync } from "node:fs";
|
|
15
16
|
import { dirname } from "node:path";
|
|
16
|
-
import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, HistoryTracker, OutputPipelineLive, PathResolutionLive, ensureMigrated, historyKey, resolveDataPath, resolveLogFile, resolveLogLevel, withPersistTransaction } from "@vitest-agent/engine";
|
|
17
|
-
import { buildAgentReport, coerceErrorField, computeTrend, formatFatalError, isTimeoutError, probeHostMetadataFromEnv } from "@vitest-agent/sdk";
|
|
17
|
+
import { ConfigLive, DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, HistoryTracker, OutputPipelineLive, PathResolutionLive, VitestAgentConfigFile, ensureMigrated, historyKey, resolveDataPath, resolveLogFile, resolveLogLevel, withPersistTransaction } from "@vitest-agent/engine";
|
|
18
|
+
import { RunRetentionPolicy, VitestAgentConfig, buildAgentReport, coerceErrorField, computeTrend, formatFatalError, isTimeoutError, probeHostMetadataFromEnv } from "@vitest-agent/sdk";
|
|
18
19
|
import { Effect, Option, PubSub } from "effect";
|
|
19
20
|
import { randomUUID } from "node:crypto";
|
|
20
21
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
@@ -73,6 +74,36 @@ function resolveDataPathCached(projectDir) {
|
|
|
73
74
|
return promise;
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
77
|
+
* Cache of the run-retention policy resolved from `vitest-agent.config.toml`,
|
|
78
|
+
* keyed by `projectDir` for the same reason as `dbPathCache`: one load per
|
|
79
|
+
* process, however many reporter instances a multi-project config builds.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
const retentionPolicyCache = /* @__PURE__ */ new Map();
|
|
84
|
+
/**
|
|
85
|
+
* The run-retention policy the `[retention]` table of
|
|
86
|
+
* `vitest-agent.config.toml` asks for (issue 623): the default policy when
|
|
87
|
+
* the file or table is absent, `Option.none()` when `retention.enabled` is
|
|
88
|
+
* `false`. Loads the config through `ConfigLive(projectDir)` alone — the
|
|
89
|
+
* light half of `PathResolutionLive`, without the workspace scan — so a
|
|
90
|
+
* programmatic `cacheDir` still gets the configured policy.
|
|
91
|
+
*
|
|
92
|
+
* A config that cannot be read resolves to `Option.none()`: the prune is
|
|
93
|
+
* skipped for this run rather than applied under a policy nobody chose, and
|
|
94
|
+
* the same unreadable file already surfaces through `resolveDataPath`.
|
|
95
|
+
*/
|
|
96
|
+
function resolveRetentionPolicyCached(projectDir) {
|
|
97
|
+
const cached = retentionPolicyCache.get(projectDir);
|
|
98
|
+
if (cached !== void 0) return cached;
|
|
99
|
+
const promise = Effect.runPromise(Effect.gen(function* () {
|
|
100
|
+
const config = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({}));
|
|
101
|
+
return RunRetentionPolicy.fromConfig(config);
|
|
102
|
+
}).pipe(Effect.provide(ConfigLive(projectDir)), Effect.provide(NodeServices.layer), Effect.catch(() => Effect.succeed(Option.none()))));
|
|
103
|
+
retentionPolicyCache.set(projectDir, promise);
|
|
104
|
+
return promise;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
76
107
|
* Compute updated baselines using ratchet logic: take the max of actual vs previous,
|
|
77
108
|
* capped by targets if set.
|
|
78
109
|
*
|
|
@@ -300,14 +331,17 @@ var AgentReporter = class {
|
|
|
300
331
|
options;
|
|
301
332
|
dbPath = null;
|
|
302
333
|
/**
|
|
303
|
-
* Set to `true`
|
|
334
|
+
* Set to `true` once `onTestRunEnd` has done this run's work.
|
|
304
335
|
*
|
|
305
|
-
* Vitest can fire `onTestRunEnd` more than once per
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
336
|
+
* Vitest can fire `onTestRunEnd` more than once per run in some
|
|
337
|
+
* multi-project configurations (e.g., once per project group, with the
|
|
338
|
+
* same `testModules` array each time). The first call does the work and
|
|
339
|
+
* the flag prevents subsequent invocations from re-rendering and
|
|
340
|
+
* re-writing to the DB. The flag is PER RUN, not per instance: Vitest
|
|
341
|
+
* keeps one reporter instance alive across watch-mode reruns, so
|
|
342
|
+
* `onTestRunStart` re-arms it — otherwise every rerun after the first
|
|
343
|
+
* would silently skip persistence, rendering and `RunFinished`
|
|
344
|
+
* (issue #576).
|
|
311
345
|
*/
|
|
312
346
|
rendered = false;
|
|
313
347
|
/**
|
|
@@ -443,6 +477,7 @@ var AgentReporter = class {
|
|
|
443
477
|
const githubActions = options.githubActions ?? false;
|
|
444
478
|
const base = {
|
|
445
479
|
...options.cacheDir !== void 0 ? { cacheDir: options.cacheDir } : {},
|
|
480
|
+
...options.retention !== void 0 ? { retention: options.retention } : {},
|
|
446
481
|
consoleOutput,
|
|
447
482
|
omitPassingTests: true,
|
|
448
483
|
coverageThresholds: resolvedThresholds,
|
|
@@ -458,6 +493,7 @@ var AgentReporter = class {
|
|
|
458
493
|
...options.projectFilter !== void 0 ? { projectFilter: options.projectFilter } : {},
|
|
459
494
|
reporter: options.reporter ?? DefaultVitestAgentReporter,
|
|
460
495
|
coverageMode: options.coverageMode ?? "full",
|
|
496
|
+
persist: options.persist ?? true,
|
|
461
497
|
transport: options.transport ?? { kind: "local" },
|
|
462
498
|
...options.passWithNoTests !== void 0 ? { passWithNoTests: options.passWithNoTests } : {}
|
|
463
499
|
};
|
|
@@ -488,6 +524,7 @@ var AgentReporter = class {
|
|
|
488
524
|
detail: opts.detail ?? "standard",
|
|
489
525
|
noColor: false,
|
|
490
526
|
coverageMode: opts.coverageMode,
|
|
527
|
+
persist: opts.persist,
|
|
491
528
|
transport: opts.transport,
|
|
492
529
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
493
530
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
@@ -510,7 +547,7 @@ var AgentReporter = class {
|
|
|
510
547
|
assertReportCapable(vitest);
|
|
511
548
|
this.reportWriter = createReportWriter(vitest, this.options.reportScope);
|
|
512
549
|
}
|
|
513
|
-
try {
|
|
550
|
+
if (this.options.persist) try {
|
|
514
551
|
await this.ensureDbPath();
|
|
515
552
|
} catch {}
|
|
516
553
|
await this.initReporters();
|
|
@@ -618,7 +655,8 @@ var AgentReporter = class {
|
|
|
618
655
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
619
656
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
620
657
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
621
|
-
coverageMode: opts.coverageMode
|
|
658
|
+
coverageMode: opts.coverageMode,
|
|
659
|
+
persist: opts.persist
|
|
622
660
|
});
|
|
623
661
|
this.reporters = normalizeReporters(opts.reporter(kit));
|
|
624
662
|
} catch (err) {
|
|
@@ -678,6 +716,8 @@ var AgentReporter = class {
|
|
|
678
716
|
* `RunStarted` event for live subscribers.
|
|
679
717
|
*/
|
|
680
718
|
onTestRunStart(_specifications) {
|
|
719
|
+
this.rendered = false;
|
|
720
|
+
this.coverage = null;
|
|
681
721
|
this.startedSpecCount = _specifications.length;
|
|
682
722
|
this.strayCapture?.reset();
|
|
683
723
|
if (this.neutralizedThresholdSnapshot.size > 0) {
|
|
@@ -1173,14 +1213,84 @@ var AgentReporter = class {
|
|
|
1173
1213
|
const wantsRunEvents = this.wantsRunEvents();
|
|
1174
1214
|
let dbPath;
|
|
1175
1215
|
let persistDisabled;
|
|
1176
|
-
try {
|
|
1216
|
+
if (opts.persist) try {
|
|
1177
1217
|
dbPath = await this.ensureDbPath();
|
|
1178
1218
|
} catch (err) {
|
|
1179
1219
|
persistDisabled = formatFatalError(err);
|
|
1180
1220
|
}
|
|
1221
|
+
const retention = dbPath === void 0 || persistDisabled !== void 0 ? Option.none() : opts.retention === false ? Option.none() : opts.retention !== void 0 ? Option.some(opts.retention) : await resolveRetentionPolicyCached(process.cwd());
|
|
1181
1222
|
const filteredModules = opts.projectFilter ? modules.filter((m) => (m.project.name || "default") === opts.projectFilter) : modules;
|
|
1182
1223
|
if (filteredModules.length === 0 && opts.projectFilter) return;
|
|
1183
|
-
|
|
1224
|
+
const coverageRoot = (() => {
|
|
1225
|
+
const root = stashedVitest?.config?.root;
|
|
1226
|
+
return typeof root === "string" ? root : void 0;
|
|
1227
|
+
})();
|
|
1228
|
+
const coverageGlobs = (() => {
|
|
1229
|
+
const coverage = stashedVitest?.config?.coverage;
|
|
1230
|
+
const asGlobs = (value) => Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
|
|
1231
|
+
const record = typeof coverage === "object" && coverage !== null ? coverage : {};
|
|
1232
|
+
const include = asGlobs(record.include);
|
|
1233
|
+
const exclude = asGlobs(record.exclude);
|
|
1234
|
+
return {
|
|
1235
|
+
...include !== void 0 ? { include } : {},
|
|
1236
|
+
...exclude !== void 0 ? { exclude } : {}
|
|
1237
|
+
};
|
|
1238
|
+
})();
|
|
1239
|
+
const analyzeCoverage = (baselines) => Effect.gen(function* () {
|
|
1240
|
+
const analyzer = yield* CoverageAnalyzer;
|
|
1241
|
+
const projectModuleCounts = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const m of modules) {
|
|
1243
|
+
const key = m.project.name || "default";
|
|
1244
|
+
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
1245
|
+
}
|
|
1246
|
+
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1247
|
+
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1248
|
+
const coverageOpts = {
|
|
1249
|
+
thresholds: opts.coverageThresholds,
|
|
1250
|
+
includeBareZero: opts.includeBareZero,
|
|
1251
|
+
...opts.coverageTargets ? { targets: opts.coverageTargets } : {},
|
|
1252
|
+
...baselines ? { baselines } : {},
|
|
1253
|
+
...isPartial ? { totalFiles: totalSpecCount } : {},
|
|
1254
|
+
...coverageRoot !== void 0 ? { root: coverageRoot } : {},
|
|
1255
|
+
...coverageGlobs
|
|
1256
|
+
};
|
|
1257
|
+
const coverageResult = stashedCoverage && isFirstProject ? isPartial ? yield* analyzer.processScoped(stashedCoverage, coverageOpts, testedFiles ?? []) : yield* analyzer.process(stashedCoverage, coverageOpts) : Option.none();
|
|
1258
|
+
const coverageReport = Option.getOrUndefined(coverageResult);
|
|
1259
|
+
if (wantsRunEvents && coverageReport !== void 0) {
|
|
1260
|
+
const globalThresholds = coverageReport.thresholds.global;
|
|
1261
|
+
emitEvent({
|
|
1262
|
+
_tag: "CoverageReady",
|
|
1263
|
+
metrics: coverageReport.totals,
|
|
1264
|
+
thresholds: globalThresholds,
|
|
1265
|
+
gaps: coverageReport.lowCoverage.map((fc) => ({
|
|
1266
|
+
file: fc.file,
|
|
1267
|
+
missing: fc.summary,
|
|
1268
|
+
uncoveredLines: fc.uncoveredLines
|
|
1269
|
+
})),
|
|
1270
|
+
...coverageReport.scoped ? { scoped: coverageReport.scoped } : {},
|
|
1271
|
+
...coverageReport.scopedFiles !== void 0 ? { scopedFiles: coverageReport.scopedFiles.length } : {},
|
|
1272
|
+
...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {},
|
|
1273
|
+
...coverageReport.globShortfalls !== void 0 ? { globShortfalls: coverageReport.globShortfalls } : {}
|
|
1274
|
+
});
|
|
1275
|
+
if (!coverageReport.scoped) for (const metric of [
|
|
1276
|
+
"lines",
|
|
1277
|
+
"branches",
|
|
1278
|
+
"functions",
|
|
1279
|
+
"statements"
|
|
1280
|
+
]) {
|
|
1281
|
+
const expected = globalThresholds[metric];
|
|
1282
|
+
const actual = coverageReport.totals[metric];
|
|
1283
|
+
if (expected !== void 0 && actual < expected) emitEvent({
|
|
1284
|
+
_tag: "ThresholdViolation",
|
|
1285
|
+
metric,
|
|
1286
|
+
expected,
|
|
1287
|
+
actual
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return coverageReport;
|
|
1292
|
+
});
|
|
1293
|
+
if (opts.coverageMode === "ui-only" || !opts.persist) {
|
|
1184
1294
|
const uiProjectGroups = /* @__PURE__ */ new Map();
|
|
1185
1295
|
for (const mod of filteredModules) {
|
|
1186
1296
|
const key = mod.project.name || "default";
|
|
@@ -1198,11 +1308,16 @@ var AgentReporter = class {
|
|
|
1198
1308
|
const detector = yield* EnvironmentDetector;
|
|
1199
1309
|
const executorResolver = yield* ExecutorResolver;
|
|
1200
1310
|
const detailResolver = yield* DetailResolver;
|
|
1311
|
+
const coverageReport = yield* analyzeCoverage(void 0);
|
|
1312
|
+
const reports = coverageReport ? uiReports.map((r) => ({
|
|
1313
|
+
...r,
|
|
1314
|
+
coverage: coverageReport
|
|
1315
|
+
})) : uiReports;
|
|
1201
1316
|
const env = yield* detector.detect();
|
|
1202
1317
|
const executor = yield* executorResolver.resolve(env);
|
|
1203
1318
|
const health = {
|
|
1204
|
-
hasFailures:
|
|
1205
|
-
belowTargets:
|
|
1319
|
+
hasFailures: reports.some((r) => r.failedFiles.length > 0 || r.unhandledErrors.length > 0),
|
|
1320
|
+
belowTargets: (coverageReport?.belowTarget?.length ?? 0) > 0,
|
|
1206
1321
|
hasTargets: !!opts.coverageTargets
|
|
1207
1322
|
};
|
|
1208
1323
|
const detail = yield* detailResolver.resolve(executor, health, opts.detail);
|
|
@@ -1222,11 +1337,12 @@ var AgentReporter = class {
|
|
|
1222
1337
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
1223
1338
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
1224
1339
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
1225
|
-
coverageMode: opts.coverageMode
|
|
1340
|
+
coverageMode: opts.coverageMode,
|
|
1341
|
+
persist: opts.persist
|
|
1226
1342
|
});
|
|
1227
1343
|
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1228
1344
|
const renderInput = {
|
|
1229
|
-
reports: withStrayOutput(
|
|
1345
|
+
reports: withStrayOutput(reports, strayOutput),
|
|
1230
1346
|
classifications: /* @__PURE__ */ new Map()
|
|
1231
1347
|
};
|
|
1232
1348
|
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
@@ -1235,7 +1351,7 @@ var AgentReporter = class {
|
|
|
1235
1351
|
...reportWriter !== null && { writeReport: reportWriter.write }
|
|
1236
1352
|
});
|
|
1237
1353
|
});
|
|
1238
|
-
await Effect.runPromise(uiProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1354
|
+
await Effect.runPromise(uiProgram.pipe(Effect.provide(CoverageAnalyzerLive), Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1239
1355
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1240
1356
|
});
|
|
1241
1357
|
await reportWriter?.flush();
|
|
@@ -1270,7 +1386,6 @@ var AgentReporter = class {
|
|
|
1270
1386
|
const persistProgram = Effect.gen(function* () {
|
|
1271
1387
|
const store = yield* DataStore;
|
|
1272
1388
|
const reader = yield* DataReader;
|
|
1273
|
-
const analyzer = yield* CoverageAnalyzer;
|
|
1274
1389
|
const tracker = yield* HistoryTracker;
|
|
1275
1390
|
const invocationId = randomUUID();
|
|
1276
1391
|
const vitest = stashedVitest;
|
|
@@ -1288,60 +1403,12 @@ var AgentReporter = class {
|
|
|
1288
1403
|
}
|
|
1289
1404
|
const baselinesOpt = yield* reader.getBaselines("__global__").pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
1290
1405
|
const baselines = Option.getOrUndefined(baselinesOpt);
|
|
1291
|
-
const
|
|
1292
|
-
for (const m of modules) {
|
|
1293
|
-
const key = m.project.name || "default";
|
|
1294
|
-
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
1295
|
-
}
|
|
1296
|
-
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1297
|
-
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1298
|
-
const coverageRoot = typeof vitestConfig.root === "string" ? vitestConfig.root : void 0;
|
|
1299
|
-
const coverageOpts = {
|
|
1300
|
-
thresholds: opts.coverageThresholds,
|
|
1301
|
-
includeBareZero: opts.includeBareZero,
|
|
1302
|
-
...opts.coverageTargets ? { targets: opts.coverageTargets } : {},
|
|
1303
|
-
...baselines ? { baselines } : {},
|
|
1304
|
-
...isPartial ? { totalFiles: totalSpecCount } : {},
|
|
1305
|
-
...coverageRoot !== void 0 ? { root: coverageRoot } : {}
|
|
1306
|
-
};
|
|
1307
|
-
const coverageResult = stashedCoverage && isFirstProject ? isPartial ? yield* analyzer.processScoped(stashedCoverage, coverageOpts, testedFiles ?? []) : yield* analyzer.process(stashedCoverage, coverageOpts) : Option.none();
|
|
1308
|
-
const coverageReport = Option.getOrUndefined(coverageResult);
|
|
1309
|
-
if (wantsRunEvents && coverageReport !== void 0) {
|
|
1310
|
-
const globalThresholds = coverageReport.thresholds.global;
|
|
1311
|
-
emitEvent({
|
|
1312
|
-
_tag: "CoverageReady",
|
|
1313
|
-
metrics: coverageReport.totals,
|
|
1314
|
-
thresholds: globalThresholds,
|
|
1315
|
-
gaps: coverageReport.lowCoverage.map((fc) => ({
|
|
1316
|
-
file: fc.file,
|
|
1317
|
-
missing: fc.summary,
|
|
1318
|
-
uncoveredLines: fc.uncoveredLines
|
|
1319
|
-
})),
|
|
1320
|
-
...coverageReport.scoped ? { scoped: coverageReport.scoped } : {},
|
|
1321
|
-
...coverageReport.scopedFiles !== void 0 ? { scopedFiles: coverageReport.scopedFiles.length } : {},
|
|
1322
|
-
...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {},
|
|
1323
|
-
...coverageReport.globShortfalls !== void 0 ? { globShortfalls: coverageReport.globShortfalls } : {}
|
|
1324
|
-
});
|
|
1325
|
-
if (!coverageReport.scoped) for (const metric of [
|
|
1326
|
-
"lines",
|
|
1327
|
-
"branches",
|
|
1328
|
-
"functions",
|
|
1329
|
-
"statements"
|
|
1330
|
-
]) {
|
|
1331
|
-
const expected = globalThresholds[metric];
|
|
1332
|
-
const actual = coverageReport.totals[metric];
|
|
1333
|
-
if (expected !== void 0 && actual < expected) emitEvent({
|
|
1334
|
-
_tag: "ThresholdViolation",
|
|
1335
|
-
metric,
|
|
1336
|
-
expected,
|
|
1337
|
-
actual
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1406
|
+
const coverageReport = yield* analyzeCoverage(baselines);
|
|
1341
1407
|
const reports = [];
|
|
1342
1408
|
const isMultiProject = projectGroups.size > 1 || !!opts.projectFilter;
|
|
1343
1409
|
yield* withPersistTransaction(Effect.gen(function* () {
|
|
1344
1410
|
yield* store.writeSettings(settingsHash, settings, envVars);
|
|
1411
|
+
let fileCoverageWritten = false;
|
|
1345
1412
|
for (const [projectName, projectModules] of projectGroups) {
|
|
1346
1413
|
const project = projectName === "default" ? "default" : projectName;
|
|
1347
1414
|
const baseReport = buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProject ? projectName : void 0);
|
|
@@ -1618,7 +1685,8 @@ var AgentReporter = class {
|
|
|
1618
1685
|
coverage: coverageReport
|
|
1619
1686
|
} : reportWithTags;
|
|
1620
1687
|
reports.push(report);
|
|
1621
|
-
if (coverageReport) {
|
|
1688
|
+
if (coverageReport && !fileCoverageWritten) {
|
|
1689
|
+
fileCoverageWritten = true;
|
|
1622
1690
|
const tierByFile = /* @__PURE__ */ new Map();
|
|
1623
1691
|
const sourceByFile = /* @__PURE__ */ new Map();
|
|
1624
1692
|
for (const fc of coverageReport.belowTarget ?? []) {
|
|
@@ -1663,6 +1731,10 @@ var AgentReporter = class {
|
|
|
1663
1731
|
if (opts.coverageThresholds !== void 0) yield* store.writeThresholds(opts.coverageThresholds);
|
|
1664
1732
|
if (opts.coverageTargets !== void 0) yield* store.writeTargets(opts.coverageTargets);
|
|
1665
1733
|
}
|
|
1734
|
+
if (Option.isSome(retention)) yield* store.pruneRuns(retention.value).pipe(Effect.tap((result) => result.prunedRuns > 0 ? Effect.logDebug("pruned runs", {
|
|
1735
|
+
prunedRuns: result.prunedRuns,
|
|
1736
|
+
prunedFiles: result.prunedFiles
|
|
1737
|
+
}) : Effect.void), Effect.catch((error) => Effect.logWarning("run retention prune failed", { error: error.message })));
|
|
1666
1738
|
}));
|
|
1667
1739
|
let trendSummary;
|
|
1668
1740
|
if (coverageReport && !coverageReport.scoped) {
|
|
@@ -1773,7 +1845,8 @@ var AgentReporter = class {
|
|
|
1773
1845
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
1774
1846
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
1775
1847
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
1776
|
-
coverageMode: opts.coverageMode
|
|
1848
|
+
coverageMode: opts.coverageMode,
|
|
1849
|
+
persist: opts.persist
|
|
1777
1850
|
});
|
|
1778
1851
|
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1779
1852
|
const renderInput = {
|
|
@@ -18,6 +18,7 @@ const buildReporterKit = (input) => {
|
|
|
18
18
|
detail: input.detail,
|
|
19
19
|
noColor: input.noColor,
|
|
20
20
|
coverageMode: input.coverageMode,
|
|
21
|
+
persist: input.persist,
|
|
21
22
|
transport: input.transport,
|
|
22
23
|
...input.dbPath !== void 0 && { dbPath: input.dbPath },
|
|
23
24
|
...input.projectFilter !== void 0 && { projectFilter: input.projectFilter },
|
|
@@ -8,6 +8,42 @@ import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/works
|
|
|
8
8
|
import { nodeSyncOps } from "@effected/workspaces/node-sync";
|
|
9
9
|
|
|
10
10
|
// src/utils/discover-projects.ts
|
|
11
|
+
/**
|
|
12
|
+
* Shallow-merges each `overrides` entry into the generated project whose
|
|
13
|
+
* `test.name` equals the entry's key. Pure: returns new project objects for the
|
|
14
|
+
* configured names and the original objects for the rest; the discovered
|
|
15
|
+
* `name` always wins over one carried by the override. Only a string
|
|
16
|
+
* `test.name` can be keyed; a project named through Vitest's object form is
|
|
17
|
+
* left untouched (and a key for it is reported as unknown).
|
|
18
|
+
*
|
|
19
|
+
* Throws when a key names no project in `configs`, listing the discovered
|
|
20
|
+
* names so the typo is visible at config-load time.
|
|
21
|
+
* @param configs - The projects a strategy produced
|
|
22
|
+
* @param overrides - Per-project `test` overrides keyed by project name
|
|
23
|
+
* @returns The projects with overrides applied
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
function applyProjectOverrides(configs, overrides) {
|
|
27
|
+
const names = new Set(configs.map((c) => c.test?.name).filter((n) => typeof n === "string"));
|
|
28
|
+
const unknown = Object.keys(overrides).filter((key) => !names.has(key));
|
|
29
|
+
if (unknown.length > 0) {
|
|
30
|
+
const quoted = unknown.map((k) => `"${k}"`).join(", ");
|
|
31
|
+
const discovered = [...names].map((n) => `"${n}"`).join(", ") || "(none)";
|
|
32
|
+
throw new Error(`[vitest-agent] configure: ${quoted} ${unknown.length === 1 ? "matches" : "match"} no discovered project. Discovered projects: ${discovered}.`);
|
|
33
|
+
}
|
|
34
|
+
return configs.map((config) => {
|
|
35
|
+
const name = config.test?.name;
|
|
36
|
+
if (typeof name !== "string" || !Object.hasOwn(overrides, name)) return config;
|
|
37
|
+
return {
|
|
38
|
+
...config,
|
|
39
|
+
test: {
|
|
40
|
+
...config.test,
|
|
41
|
+
...overrides[name],
|
|
42
|
+
name
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
11
47
|
const DISCOVERY_LAST_SCAN_SYMBOL = Symbol.for("vitest-agent:discovery:last-scan-at");
|
|
12
48
|
const warnedDeclinedPackagePaths = /* @__PURE__ */ new Set();
|
|
13
49
|
async function warnIfDeclinedPackageIsTestShaped(pkg, fs) {
|
|
@@ -101,6 +137,7 @@ async function discoverProjects(options) {
|
|
|
101
137
|
const strategy = options?.strategy;
|
|
102
138
|
const cwd = options?.cwd;
|
|
103
139
|
const additionalEntries = options?.additionalEntries ?? [];
|
|
140
|
+
const configure = options?.configure;
|
|
104
141
|
const fs = options?.fs ?? nodeWalkerFs;
|
|
105
142
|
const syncOps = options?.syncOps ?? nodeSyncOps;
|
|
106
143
|
const packageEnumerationOptions = {
|
|
@@ -109,7 +146,7 @@ async function discoverProjects(options) {
|
|
|
109
146
|
};
|
|
110
147
|
const root = findWorkspaceRootSync(cwd ?? process.cwd(), syncOps);
|
|
111
148
|
if (!root) throw new Error(`[vitest-agent] Could not find workspace root from ${cwd ?? process.cwd()}. Ensure a pnpm-workspace.yaml or package.json with "workspaces" exists.`);
|
|
112
|
-
const useCache = strategy === void 0 && additionalEntries.length === 0;
|
|
149
|
+
const useCache = strategy === void 0 && additionalEntries.length === 0 && configure === void 0;
|
|
113
150
|
const resolvedStrategy = strategy ?? new DefaultDiscoverStrategy();
|
|
114
151
|
const packages = getWorkspacePackagesSync(root, packageEnumerationOptions);
|
|
115
152
|
let signature;
|
|
@@ -154,8 +191,9 @@ async function discoverProjects(options) {
|
|
|
154
191
|
configs.push(config);
|
|
155
192
|
}
|
|
156
193
|
const tags = [...resolvedStrategy.tagDefinitions];
|
|
194
|
+
const configured = configure === void 0 ? configs : applyProjectOverrides(configs, configure);
|
|
157
195
|
const result = {
|
|
158
|
-
projects:
|
|
196
|
+
projects: configured.length > 0 ? configured : void 0,
|
|
159
197
|
tags
|
|
160
198
|
};
|
|
161
199
|
if (useCache && signature !== void 0) _cache.set(root, {
|
|
@@ -166,4 +204,4 @@ async function discoverProjects(options) {
|
|
|
166
204
|
return result;
|
|
167
205
|
}
|
|
168
206
|
|
|
169
|
-
export { discoverProjects };
|
|
207
|
+
export { applyProjectOverrides, discoverProjects };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/utils/resolve-persistence.ts
|
|
2
|
+
/**
|
|
3
|
+
* The environment variable an operator sets to override `AgentPlugin`'s
|
|
4
|
+
* `persist` option when the config cannot be edited (issue #625).
|
|
5
|
+
*/
|
|
6
|
+
const PERSIST_ENV_VAR = "VITEST_AGENT_PERSIST";
|
|
7
|
+
/** Values of `VITEST_AGENT_PERSIST` that turn persistence off (compared lower-cased and trimmed). */
|
|
8
|
+
const OFF_VALUES = /* @__PURE__ */ new Set([
|
|
9
|
+
"0",
|
|
10
|
+
"false",
|
|
11
|
+
"off"
|
|
12
|
+
]);
|
|
13
|
+
/** Values of `VITEST_AGENT_PERSIST` that turn persistence on (compared lower-cased and trimmed). */
|
|
14
|
+
const ON_VALUES = /* @__PURE__ */ new Set([
|
|
15
|
+
"1",
|
|
16
|
+
"true",
|
|
17
|
+
"on"
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Pure decision: should this run be recorded in the SQLite `data.db`?
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* The env var is an operator override and wins over the option in either
|
|
24
|
+
* direction — `VITEST_AGENT_PERSIST=0` turns persistence off under
|
|
25
|
+
* `persist: true`, and `=1` turns it back on under `persist: false` — so a
|
|
26
|
+
* runner whose `vitest.config.ts` is not editable can still decide. An
|
|
27
|
+
* empty value is unset. Nothing here reads `CI`: the opt-out is explicit
|
|
28
|
+
* by design, because some CI setups cache `data.db` between jobs and
|
|
29
|
+
* query it, and an automatic opt-out would silently empty their history.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
function resolvePersistence(input) {
|
|
34
|
+
const raw = input.env[PERSIST_ENV_VAR];
|
|
35
|
+
let ignoredEnvValue;
|
|
36
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
37
|
+
const normalized = raw.trim().toLowerCase();
|
|
38
|
+
if (OFF_VALUES.has(normalized)) return {
|
|
39
|
+
persist: false,
|
|
40
|
+
source: "env"
|
|
41
|
+
};
|
|
42
|
+
if (ON_VALUES.has(normalized)) return {
|
|
43
|
+
persist: true,
|
|
44
|
+
source: "env"
|
|
45
|
+
};
|
|
46
|
+
ignoredEnvValue = raw;
|
|
47
|
+
}
|
|
48
|
+
const fromOption = input.option === void 0 ? {
|
|
49
|
+
persist: true,
|
|
50
|
+
source: "default"
|
|
51
|
+
} : {
|
|
52
|
+
persist: input.option,
|
|
53
|
+
source: "option"
|
|
54
|
+
};
|
|
55
|
+
return ignoredEnvValue === void 0 ? fromOption : {
|
|
56
|
+
...fromOption,
|
|
57
|
+
ignoredEnvValue
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { PERSIST_ENV_VAR, resolvePersistence };
|