@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 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` and deeper package traversal via
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` after the first `onTestRunEnd` invocation completes.
710
+ * Set to `true` once `onTestRunEnd` has done this run's work.
632
711
  *
633
- * Vitest can fire `onTestRunEnd` more than once per `vitest run` in
634
- * some multi-project configurations (e.g., once per project group, with
635
- * the same `testModules` array each time). The plugin pushes only ONE
636
- * AgentReporter per run, so a fresh instance always starts with this
637
- * flag `false`; the first call does the work and the flag prevents
638
- * subsequent invocations from re-rendering and re-writing to the DB.
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.4.8",
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.20.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.36.1",
52
- "@vitest-agent/cli": "4.0.12",
53
- "@vitest-agent/engine": "1.1.1",
54
- "@vitest-agent/mcp": "5.2.8",
55
- "@vitest-agent/reporter": "5.0.7",
56
- "@vitest-agent/sdk": "7.0.5",
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` after the first `onTestRunEnd` invocation completes.
334
+ * Set to `true` once `onTestRunEnd` has done this run's work.
304
335
  *
305
- * Vitest can fire `onTestRunEnd` more than once per `vitest run` in
306
- * some multi-project configurations (e.g., once per project group, with
307
- * the same `testModules` array each time). The plugin pushes only ONE
308
- * AgentReporter per run, so a fresh instance always starts with this
309
- * flag `false`; the first call does the work and the flag prevents
310
- * subsequent invocations from re-rendering and re-writing to the DB.
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
- if (opts.coverageMode === "ui-only") {
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: uiReports.some((r) => r.failedFiles.length > 0 || r.unhandledErrors.length > 0),
1205
- belowTargets: false,
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(uiReports, strayOutput),
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 projectModuleCounts = /* @__PURE__ */ new Map();
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: configs.length > 0 ? configs : void 0,
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 };
package/version.js CHANGED
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_PLUGIN_VERSION = "5.4.8";
11
+ const CURRENT_PLUGIN_VERSION = "5.5.0";
12
12
 
13
13
  export { CURRENT_PLUGIN_VERSION };