@vitest-agent/plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { ConfigValidation } from "./services/ConfigValidation.js";
2
+ import { resolveThresholds } from "./utils/resolve-thresholds.js";
3
+ import { ConfigValidationLive } from "./layers/ConfigValidationLive.js";
4
+ import { CoverageAnalyzer } from "./services/CoverageAnalyzer.js";
5
+ import { CoverageAnalyzerLive } from "./layers/CoverageAnalyzerLive.js";
6
+ import { ReporterLive } from "./layers/ReporterLive.js";
7
+ import { captureEnvVars } from "./utils/capture-env.js";
8
+ import { captureSettings, hashSettings } from "./utils/capture-settings.js";
9
+ import { processFailure } from "./utils/process-failure.js";
10
+ import { AgentReporter } from "./reporter.js";
11
+ import { findTestFiles } from "./utils/find-test-files.js";
12
+ import { Tag } from "./utils/tag.js";
13
+ import { DefaultDiscoverStrategy, DiscoverStrategy } from "./utils/discover-strategy.js";
14
+ import { discoverProjects } from "./utils/discover-projects.js";
15
+ import { CONSOLE_REPORTERS, stripConsoleReporters } from "./utils/strip-console-reporters.js";
16
+ import { AgentPlugin, CURRENT_PLUGIN_VERSION } from "./plugin.js";
17
+ import { classifyByDirectory, classifyByFilename, combineClassifiers } from "./utils/classify-helpers.js";
18
+ import { CoverageAnalyzerTest } from "./layers/CoverageAnalyzerTest.js";
19
+ import { ConfigValidationTest } from "./layers/ConfigValidationTest.js";
20
+ import { CoverageLevel, resolveCoverageInput, validateCoverageConfig } from "@vitest-agent/sdk";
21
+
22
+ //#region src/index.ts
23
+ /** Preset map for coverage levels without per-file enforcement. Mirrors `AgentPlugin.COVERAGE_LEVELS`. @public */
24
+ const COVERAGE_LEVELS = AgentPlugin.COVERAGE_LEVELS;
25
+ /** Preset map for coverage levels with per-file enforcement. Mirrors `AgentPlugin.COVERAGE_LEVELS_PER_FILE`. @public */
26
+ const COVERAGE_LEVELS_PER_FILE = AgentPlugin.COVERAGE_LEVELS_PER_FILE;
27
+ /** Auto-update tolerance functions for `coverage.thresholds.autoUpdate`. Mirrors `AgentPlugin.COVERAGE_AUTOUPDATE`. @public */
28
+ const COVERAGE_AUTOUPDATE = AgentPlugin.COVERAGE_AUTOUPDATE;
29
+
30
+ //#endregion
31
+ export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
@@ -0,0 +1,148 @@
1
+ import { ConfigValidation } from "../services/ConfigValidation.js";
2
+ import { resolveThresholds } from "../utils/resolve-thresholds.js";
3
+ import { createRequire } from "node:module";
4
+ import { validateCoverageTargetsShape } from "@vitest-agent/sdk";
5
+ import { Effect, Layer } from "effect";
6
+
7
+ //#region src/layers/ConfigValidationLive.ts
8
+ /**
9
+ * Live implementation of the ConfigValidation service.
10
+ *
11
+ * Implements all seven validation rules from the 2.0 starter rule set.
12
+ * Rules run inside `validate(...)` — no I/O at construction time.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /** Metrics that can appear at the top level in both coverageTargets and coverage.thresholds. */
17
+ const COVERAGE_METRICS = [
18
+ "lines",
19
+ "functions",
20
+ "branches",
21
+ "statements"
22
+ ];
23
+ /** Supported provider package names mapped to their npm package. */
24
+ const SUPPORTED_PROVIDERS = {
25
+ v8: "@vitest/coverage-v8",
26
+ istanbul: "@vitest/coverage-istanbul"
27
+ };
28
+ /**
29
+ * Resolves the operating mode from the vitest config.
30
+ * Full mode when coverage.enabled !== false; UI-only otherwise.
31
+ */
32
+ function resolveMode(vitestConfig) {
33
+ return vitestConfig.coverage?.enabled === false ? "ui-only" : "full";
34
+ }
35
+ /**
36
+ * Try to require-resolve a package from the workspace.
37
+ * Returns true when the package is resolvable, false when it is not installed.
38
+ */
39
+ function isPackageInstalled(packageName) {
40
+ try {
41
+ createRequire(import.meta.url).resolve(packageName);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Run the TARGET_WITHOUT_THRESHOLD and TARGET_BELOW_THRESHOLD rules.
49
+ *
50
+ * Both targets and thresholds run through resolveThresholds so the `100: true`
51
+ * shorthand and glob-pattern entries are expanded into the canonical `global`
52
+ * per-metric numeric form before comparison. Without normalization a config
53
+ * like `coverage.thresholds: { 100: true }` would look "unset" per metric and
54
+ * fire false-positive TARGET_WITHOUT_THRESHOLD warnings.
55
+ *
56
+ * One entry per affected metric.
57
+ */
58
+ function runTargetThresholdRules(input, errors, warnings) {
59
+ const rawTargets = input.pluginOptions.coverageTargets;
60
+ if (!rawTargets) return;
61
+ const coverageCfg = input.vitestConfig.coverage;
62
+ const targets = resolveThresholds(rawTargets).global;
63
+ const thresholds = resolveThresholds(coverageCfg?.thresholds).global;
64
+ for (const metric of COVERAGE_METRICS) {
65
+ const targetValue = targets[metric];
66
+ const thresholdValue = thresholds[metric];
67
+ if (typeof targetValue !== "number") continue;
68
+ if (thresholdValue === void 0) warnings.push({
69
+ code: "TARGET_WITHOUT_THRESHOLD",
70
+ message: `coverageTargets.${metric} is set to ${targetValue} but coverage.thresholds.${metric} is not configured. Set coverage.thresholds.${metric} to enforce a minimum coverage floor.`
71
+ });
72
+ else if (targetValue < thresholdValue) errors.push({
73
+ code: "TARGET_BELOW_THRESHOLD",
74
+ message: `coverageTargets.${metric} (${targetValue}) is below coverage.thresholds.${metric} (${thresholdValue}). The target must be at or above the threshold for ${metric}.`
75
+ });
76
+ }
77
+ }
78
+ /**
79
+ * Run the INVALID_TARGET_VALUE rule by delegating to the SDK helper.
80
+ * Also picks up PERFILE_ON_TARGETS from the same walk.
81
+ */
82
+ function runInvalidTargetValueRule(input, errors, warnings) {
83
+ const targets = input.pluginOptions.coverageTargets;
84
+ if (!targets) return;
85
+ const result = validateCoverageTargetsShape(targets);
86
+ for (const err of result.errors) errors.push({
87
+ code: err.code,
88
+ path: err.path,
89
+ message: err.message
90
+ });
91
+ for (const warn of result.warnings) if (warn.code === "PERFILE_ON_TARGETS") warnings.push({
92
+ code: "PERFILE_ON_TARGETS",
93
+ message: `The "perFile" key should not be set inside coverageTargets. Set coverage.thresholds.perFile instead.`
94
+ });
95
+ }
96
+ /**
97
+ * Run the UNSUPPORTED_PROVIDER rule (Full mode only).
98
+ */
99
+ function runUnsupportedProviderRule(input, errors) {
100
+ const provider = input.vitestConfig.coverage?.provider;
101
+ if (provider === void 0) return;
102
+ if (!Object.hasOwn(SUPPORTED_PROVIDERS, provider)) errors.push({
103
+ code: "UNSUPPORTED_PROVIDER",
104
+ message: `coverage.provider "${provider}" is not supported by vitest-agent. Supported providers: v8, istanbul.`
105
+ });
106
+ }
107
+ /**
108
+ * Run the MISSING_PROVIDER_PACKAGE rule (Full mode only).
109
+ */
110
+ function runMissingProviderPackageRule(input, errors) {
111
+ const provider = input.vitestConfig.coverage?.provider;
112
+ if (provider === void 0) return;
113
+ if (!Object.hasOwn(SUPPORTED_PROVIDERS, provider)) return;
114
+ const packageName = SUPPORTED_PROVIDERS[provider];
115
+ if (!isPackageInstalled(packageName)) errors.push({
116
+ code: "MISSING_PROVIDER_PACKAGE",
117
+ message: `The coverage provider "${provider}" requires the "${packageName}" package, which does not appear to be installed.`,
118
+ remediation: `npm install --save-dev ${packageName}`
119
+ });
120
+ }
121
+ /**
122
+ * Core validation logic. Runs all rules and accumulates results.
123
+ */
124
+ function runAllRules(input) {
125
+ const errors = [];
126
+ const warnings = [];
127
+ const info = [];
128
+ const mode = resolveMode(input.vitestConfig);
129
+ runTargetThresholdRules(input, errors, warnings);
130
+ runInvalidTargetValueRule(input, errors, warnings);
131
+ if (mode === "full") {
132
+ runUnsupportedProviderRule(input, errors);
133
+ runMissingProviderPackageRule(input, errors);
134
+ }
135
+ return {
136
+ errors,
137
+ warnings,
138
+ info
139
+ };
140
+ }
141
+ /**
142
+ * Live implementation of the ConfigValidation service running the built-in rule registry.
143
+ * @public
144
+ */
145
+ const ConfigValidationLive = Layer.succeed(ConfigValidation, { validate: (input) => Effect.sync(() => runAllRules(input)) });
146
+
147
+ //#endregion
148
+ export { ConfigValidationLive };
@@ -0,0 +1,16 @@
1
+ import { ConfigValidation } from "../services/ConfigValidation.js";
2
+ import { Effect, Layer } from "effect";
3
+
4
+ //#region src/layers/ConfigValidationTest.ts
5
+ /**
6
+ * Test-double layer factory for ConfigValidation. Pass a pre-built `ValidationResult` to inject.
7
+ * @public
8
+ */
9
+ const ConfigValidationTest = { layer: (override) => Layer.succeed(ConfigValidation, { validate: () => Effect.succeed(override ?? {
10
+ errors: [],
11
+ warnings: [],
12
+ info: []
13
+ }) }) };
14
+
15
+ //#endregion
16
+ export { ConfigValidationTest };
@@ -0,0 +1,138 @@
1
+ import { CoverageAnalyzer } from "../services/CoverageAnalyzer.js";
2
+ import { compressLines } from "@vitest-agent/sdk";
3
+ import { Effect, Layer, Option } from "effect";
4
+
5
+ //#region src/layers/CoverageAnalyzerLive.ts
6
+ /**
7
+ * Check whether any metric in `stats` falls below its corresponding threshold.
8
+ * Only metrics that are defined in `thresholds` are checked.
9
+ */
10
+ function isBelowMetricThresholds(stats, thresholds) {
11
+ if (thresholds.lines !== void 0 && stats.lines < thresholds.lines) return true;
12
+ if (thresholds.functions !== void 0 && stats.functions < thresholds.functions) return true;
13
+ if (thresholds.branches !== void 0 && stats.branches < thresholds.branches) return true;
14
+ if (thresholds.statements !== void 0 && stats.statements < thresholds.statements) return true;
15
+ return false;
16
+ }
17
+ /**
18
+ * Runtime duck-type check for istanbul CoverageMap.
19
+ */
20
+ function isIstanbulCoverageMap(value) {
21
+ if (value === null || typeof value !== "object") return false;
22
+ const obj = value;
23
+ return typeof obj.getCoverageSummary === "function" && typeof obj.files === "function" && typeof obj.fileCoverageFor === "function";
24
+ }
25
+ /**
26
+ * Match a file path against a glob pattern using basic matching.
27
+ * Supports `*` (any segment chars) and `**` (any path segments).
28
+ */
29
+ function matchGlob(filePath, pattern) {
30
+ const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*").replace(/\?/g, "[^/]");
31
+ return new RegExp(`^${regexStr}$`).test(filePath);
32
+ }
33
+ /**
34
+ * Resolve the effective thresholds for a file path by checking pattern
35
+ * overrides first, falling back to global thresholds.
36
+ */
37
+ function resolveEffectiveThresholds(filePath, resolved) {
38
+ for (const [pattern, metrics] of resolved.patterns) if (matchGlob(filePath, pattern)) return metrics;
39
+ return resolved.global;
40
+ }
41
+ /**
42
+ * Internal coverage processing logic. Shared by both `process` and `processScoped`.
43
+ *
44
+ * @param coverageMap - The value received by `onCoverage`; duck-typed at runtime
45
+ * @param options - Coverage processing options
46
+ * @param testedFiles - When provided, only flag threshold violations for files in this set
47
+ * @returns Structured coverage report, or undefined if duck-typing fails
48
+ */
49
+ function processCoverageInternal(coverageMap, options, testedFiles) {
50
+ if (!isIstanbulCoverageMap(coverageMap)) return void 0;
51
+ const { includeBareZero } = options;
52
+ const scoped = testedFiles !== void 0;
53
+ const summary = coverageMap.getCoverageSummary();
54
+ const totals = {
55
+ statements: summary.statements.pct,
56
+ branches: summary.branches.pct,
57
+ functions: summary.functions.pct,
58
+ lines: summary.lines.pct
59
+ };
60
+ const testedFileSet = testedFiles ? new Set(testedFiles) : void 0;
61
+ const lowCoverage = [];
62
+ const belowTarget = [];
63
+ for (const filePath of coverageMap.files()) {
64
+ const fileCoverage = coverageMap.fileCoverageFor(filePath);
65
+ const fileSummary = fileCoverage.toSummary();
66
+ const fileStats = {
67
+ statements: fileSummary.statements.pct,
68
+ branches: fileSummary.branches.pct,
69
+ functions: fileSummary.functions.pct,
70
+ lines: fileSummary.lines.pct
71
+ };
72
+ const isBareZero = fileStats.statements === 0 && fileStats.branches === 0 && fileStats.functions === 0 && fileStats.lines === 0;
73
+ if (isBareZero && !includeBareZero) continue;
74
+ if (scoped && !testedFileSet?.has(filePath)) continue;
75
+ const isBelowThreshold = isBelowMetricThresholds(fileStats, resolveEffectiveThresholds(filePath, options.thresholds));
76
+ if (isBareZero || isBelowThreshold) {
77
+ const uncoveredLines = compressLines(fileCoverage.getUncoveredLines());
78
+ lowCoverage.push({
79
+ file: filePath,
80
+ summary: fileStats,
81
+ uncoveredLines
82
+ });
83
+ continue;
84
+ }
85
+ if (options.targets) {
86
+ if (isBelowMetricThresholds(fileStats, resolveEffectiveThresholds(filePath, options.targets))) {
87
+ const uncoveredLines = compressLines(fileCoverage.getUncoveredLines());
88
+ belowTarget.push({
89
+ file: filePath,
90
+ summary: fileStats,
91
+ uncoveredLines
92
+ });
93
+ }
94
+ }
95
+ }
96
+ lowCoverage.sort((a, b) => a.summary.lines - b.summary.lines);
97
+ belowTarget.sort((a, b) => a.summary.lines - b.summary.lines);
98
+ return {
99
+ totals,
100
+ thresholds: {
101
+ global: options.thresholds.global,
102
+ patterns: options.thresholds.patterns
103
+ },
104
+ ...options.targets ? { targets: {
105
+ global: options.targets.global,
106
+ patterns: options.targets.patterns
107
+ } } : {},
108
+ ...options.baselines ? { baselines: {
109
+ global: options.baselines.global,
110
+ patterns: options.baselines.patterns
111
+ } } : {},
112
+ scoped,
113
+ ...scoped && testedFiles ? { scopedFiles: [...testedFiles] } : {},
114
+ lowCoverage,
115
+ lowCoverageFiles: lowCoverage.map((f) => f.file),
116
+ ...options.targets ? {
117
+ belowTarget,
118
+ belowTargetFiles: belowTarget.map((f) => f.file)
119
+ } : {}
120
+ };
121
+ }
122
+ /**
123
+ * Live implementation of the CoverageAnalyzer service backed by istanbul.
124
+ * @public
125
+ */
126
+ const CoverageAnalyzerLive = Layer.succeed(CoverageAnalyzer, {
127
+ process: (coverage, options) => Effect.sync(() => {
128
+ const result = processCoverageInternal(coverage, options);
129
+ return result ? Option.some(result) : Option.none();
130
+ }),
131
+ processScoped: (coverage, options, testedFiles) => Effect.sync(() => {
132
+ const result = processCoverageInternal(coverage, options, testedFiles);
133
+ return result ? Option.some(result) : Option.none();
134
+ })
135
+ });
136
+
137
+ //#endregion
138
+ export { CoverageAnalyzerLive };
@@ -0,0 +1,15 @@
1
+ import { CoverageAnalyzer } from "../services/CoverageAnalyzer.js";
2
+ import { Effect, Layer, Option } from "effect";
3
+
4
+ //#region src/layers/CoverageAnalyzerTest.ts
5
+ /**
6
+ * Test-double layer factory for CoverageAnalyzer. Pass a pre-built `CoverageReport` to inject.
7
+ * @public
8
+ */
9
+ const CoverageAnalyzerTest = { layer: (data) => Layer.succeed(CoverageAnalyzer, {
10
+ process: () => Effect.succeed(data ? Option.some(data) : Option.none()),
11
+ processScoped: () => Effect.succeed(data ? Option.some(data) : Option.none())
12
+ }) };
13
+
14
+ //#endregion
15
+ export { CoverageAnalyzerTest };
@@ -0,0 +1,21 @@
1
+ import { CoverageAnalyzerLive } from "./CoverageAnalyzerLive.js";
2
+ import { DataReaderLive, DataStoreLive, HistoryTrackerLive, LoggerLive, OutputPipelineLive, migration0001 } from "@vitest-agent/sdk";
3
+ import { Layer } from "effect";
4
+ import * as NodeContext from "@effect/platform-node/NodeContext";
5
+ import { layer } from "@effect/sql-sqlite-node/SqliteClient";
6
+ import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
7
+
8
+ //#region src/layers/ReporterLive.ts
9
+ /**
10
+ * Composition layer for a single `AgentReporter` run. Wires SQLite, migrations, and all service layers.
11
+ * @public
12
+ */
13
+ const ReporterLive = (dbPath, logLevel, logFile) => {
14
+ const SqliteLayer = layer({ filename: dbPath });
15
+ const PlatformLayer = NodeContext.layer;
16
+ const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": migration0001 }) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
17
+ return Layer.mergeAll(DataStoreLive, CoverageAnalyzerLive, HistoryTrackerLive, OutputPipelineLive).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
18
+ };
19
+
20
+ //#endregion
21
+ export { ReporterLive };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@vitest-agent/plugin",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Vitest plugin for the vitest-agent ecosystem: owns persistence, classification, baselines, trends, and dispatches rendering to a configurable reporter.",
6
+ "keywords": [
7
+ "vitest",
8
+ "vitest-plugin",
9
+ "agent",
10
+ "llm",
11
+ "ai",
12
+ "testing",
13
+ "sqlite",
14
+ "coverage",
15
+ "mcp"
16
+ ],
17
+ "homepage": "https://github.com/spencerbeggs/vitest-agent#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/spencerbeggs/vitest-agent/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/spencerbeggs/vitest-agent.git",
24
+ "directory": "packages/plugin"
25
+ },
26
+ "license": "MIT",
27
+ "author": {
28
+ "name": "C. Spencer Beggs",
29
+ "email": "spencer@beggs.codes",
30
+ "url": "https://spencerbeg.gs"
31
+ },
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./index.d.ts",
36
+ "import": "./index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "dependencies": {
41
+ "@effect/cluster": "^0.59.0",
42
+ "@effect/platform": "^0.96.2",
43
+ "@effect/platform-node": "^0.107.0",
44
+ "@effect/rpc": "^0.75.1",
45
+ "@effect/sql": "^0.51.1",
46
+ "@effect/sql-sqlite-node": "^0.52.0",
47
+ "@vitest-agent/reporter": "1.0.0",
48
+ "@vitest-agent/sdk": "1.0.0",
49
+ "acorn": "^8.17.0",
50
+ "acorn-typescript": "^1.4.13",
51
+ "effect": "^3.21.4",
52
+ "magic-string": "^0.30.21",
53
+ "workspaces-effect": "^1.2.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@vitest-agent/cli": "1.0.0",
57
+ "@vitest-agent/mcp": "1.0.0",
58
+ "@vitest/coverage-istanbul": "^4.1.0",
59
+ "@vitest/coverage-v8": "^4.1.0",
60
+ "vitest": "^4.1.0"
61
+ },
62
+ "engines": {
63
+ "node": ">=24.11.0"
64
+ }
65
+ }