@vitest-agent/plugin 5.4.9 → 5.6.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 +120 -12
- package/index.js +2 -2
- package/layers/CoverageAnalyzerLive.js +24 -1
- package/package.json +6 -6
- package/plugin.js +24 -6
- package/reporter.js +151 -74
- package/utils/build-reporter-kit.js +23 -27
- package/utils/config-suggested-actions.js +42 -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, SuggestedActionRecord, 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;
|
|
@@ -563,6 +631,17 @@ interface AgentReporterConstructorOptions extends AgentReporterOptions {
|
|
|
563
631
|
* `run_tests` tool.
|
|
564
632
|
*/
|
|
565
633
|
passWithNoTests?: boolean;
|
|
634
|
+
/**
|
|
635
|
+
* Run-level suggested actions the plugin resolved at config time — the
|
|
636
|
+
* `ConfigValidation` warnings and info entries, mapped onto
|
|
637
|
+
* `SuggestedActionRecord`s (issue #544). Forwarded verbatim onto
|
|
638
|
+
* `ReporterRenderInput.suggestedActions` for every render, so the
|
|
639
|
+
* `Actions:` trailer repeats them in watch mode. Absent when the config
|
|
640
|
+
* validated clean.
|
|
641
|
+
*
|
|
642
|
+
* @internal
|
|
643
|
+
*/
|
|
644
|
+
suggestedActions?: ReadonlyArray<SuggestedActionRecord>;
|
|
566
645
|
coverageThresholds?: ResolvedThresholds | Record<string, unknown>;
|
|
567
646
|
coverageTargets?: ResolvedThresholds | Record<string, unknown>;
|
|
568
647
|
/**
|
|
@@ -574,6 +653,17 @@ interface AgentReporterConstructorOptions extends AgentReporterOptions {
|
|
|
574
653
|
* @internal
|
|
575
654
|
*/
|
|
576
655
|
cacheDir?: string;
|
|
656
|
+
/**
|
|
657
|
+
* Run-retention override (issue 623). A `RunRetentionPolicy` is applied
|
|
658
|
+
* as given; `false` disables run pruning for this reporter; absent means
|
|
659
|
+
* the policy the `[retention]` table of `vitest-agent.config.toml`
|
|
660
|
+
* resolves to (the default of `RunRetentionPolicy.DEFAULT_KEEP_RUNS`
|
|
661
|
+
* newest runs per project when the table is absent). Takes precedence
|
|
662
|
+
* over the TOML file, like `cacheDir` does for the database path.
|
|
663
|
+
*
|
|
664
|
+
* @internal
|
|
665
|
+
*/
|
|
666
|
+
retention?: RunRetentionPolicy | false;
|
|
577
667
|
}
|
|
578
668
|
/**
|
|
579
669
|
* Vitest Reporter that produces structured output for LLM coding agents.
|
|
@@ -628,14 +718,17 @@ export declare class AgentReporter {
|
|
|
628
718
|
private options;
|
|
629
719
|
private dbPath;
|
|
630
720
|
/**
|
|
631
|
-
* Set to `true`
|
|
721
|
+
* Set to `true` once `onTestRunEnd` has done this run's work.
|
|
632
722
|
*
|
|
633
|
-
* Vitest can fire `onTestRunEnd` more than once per
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
723
|
+
* Vitest can fire `onTestRunEnd` more than once per run in some
|
|
724
|
+
* multi-project configurations (e.g., once per project group, with the
|
|
725
|
+
* same `testModules` array each time). The first call does the work and
|
|
726
|
+
* the flag prevents subsequent invocations from re-rendering and
|
|
727
|
+
* re-writing to the DB. The flag is PER RUN, not per instance: Vitest
|
|
728
|
+
* keeps one reporter instance alive across watch-mode reruns, so
|
|
729
|
+
* `onTestRunStart` re-arms it — otherwise every rerun after the first
|
|
730
|
+
* would silently skip persistence, rendering and `RunFinished`
|
|
731
|
+
* (issue #576).
|
|
639
732
|
*/
|
|
640
733
|
private rendered;
|
|
641
734
|
/**
|
|
@@ -1270,6 +1363,21 @@ interface CoverageOptions {
|
|
|
1270
1363
|
* production; forward or back slashes both work).
|
|
1271
1364
|
*/
|
|
1272
1365
|
readonly root?: string;
|
|
1366
|
+
/**
|
|
1367
|
+
* Vitest's resolved `coverage.include` globs. When set, a coverage-map
|
|
1368
|
+
* key matching none of them is dropped before any per-file or glob
|
|
1369
|
+
* evaluation (issue #622). Matched like threshold patterns: against
|
|
1370
|
+
* `relative(root, key)` when `root` is set, verbatim otherwise. Absent,
|
|
1371
|
+
* every key is kept. A path under a `node_modules` segment is dropped
|
|
1372
|
+
* regardless of this option.
|
|
1373
|
+
*/
|
|
1374
|
+
readonly include?: ReadonlyArray<string>;
|
|
1375
|
+
/**
|
|
1376
|
+
* Vitest's resolved `coverage.exclude` globs. A coverage-map key matching
|
|
1377
|
+
* any of them is dropped before evaluation (issue #622), with the same
|
|
1378
|
+
* matching rule as `include`.
|
|
1379
|
+
*/
|
|
1380
|
+
readonly exclude?: ReadonlyArray<string>;
|
|
1273
1381
|
}
|
|
1274
1382
|
declare const CoverageAnalyzer_base: Context.ServiceClass<CoverageAnalyzer, "vitest-agent/CoverageAnalyzer", {
|
|
1275
1383
|
readonly process: (coverage: unknown, options: CoverageOptions) => Effect.Effect<Option.Option<CoverageReport>>;
|
|
@@ -1490,5 +1598,5 @@ export declare const COVERAGE_AUTOUPDATE: Readonly<{
|
|
|
1490
1598
|
strict: (next: number, previous: number) => number;
|
|
1491
1599
|
lenient: (next: number, previous: number) => number;
|
|
1492
1600
|
}>;
|
|
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 };
|
|
1601
|
+
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
1602
|
//# 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.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Vitest plugin for the vitest-agent ecosystem: owns persistence, classification, baselines, trends, and dispatches rendering to a configurable reporter.",
|
|
6
6
|
"keywords": [
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"@effected/glob": "^0.14.0",
|
|
50
50
|
"@effected/walker": "^0.19.0",
|
|
51
51
|
"@effected/workspaces": "^0.38.0",
|
|
52
|
-
"@vitest-agent/cli": "4.
|
|
53
|
-
"@vitest-agent/engine": "
|
|
54
|
-
"@vitest-agent/mcp": "5.
|
|
55
|
-
"@vitest-agent/reporter": "
|
|
56
|
-
"@vitest-agent/sdk": "
|
|
52
|
+
"@vitest-agent/cli": "4.1.1",
|
|
53
|
+
"@vitest-agent/engine": "2.0.0",
|
|
54
|
+
"@vitest-agent/mcp": "5.3.0",
|
|
55
|
+
"@vitest-agent/reporter": "6.0.0",
|
|
56
|
+
"@vitest-agent/sdk": "8.0.0",
|
|
57
57
|
"effect": "^4.0.6",
|
|
58
58
|
"magic-string": "^1.4.3"
|
|
59
59
|
},
|
package/plugin.js
CHANGED
|
@@ -6,12 +6,14 @@ import { assertFlatScope } from "./utils/report-writer.js";
|
|
|
6
6
|
import { installStrayOutputCapture } from "./utils/stray-output-capture.js";
|
|
7
7
|
import { AgentReporter } from "./reporter.js";
|
|
8
8
|
import { buildModuleInfo } from "./utils/build-module-info.js";
|
|
9
|
+
import { publishConfigSuggestedActions, suggestedActionsFromValidation } from "./utils/config-suggested-actions.js";
|
|
9
10
|
import { DefaultDiscoverStrategy } from "./utils/discover-strategy.js";
|
|
10
11
|
import { discoverProjects } from "./utils/discover-projects.js";
|
|
11
12
|
import { ensureGithubActionsReporter } from "./utils/ensure-github-reporter.js";
|
|
12
13
|
import { injectTags } from "./utils/inject-tags.js";
|
|
13
14
|
import { isBenignViteSourceMapWarning } from "./utils/is-benign-vite-source-map-warning.js";
|
|
14
15
|
import { resolveCoverageDirIsolation } from "./utils/resolve-coverage-dir-isolation.js";
|
|
16
|
+
import { PERSIST_ENV_VAR, resolvePersistence } from "./utils/resolve-persistence.js";
|
|
15
17
|
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
18
|
import { stripConsoleReporters } from "./utils/strip-console-reporters.js";
|
|
17
19
|
import { makeTagCacheKeyGenerator } from "./utils/tag-cache-key.js";
|
|
@@ -215,11 +217,15 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
215
217
|
ctx.defineCacheKeyGenerator(makeTagCacheKeyGenerator(classifyForCache));
|
|
216
218
|
log("registered fsModuleCache tag cache-key generator");
|
|
217
219
|
}
|
|
218
|
-
const { env, executor } = await Effect.runPromise(Effect.gen(function* () {
|
|
219
|
-
const
|
|
220
|
+
const { env, executor, inGithubActions } = await Effect.runPromise(Effect.gen(function* () {
|
|
221
|
+
const detector = yield* EnvironmentDetector;
|
|
222
|
+
const env = yield* detector.detect();
|
|
223
|
+
const executor = yield* (yield* ExecutorResolver).resolve(env);
|
|
224
|
+
const ci = yield* detector.ci;
|
|
220
225
|
return {
|
|
221
226
|
env,
|
|
222
|
-
executor
|
|
227
|
+
executor,
|
|
228
|
+
inGithubActions: Option.contains(ci, "github-actions")
|
|
223
229
|
};
|
|
224
230
|
}).pipe(Effect.provide(Layer.merge(layer, ExecutorResolverLive))));
|
|
225
231
|
const reportOption = options.report;
|
|
@@ -253,17 +259,19 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
253
259
|
stderr: process.stderr
|
|
254
260
|
});
|
|
255
261
|
}
|
|
256
|
-
|
|
262
|
+
const githubActions = inGithubActions && consoleMode !== "silent";
|
|
263
|
+
if (githubActions) {
|
|
257
264
|
log("normalizing the github-actions reporter entry");
|
|
258
265
|
const withGithubActions = ensureGithubActionsReporter(vitest.config.reporters);
|
|
259
266
|
vitest.config.reporters = withGithubActions;
|
|
260
267
|
}
|
|
261
|
-
const githubActions = env === "ci-github" && consoleMode !== "silent";
|
|
262
268
|
log("githubActions (auto):", githubActions);
|
|
263
269
|
const validation = await Effect.runPromise(Effect.provide(Effect.flatMap(ConfigValidation, (cv) => cv.validate({
|
|
264
270
|
vitestConfig: vitest.config,
|
|
265
271
|
pluginOptions: options
|
|
266
272
|
})), ConfigValidationLive));
|
|
273
|
+
const configSuggestedActions = suggestedActionsFromValidation(validation);
|
|
274
|
+
publishConfigSuggestedActions(vitest, configSuggestedActions);
|
|
267
275
|
for (const w of validation.warnings) reportOnce(`[vitest-agent:plugin] warning ${w.code}: ${w.message}` + (w.remediation ? `\n ${w.remediation}` : "") + "\n");
|
|
268
276
|
for (const i of validation.info) reportOnce(`[vitest-agent:plugin] info ${i.code}: ${i.message}\n`);
|
|
269
277
|
if (validation.errors.length > 0) {
|
|
@@ -275,6 +283,13 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
275
283
|
const rawTargets = options.coverageTargets;
|
|
276
284
|
const coverageTargets = rawTargets ? resolveThresholds(rawTargets) : void 0;
|
|
277
285
|
const coverageMode = coverageConfig?.enabled === false ? "ui-only" : "full";
|
|
286
|
+
const persistence = resolvePersistence({
|
|
287
|
+
option: options.persist,
|
|
288
|
+
env: process.env
|
|
289
|
+
});
|
|
290
|
+
if (persistence.ignoredEnvValue !== void 0) reportOnce(`[vitest-agent:plugin] ignoring ${PERSIST_ENV_VAR}=${persistence.ignoredEnvValue}: expected 0|false|off or 1|true|on\n`);
|
|
291
|
+
const persist = persistence.persist;
|
|
292
|
+
log("coverageMode:", coverageMode, "| persist:", persist, `(${persistence.source})`);
|
|
278
293
|
if (coverageConfig && !coverageDirDecidedByVitest.has(vitest)) {
|
|
279
294
|
coverageDirDecidedByVitest.add(vitest);
|
|
280
295
|
const dirDecision = resolveCoverageDirIsolation({
|
|
@@ -313,12 +328,14 @@ function AgentPlugin(options = {}, _layer) {
|
|
|
313
328
|
...coverageThresholds !== void 0 ? { coverageThresholds } : {},
|
|
314
329
|
...coverageTargets !== void 0 ? { coverageTargets } : {},
|
|
315
330
|
coverageMode,
|
|
331
|
+
persist,
|
|
316
332
|
consoleMode,
|
|
317
333
|
mcp,
|
|
318
334
|
githubActions,
|
|
319
335
|
...reportScope !== void 0 && { reportScope },
|
|
320
336
|
transport,
|
|
321
337
|
...passWithNoTests !== void 0 ? { passWithNoTests } : {},
|
|
338
|
+
...configSuggestedActions.length > 0 ? { suggestedActions: configSuggestedActions } : {},
|
|
322
339
|
...options.reporter !== void 0 && { reporter: options.reporter },
|
|
323
340
|
...options.onRunEvent !== void 0 ? { onRunEvent: options.onRunEvent } : {}
|
|
324
341
|
});
|
|
@@ -417,7 +434,8 @@ function makeDiscoverBuilder(options) {
|
|
|
417
434
|
return makeDiscoverBuilder({
|
|
418
435
|
...opts.strategy !== void 0 ? { strategy: opts.strategy } : {},
|
|
419
436
|
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
420
|
-
...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {}
|
|
437
|
+
...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {},
|
|
438
|
+
...opts.configure !== void 0 ? { configure: opts.configure } : {}
|
|
421
439
|
});
|
|
422
440
|
}
|
|
423
441
|
_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,8 +493,10 @@ 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
|
-
...options.passWithNoTests !== void 0 ? { passWithNoTests: options.passWithNoTests } : {}
|
|
498
|
+
...options.passWithNoTests !== void 0 ? { passWithNoTests: options.passWithNoTests } : {},
|
|
499
|
+
...options.suggestedActions !== void 0 && options.suggestedActions.length > 0 ? { suggestedActions: options.suggestedActions } : {}
|
|
463
500
|
};
|
|
464
501
|
this.options = resolvedTargets ? {
|
|
465
502
|
...base,
|
|
@@ -488,6 +525,7 @@ var AgentReporter = class {
|
|
|
488
525
|
detail: opts.detail ?? "standard",
|
|
489
526
|
noColor: false,
|
|
490
527
|
coverageMode: opts.coverageMode,
|
|
528
|
+
persist: opts.persist,
|
|
491
529
|
transport: opts.transport,
|
|
492
530
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
493
531
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
@@ -510,7 +548,7 @@ var AgentReporter = class {
|
|
|
510
548
|
assertReportCapable(vitest);
|
|
511
549
|
this.reportWriter = createReportWriter(vitest, this.options.reportScope);
|
|
512
550
|
}
|
|
513
|
-
try {
|
|
551
|
+
if (this.options.persist) try {
|
|
514
552
|
await this.ensureDbPath();
|
|
515
553
|
} catch {}
|
|
516
554
|
await this.initReporters();
|
|
@@ -618,7 +656,8 @@ var AgentReporter = class {
|
|
|
618
656
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
619
657
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
620
658
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
621
|
-
coverageMode: opts.coverageMode
|
|
659
|
+
coverageMode: opts.coverageMode,
|
|
660
|
+
persist: opts.persist
|
|
622
661
|
});
|
|
623
662
|
this.reporters = normalizeReporters(opts.reporter(kit));
|
|
624
663
|
} catch (err) {
|
|
@@ -678,6 +717,8 @@ var AgentReporter = class {
|
|
|
678
717
|
* `RunStarted` event for live subscribers.
|
|
679
718
|
*/
|
|
680
719
|
onTestRunStart(_specifications) {
|
|
720
|
+
this.rendered = false;
|
|
721
|
+
this.coverage = null;
|
|
681
722
|
this.startedSpecCount = _specifications.length;
|
|
682
723
|
this.strayCapture?.reset();
|
|
683
724
|
if (this.neutralizedThresholdSnapshot.size > 0) {
|
|
@@ -1173,14 +1214,84 @@ var AgentReporter = class {
|
|
|
1173
1214
|
const wantsRunEvents = this.wantsRunEvents();
|
|
1174
1215
|
let dbPath;
|
|
1175
1216
|
let persistDisabled;
|
|
1176
|
-
try {
|
|
1217
|
+
if (opts.persist) try {
|
|
1177
1218
|
dbPath = await this.ensureDbPath();
|
|
1178
1219
|
} catch (err) {
|
|
1179
1220
|
persistDisabled = formatFatalError(err);
|
|
1180
1221
|
}
|
|
1222
|
+
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
1223
|
const filteredModules = opts.projectFilter ? modules.filter((m) => (m.project.name || "default") === opts.projectFilter) : modules;
|
|
1182
1224
|
if (filteredModules.length === 0 && opts.projectFilter) return;
|
|
1183
|
-
|
|
1225
|
+
const coverageRoot = (() => {
|
|
1226
|
+
const root = stashedVitest?.config?.root;
|
|
1227
|
+
return typeof root === "string" ? root : void 0;
|
|
1228
|
+
})();
|
|
1229
|
+
const coverageGlobs = (() => {
|
|
1230
|
+
const coverage = stashedVitest?.config?.coverage;
|
|
1231
|
+
const asGlobs = (value) => Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
|
|
1232
|
+
const record = typeof coverage === "object" && coverage !== null ? coverage : {};
|
|
1233
|
+
const include = asGlobs(record.include);
|
|
1234
|
+
const exclude = asGlobs(record.exclude);
|
|
1235
|
+
return {
|
|
1236
|
+
...include !== void 0 ? { include } : {},
|
|
1237
|
+
...exclude !== void 0 ? { exclude } : {}
|
|
1238
|
+
};
|
|
1239
|
+
})();
|
|
1240
|
+
const analyzeCoverage = (baselines) => Effect.gen(function* () {
|
|
1241
|
+
const analyzer = yield* CoverageAnalyzer;
|
|
1242
|
+
const projectModuleCounts = /* @__PURE__ */ new Map();
|
|
1243
|
+
for (const m of modules) {
|
|
1244
|
+
const key = m.project.name || "default";
|
|
1245
|
+
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
1246
|
+
}
|
|
1247
|
+
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1248
|
+
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1249
|
+
const coverageOpts = {
|
|
1250
|
+
thresholds: opts.coverageThresholds,
|
|
1251
|
+
includeBareZero: opts.includeBareZero,
|
|
1252
|
+
...opts.coverageTargets ? { targets: opts.coverageTargets } : {},
|
|
1253
|
+
...baselines ? { baselines } : {},
|
|
1254
|
+
...isPartial ? { totalFiles: totalSpecCount } : {},
|
|
1255
|
+
...coverageRoot !== void 0 ? { root: coverageRoot } : {},
|
|
1256
|
+
...coverageGlobs
|
|
1257
|
+
};
|
|
1258
|
+
const coverageResult = stashedCoverage && isFirstProject ? isPartial ? yield* analyzer.processScoped(stashedCoverage, coverageOpts, testedFiles ?? []) : yield* analyzer.process(stashedCoverage, coverageOpts) : Option.none();
|
|
1259
|
+
const coverageReport = Option.getOrUndefined(coverageResult);
|
|
1260
|
+
if (wantsRunEvents && coverageReport !== void 0) {
|
|
1261
|
+
const globalThresholds = coverageReport.thresholds.global;
|
|
1262
|
+
emitEvent({
|
|
1263
|
+
_tag: "CoverageReady",
|
|
1264
|
+
metrics: coverageReport.totals,
|
|
1265
|
+
thresholds: globalThresholds,
|
|
1266
|
+
gaps: coverageReport.lowCoverage.map((fc) => ({
|
|
1267
|
+
file: fc.file,
|
|
1268
|
+
missing: fc.summary,
|
|
1269
|
+
uncoveredLines: fc.uncoveredLines
|
|
1270
|
+
})),
|
|
1271
|
+
...coverageReport.scoped ? { scoped: coverageReport.scoped } : {},
|
|
1272
|
+
...coverageReport.scopedFiles !== void 0 ? { scopedFiles: coverageReport.scopedFiles.length } : {},
|
|
1273
|
+
...coverageReport.totalFiles !== void 0 ? { totalFiles: coverageReport.totalFiles } : {},
|
|
1274
|
+
...coverageReport.globShortfalls !== void 0 ? { globShortfalls: coverageReport.globShortfalls } : {}
|
|
1275
|
+
});
|
|
1276
|
+
if (!coverageReport.scoped) for (const metric of [
|
|
1277
|
+
"lines",
|
|
1278
|
+
"branches",
|
|
1279
|
+
"functions",
|
|
1280
|
+
"statements"
|
|
1281
|
+
]) {
|
|
1282
|
+
const expected = globalThresholds[metric];
|
|
1283
|
+
const actual = coverageReport.totals[metric];
|
|
1284
|
+
if (expected !== void 0 && actual < expected) emitEvent({
|
|
1285
|
+
_tag: "ThresholdViolation",
|
|
1286
|
+
metric,
|
|
1287
|
+
expected,
|
|
1288
|
+
actual
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return coverageReport;
|
|
1293
|
+
});
|
|
1294
|
+
if (opts.coverageMode === "ui-only" || !opts.persist) {
|
|
1184
1295
|
const uiProjectGroups = /* @__PURE__ */ new Map();
|
|
1185
1296
|
for (const mod of filteredModules) {
|
|
1186
1297
|
const key = mod.project.name || "default";
|
|
@@ -1198,11 +1309,16 @@ var AgentReporter = class {
|
|
|
1198
1309
|
const detector = yield* EnvironmentDetector;
|
|
1199
1310
|
const executorResolver = yield* ExecutorResolver;
|
|
1200
1311
|
const detailResolver = yield* DetailResolver;
|
|
1312
|
+
const coverageReport = yield* analyzeCoverage(void 0);
|
|
1313
|
+
const reports = coverageReport ? uiReports.map((r) => ({
|
|
1314
|
+
...r,
|
|
1315
|
+
coverage: coverageReport
|
|
1316
|
+
})) : uiReports;
|
|
1201
1317
|
const env = yield* detector.detect();
|
|
1202
1318
|
const executor = yield* executorResolver.resolve(env);
|
|
1203
1319
|
const health = {
|
|
1204
|
-
hasFailures:
|
|
1205
|
-
belowTargets:
|
|
1320
|
+
hasFailures: reports.some((r) => r.failedFiles.length > 0 || r.unhandledErrors.length > 0),
|
|
1321
|
+
belowTargets: (coverageReport?.belowTarget?.length ?? 0) > 0,
|
|
1206
1322
|
hasTargets: !!opts.coverageTargets
|
|
1207
1323
|
};
|
|
1208
1324
|
const detail = yield* detailResolver.resolve(executor, health, opts.detail);
|
|
@@ -1222,12 +1338,14 @@ var AgentReporter = class {
|
|
|
1222
1338
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
1223
1339
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
1224
1340
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
1225
|
-
coverageMode: opts.coverageMode
|
|
1341
|
+
coverageMode: opts.coverageMode,
|
|
1342
|
+
persist: opts.persist
|
|
1226
1343
|
});
|
|
1227
1344
|
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1228
1345
|
const renderInput = {
|
|
1229
|
-
reports: withStrayOutput(
|
|
1230
|
-
classifications: /* @__PURE__ */ new Map()
|
|
1346
|
+
reports: withStrayOutput(reports, strayOutput),
|
|
1347
|
+
classifications: /* @__PURE__ */ new Map(),
|
|
1348
|
+
...opts.suggestedActions !== void 0 && { suggestedActions: opts.suggestedActions }
|
|
1231
1349
|
};
|
|
1232
1350
|
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
1233
1351
|
for (const output of allOutputs) routeRenderedOutput(output, {
|
|
@@ -1235,7 +1353,7 @@ var AgentReporter = class {
|
|
|
1235
1353
|
...reportWriter !== null && { writeReport: reportWriter.write }
|
|
1236
1354
|
});
|
|
1237
1355
|
});
|
|
1238
|
-
await Effect.runPromise(uiProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1356
|
+
await Effect.runPromise(uiProgram.pipe(Effect.provide(CoverageAnalyzerLive), Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1239
1357
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1240
1358
|
});
|
|
1241
1359
|
await reportWriter?.flush();
|
|
@@ -1270,7 +1388,6 @@ var AgentReporter = class {
|
|
|
1270
1388
|
const persistProgram = Effect.gen(function* () {
|
|
1271
1389
|
const store = yield* DataStore;
|
|
1272
1390
|
const reader = yield* DataReader;
|
|
1273
|
-
const analyzer = yield* CoverageAnalyzer;
|
|
1274
1391
|
const tracker = yield* HistoryTracker;
|
|
1275
1392
|
const invocationId = randomUUID();
|
|
1276
1393
|
const vitest = stashedVitest;
|
|
@@ -1288,60 +1405,12 @@ var AgentReporter = class {
|
|
|
1288
1405
|
}
|
|
1289
1406
|
const baselinesOpt = yield* reader.getBaselines("__global__").pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
1290
1407
|
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
|
-
}
|
|
1408
|
+
const coverageReport = yield* analyzeCoverage(baselines);
|
|
1341
1409
|
const reports = [];
|
|
1342
1410
|
const isMultiProject = projectGroups.size > 1 || !!opts.projectFilter;
|
|
1343
1411
|
yield* withPersistTransaction(Effect.gen(function* () {
|
|
1344
1412
|
yield* store.writeSettings(settingsHash, settings, envVars);
|
|
1413
|
+
let fileCoverageWritten = false;
|
|
1345
1414
|
for (const [projectName, projectModules] of projectGroups) {
|
|
1346
1415
|
const project = projectName === "default" ? "default" : projectName;
|
|
1347
1416
|
const baseReport = buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProject ? projectName : void 0);
|
|
@@ -1381,7 +1450,8 @@ var AgentReporter = class {
|
|
|
1381
1450
|
gitCommitSha: process.env.GITHUB_SHA ?? null,
|
|
1382
1451
|
hostSource: hostProbe.source,
|
|
1383
1452
|
hostValue: hostProbe.value,
|
|
1384
|
-
hostMetadata: hostProbe.metadata
|
|
1453
|
+
hostMetadata: hostProbe.metadata,
|
|
1454
|
+
strayOutput: strayOutput ?? null
|
|
1385
1455
|
});
|
|
1386
1456
|
for (const mod of projectModules) {
|
|
1387
1457
|
const fileId = yield* store.ensureFile(mod.relativeModuleId);
|
|
@@ -1618,7 +1688,8 @@ var AgentReporter = class {
|
|
|
1618
1688
|
coverage: coverageReport
|
|
1619
1689
|
} : reportWithTags;
|
|
1620
1690
|
reports.push(report);
|
|
1621
|
-
if (coverageReport) {
|
|
1691
|
+
if (coverageReport && !fileCoverageWritten) {
|
|
1692
|
+
fileCoverageWritten = true;
|
|
1622
1693
|
const tierByFile = /* @__PURE__ */ new Map();
|
|
1623
1694
|
const sourceByFile = /* @__PURE__ */ new Map();
|
|
1624
1695
|
for (const fc of coverageReport.belowTarget ?? []) {
|
|
@@ -1663,6 +1734,10 @@ var AgentReporter = class {
|
|
|
1663
1734
|
if (opts.coverageThresholds !== void 0) yield* store.writeThresholds(opts.coverageThresholds);
|
|
1664
1735
|
if (opts.coverageTargets !== void 0) yield* store.writeTargets(opts.coverageTargets);
|
|
1665
1736
|
}
|
|
1737
|
+
if (Option.isSome(retention)) yield* store.pruneRuns(retention.value).pipe(Effect.tap((result) => result.prunedRuns > 0 ? Effect.logDebug("pruned runs", {
|
|
1738
|
+
prunedRuns: result.prunedRuns,
|
|
1739
|
+
prunedFiles: result.prunedFiles
|
|
1740
|
+
}) : Effect.void), Effect.catch((error) => Effect.logWarning("run retention prune failed", { error: error.message })));
|
|
1666
1741
|
}));
|
|
1667
1742
|
let trendSummary;
|
|
1668
1743
|
if (coverageReport && !coverageReport.scoped) {
|
|
@@ -1773,12 +1848,14 @@ var AgentReporter = class {
|
|
|
1773
1848
|
...opts.coverageThresholds !== void 0 && { coverageThresholds: opts.coverageThresholds },
|
|
1774
1849
|
...opts.coverageTargets !== void 0 && { coverageTargets: opts.coverageTargets },
|
|
1775
1850
|
...opts.passWithNoTests !== void 0 && { passWithNoTests: opts.passWithNoTests },
|
|
1776
|
-
coverageMode: opts.coverageMode
|
|
1851
|
+
coverageMode: opts.coverageMode,
|
|
1852
|
+
persist: opts.persist
|
|
1777
1853
|
});
|
|
1778
1854
|
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1779
1855
|
const renderInput = {
|
|
1780
1856
|
reports: withStrayOutput(reports, strayOutput),
|
|
1781
1857
|
classifications,
|
|
1858
|
+
...opts.suggestedActions !== void 0 && { suggestedActions: opts.suggestedActions },
|
|
1782
1859
|
...trendSummary !== void 0 && { trendSummary }
|
|
1783
1860
|
};
|
|
1784
1861
|
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
@@ -1,37 +1,33 @@
|
|
|
1
|
-
import { osc8 } from "@vitest-agent/sdk";
|
|
2
|
-
|
|
3
1
|
// src/utils/build-reporter-kit.ts
|
|
4
2
|
const buildReporterKit = (input) => {
|
|
5
3
|
const consoleOutput = input.consoleMode === "silent" ? "silent" : "failures";
|
|
6
4
|
const githubSummary = input.githubActions;
|
|
7
5
|
const githubSummaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
8
|
-
const config = {
|
|
9
|
-
executor: input.executor,
|
|
10
|
-
consoleMode: input.consoleMode,
|
|
11
|
-
mcp: input.mcp,
|
|
12
|
-
consoleOutput,
|
|
13
|
-
omitPassingTests: true,
|
|
14
|
-
coverageConsoleLimit: 10,
|
|
15
|
-
includeBareZero: false,
|
|
16
|
-
githubActions: input.githubActions,
|
|
17
|
-
githubSummary,
|
|
18
|
-
detail: input.detail,
|
|
19
|
-
noColor: input.noColor,
|
|
20
|
-
coverageMode: input.coverageMode,
|
|
21
|
-
transport: input.transport,
|
|
22
|
-
...input.dbPath !== void 0 && { dbPath: input.dbPath },
|
|
23
|
-
...input.projectFilter !== void 0 && { projectFilter: input.projectFilter },
|
|
24
|
-
...githubSummaryFile !== void 0 && { githubSummaryFile },
|
|
25
|
-
...input.runCommand !== void 0 && { runCommand: input.runCommand },
|
|
26
|
-
...input.coverageThresholds !== void 0 && { coverageThresholds: input.coverageThresholds },
|
|
27
|
-
...input.coverageTargets !== void 0 && { coverageTargets: input.coverageTargets },
|
|
28
|
-
...input.passWithNoTests !== void 0 && { passWithNoTests: input.passWithNoTests }
|
|
29
|
-
};
|
|
30
|
-
const osc8Enabled = !input.noColor && (input.env === "terminal" || input.env === "agent-shell");
|
|
31
6
|
return {
|
|
32
|
-
config
|
|
7
|
+
config: {
|
|
8
|
+
executor: input.executor,
|
|
9
|
+
consoleMode: input.consoleMode,
|
|
10
|
+
mcp: input.mcp,
|
|
11
|
+
consoleOutput,
|
|
12
|
+
omitPassingTests: true,
|
|
13
|
+
coverageConsoleLimit: 10,
|
|
14
|
+
includeBareZero: false,
|
|
15
|
+
githubActions: input.githubActions,
|
|
16
|
+
githubSummary,
|
|
17
|
+
detail: input.detail,
|
|
18
|
+
noColor: input.noColor,
|
|
19
|
+
coverageMode: input.coverageMode,
|
|
20
|
+
persist: input.persist,
|
|
21
|
+
transport: input.transport,
|
|
22
|
+
...input.dbPath !== void 0 && { dbPath: input.dbPath },
|
|
23
|
+
...input.projectFilter !== void 0 && { projectFilter: input.projectFilter },
|
|
24
|
+
...githubSummaryFile !== void 0 && { githubSummaryFile },
|
|
25
|
+
...input.runCommand !== void 0 && { runCommand: input.runCommand },
|
|
26
|
+
...input.coverageThresholds !== void 0 && { coverageThresholds: input.coverageThresholds },
|
|
27
|
+
...input.coverageTargets !== void 0 && { coverageTargets: input.coverageTargets },
|
|
28
|
+
...input.passWithNoTests !== void 0 && { passWithNoTests: input.passWithNoTests }
|
|
29
|
+
},
|
|
33
30
|
stdEnv: input.env,
|
|
34
|
-
stdOsc8: (url, label) => osc8(url, label, { enabled: osc8Enabled }),
|
|
35
31
|
...input.runEvents !== void 0 && { runEvents: input.runEvents }
|
|
36
32
|
};
|
|
37
33
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/utils/config-suggested-actions.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `Symbol.for` key under which `configureVitest` publishes the run's
|
|
4
|
+
* config-derived suggested actions on the Vitest instance. `Symbol.for`
|
|
5
|
+
* resolves to the identical symbol across module copies in one process, so
|
|
6
|
+
* the MCP `run_tests` tool reads the same slot off the Vitest instance it
|
|
7
|
+
* created (mirrors the discovery last-scan slot).
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
const CONFIG_SUGGESTED_ACTIONS_SYMBOL = Symbol.for("vitest-agent:config-suggested-actions");
|
|
12
|
+
/**
|
|
13
|
+
* Map a `ValidationResult`'s warnings and info entries onto suggested
|
|
14
|
+
* actions, in the order the validator produced them. A warning becomes a
|
|
15
|
+
* `warn` action and an info entry an `info` action; the title names the
|
|
16
|
+
* rule code, the detail carries the message followed by the remediation
|
|
17
|
+
* when the rule has one. No `targetTool` — there is no MCP tool that edits
|
|
18
|
+
* the Vitest config.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const suggestedActionsFromValidation = (result) => [...result.warnings.map((w) => ({
|
|
23
|
+
severity: "warn",
|
|
24
|
+
title: `Coverage config: ${w.code}`,
|
|
25
|
+
detail: w.remediation !== void 0 ? `${w.message} ${w.remediation}` : w.message
|
|
26
|
+
})), ...result.info.map((i) => ({
|
|
27
|
+
severity: "info",
|
|
28
|
+
title: `Coverage config: ${i.code}`,
|
|
29
|
+
detail: i.message
|
|
30
|
+
}))];
|
|
31
|
+
/**
|
|
32
|
+
* Publish the run's config-derived suggested actions on the Vitest instance
|
|
33
|
+
* for readers in the same process (the MCP `run_tests` tool). An empty list
|
|
34
|
+
* clears the slot so a watch-mode rerun after a config fix reports nothing.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
const publishConfigSuggestedActions = (vitest, actions) => {
|
|
39
|
+
vitest[CONFIG_SUGGESTED_ACTIONS_SYMBOL] = actions.length > 0 ? actions : void 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { CONFIG_SUGGESTED_ACTIONS_SYMBOL, publishConfigSuggestedActions, suggestedActionsFromValidation };
|
|
@@ -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 };
|