@vitest-agent/sdk 8.0.0 → 9.0.1

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.
@@ -781,6 +781,98 @@
781
781
  "required": ["timestamp", "reason", "summary", "failed", "unhandledErrors", "failedFiles"],
782
782
  "additionalProperties": true
783
783
  },
784
+ "PerformanceHintKind": {
785
+ "type": "string",
786
+ "enum": ["environment", "import", "transform", "isolate"]
787
+ },
788
+ "PerformanceHint": {
789
+ "type": "object",
790
+ "properties": {
791
+ "kind": {
792
+ "$ref": "#/$defs/PerformanceHintKind"
793
+ },
794
+ "project": {
795
+ "type": "string"
796
+ },
797
+ "message": {
798
+ "type": "string"
799
+ },
800
+ "remedy": {
801
+ "type": "string"
802
+ },
803
+ "detail": {
804
+ "type": "string"
805
+ },
806
+ "learnMoreUrl": {
807
+ "type": "string"
808
+ },
809
+ "estimatedSavingMs": {
810
+ "type": "number"
811
+ },
812
+ "share": {
813
+ "type": "number"
814
+ }
815
+ },
816
+ "required": ["kind", "message", "remedy", "detail", "learnMoreUrl"],
817
+ "additionalProperties": true
818
+ },
819
+ "PostRunTiming": {
820
+ "type": "object",
821
+ "properties": {
822
+ "persistMs": {
823
+ "type": "number"
824
+ },
825
+ "coverageScanMs": {
826
+ "type": "number"
827
+ },
828
+ "totalMs": {
829
+ "type": "number"
830
+ },
831
+ "renderMs": {
832
+ "type": "number"
833
+ },
834
+ "vitestDurationMs": {
835
+ "type": "number"
836
+ }
837
+ },
838
+ "required": ["persistMs", "coverageScanMs", "totalMs"],
839
+ "additionalProperties": true
840
+ },
841
+ "PersistenceStatus": {
842
+ "type": "object",
843
+ "properties": {
844
+ "ok": {
845
+ "type": "boolean"
846
+ },
847
+ "reason": {
848
+ "type": "string"
849
+ },
850
+ "migrationFailed": {
851
+ "type": "boolean"
852
+ }
853
+ },
854
+ "required": ["ok"],
855
+ "additionalProperties": true
856
+ },
857
+ "RunDiagnostics": {
858
+ "type": "object",
859
+ "properties": {
860
+ "performanceHints": {
861
+ "type": "array",
862
+ "items": {
863
+ "$ref": "#/$defs/PerformanceHint"
864
+ }
865
+ },
866
+ "postRun": {
867
+ "$ref": "#/$defs/PostRunTiming"
868
+ },
869
+ "persistence": {
870
+ "$ref": "#/$defs/PersistenceStatus"
871
+ }
872
+ },
873
+ "required": ["performanceHints"],
874
+ "additionalProperties": true
875
+ },
784
876
  "RunReportFile": {
785
877
  "type": "object",
786
878
  "properties": {
@@ -799,6 +891,9 @@
799
891
  "items": {
800
892
  "$ref": "#/$defs/AgentReport"
801
893
  }
894
+ },
895
+ "runDiagnostics": {
896
+ "$ref": "#/$defs/RunDiagnostics"
802
897
  }
803
898
  },
804
899
  "required": ["schemaVersion", "generatedAt", "reports"],
@@ -0,0 +1,61 @@
1
+ import { FiniteNumber } from "./Common.js";
2
+ import { Schema } from "effect";
3
+
4
+ // src/schemas/RunDiagnostics.ts
5
+ /**
6
+ * Which Vitest performance diagnostic a hint came from.
7
+ * @public
8
+ */
9
+ const PerformanceHintKind = Schema.Literals([
10
+ "environment",
11
+ "import",
12
+ "transform",
13
+ "isolate"
14
+ ]).annotate({ identifier: "PerformanceHintKind" });
15
+ /**
16
+ * One performance hint derived from Vitest's run diagnostics. `project` is
17
+ * omitted for a hint that applies to the whole run (for example `isolate`).
18
+ * @public
19
+ */
20
+ const PerformanceHint = Schema.Struct({
21
+ kind: PerformanceHintKind,
22
+ project: Schema.optionalKey(Schema.String),
23
+ message: Schema.String,
24
+ remedy: Schema.String,
25
+ detail: Schema.String,
26
+ learnMoreUrl: Schema.String,
27
+ estimatedSavingMs: Schema.optionalKey(FiniteNumber),
28
+ share: Schema.optionalKey(FiniteNumber)
29
+ }).annotate({ identifier: "PerformanceHint" });
30
+ /**
31
+ * Wall-clock timing of the plugin's post-run work, in milliseconds.
32
+ * @public
33
+ */
34
+ const PostRunTiming = Schema.Struct({
35
+ persistMs: FiniteNumber,
36
+ coverageScanMs: FiniteNumber,
37
+ totalMs: FiniteNumber,
38
+ renderMs: Schema.optionalKey(FiniteNumber),
39
+ vitestDurationMs: Schema.optionalKey(FiniteNumber)
40
+ }).annotate({ identifier: "PostRunTiming" });
41
+ /**
42
+ * Whether the run's results were persisted to the database.
43
+ * @public
44
+ */
45
+ const PersistenceStatus = Schema.Struct({
46
+ ok: Schema.Boolean,
47
+ reason: Schema.optionalKey(Schema.String),
48
+ migrationFailed: Schema.optionalKey(Schema.Boolean)
49
+ }).annotate({ identifier: "PersistenceStatus" });
50
+ /**
51
+ * Run-level diagnostics: performance hints, post-run timing and persistence
52
+ * status (issue #544).
53
+ * @public
54
+ */
55
+ const RunDiagnostics = Schema.Struct({
56
+ performanceHints: Schema.Array(PerformanceHint),
57
+ postRun: Schema.optionalKey(PostRunTiming),
58
+ persistence: Schema.optionalKey(PersistenceStatus)
59
+ }).annotate({ identifier: "RunDiagnostics" });
60
+
61
+ export { PerformanceHint, PerformanceHintKind, PersistenceStatus, PostRunTiming, RunDiagnostics };
@@ -1,4 +1,5 @@
1
1
  import { AgentReport } from "./AgentReport.js";
2
+ import { RunDiagnostics } from "./RunDiagnostics.js";
2
3
  import { Schema } from "effect";
3
4
 
4
5
  // src/schemas/RunReportFile.ts
@@ -47,7 +48,8 @@ const RunReportFile = Schema.Struct({
47
48
  $schema: Schema.optionalKey(Schema.String),
48
49
  schemaVersion: Schema.Literal(1),
49
50
  generatedAt: IsoInstant,
50
- reports: Schema.Array(AgentReport)
51
+ reports: Schema.Array(AgentReport),
52
+ runDiagnostics: Schema.optionalKey(RunDiagnostics)
51
53
  }).annotate({
52
54
  identifier: "RunReportFile",
53
55
  description: "The vitest-agent run report file (.vitest/vitest-agent/run.json): one AgentReport per Vitest project from the most recent run. Read reports[].summary for pass/fail counts and reports[].failed for the failing modules and their tests. schemaVersion is the envelope contract version; generatedAt is when the run finished."
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.60.0"
9
9
  }
10
10
  ]
11
11
  }
package/utils/ansi.js CHANGED
@@ -1,38 +1,4 @@
1
1
  // src/utils/ansi.ts
2
- const ANSI_CODES = {
3
- reset: [0, 0],
4
- bold: [1, 22],
5
- dim: [2, 22],
6
- red: [31, 39],
7
- green: [32, 39],
8
- yellow: [33, 39],
9
- cyan: [36, 39]
10
- };
11
- /**
12
- * Wrap text with an ANSI color/style escape code.
13
- *
14
- * No-ops when `options.noColor` is true, keeping output as valid plain
15
- * markdown when captured by agents or piped to files.
16
- *
17
- * @privateRemarks
18
- * Uses raw ANSI escape sequences (`\x1b[...m`) rather than a dependency
19
- * like `chalk` or `picocolors` to keep the package zero-dependency. The
20
- * `noColor` flag respects the `NO_COLOR` environment variable convention.
21
- *
22
- * @param text - Text to wrap
23
- * @param color - ANSI code name: `reset`, `bold`, `dim`, `red`, `green`, `yellow`, `cyan`
24
- * @param options - Control color output
25
- * @returns Text with ANSI escape codes, or plain text if noColor
26
- *
27
- * @see {@link https://no-color.org/ | NO_COLOR convention}
28
- * @internal
29
- */
30
- function ansi(text, color, options) {
31
- if (options?.noColor) return text;
32
- const codes = ANSI_CODES[color];
33
- if (!codes) return text;
34
- return `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m`;
35
- }
36
2
  /**
37
3
  * Strip all ANSI escape codes from a string.
38
4
  *
@@ -45,4 +11,4 @@ function stripAnsi(text) {
45
11
  return text.replace(/\x1b\[[0-9;]*m/g, "");
46
12
  }
47
13
 
48
- export { ansi, stripAnsi };
14
+ export { stripAnsi };
@@ -0,0 +1,58 @@
1
+ // src/utils/run-diagnostics-actions.ts
2
+ /**
3
+ * Post-run work at or above this many milliseconds is always "slow".
4
+ * @public
5
+ */
6
+ const POST_RUN_SLOW_MS = 5e3;
7
+ /**
8
+ * Post-run work at or above this share of Vitest's own duration is "slow"
9
+ * (once it also clears {@link POST_RUN_SLOW_MIN_MS}).
10
+ * @public
11
+ */
12
+ const POST_RUN_SLOW_SHARE = .1;
13
+ /**
14
+ * Minimum post-run milliseconds before the share rule applies.
15
+ * @public
16
+ */
17
+ const POST_RUN_SLOW_MIN_MS = 1e3;
18
+ /**
19
+ * Whether the plugin's post-run work was slow enough to surface.
20
+ * @public
21
+ */
22
+ function isPostRunSlow(timing) {
23
+ if (timing.totalMs >= 5e3) return true;
24
+ return timing.totalMs >= 1e3 && timing.vitestDurationMs !== void 0 && timing.totalMs >= .1 * timing.vitestDurationMs;
25
+ }
26
+ const fmt = (ms) => `${(ms / 1e3).toFixed(1)}s`;
27
+ /**
28
+ * Map run diagnostics onto suggested actions: persistence failure first, then
29
+ * slow post-run work, then performance hints in input order.
30
+ * @public
31
+ */
32
+ function suggestedActionsFromRunDiagnostics(d) {
33
+ const out = [];
34
+ if (d.persistence !== void 0 && !d.persistence.ok) {
35
+ const { reason, migrationFailed } = d.persistence;
36
+ out.push({
37
+ severity: "warn",
38
+ title: "Results were not saved",
39
+ detail: migrationFailed ? `${reason ?? "unknown error"}. Run \`vitest-agent db reset\` to recreate the database.` : reason ?? "unknown error"
40
+ });
41
+ }
42
+ if (d.postRun !== void 0 && isPostRunSlow(d.postRun)) {
43
+ const { totalMs, persistMs, coverageScanMs } = d.postRun;
44
+ out.push({
45
+ severity: "warn",
46
+ title: `Post-run work took ${fmt(totalMs)} (persist ${fmt(persistMs)}, coverage scan ${fmt(coverageScanMs)})`,
47
+ detail: "Time vitest-agent spent after Vitest finished. Set persist: false where history is not needed (e.g. CI), or narrow coverage.include."
48
+ });
49
+ }
50
+ for (const hint of d.performanceHints) out.push({
51
+ severity: "info",
52
+ title: hint.project !== void 0 ? `${hint.project}: ${hint.message}` : hint.message,
53
+ detail: `${hint.remedy} — ${hint.detail} (${hint.learnMoreUrl})`
54
+ });
55
+ return out;
56
+ }
57
+
58
+ export { POST_RUN_SLOW_MIN_MS, POST_RUN_SLOW_MS, POST_RUN_SLOW_SHARE, isPostRunSlow, suggestedActionsFromRunDiagnostics };
@@ -0,0 +1,24 @@
1
+ // src/utils/vitest-config-files.ts
2
+ /**
3
+ * The config filenames Vitest 5 itself loads, in the order it prefers them:
4
+ * `vitest.config.*` before `vite.config.*`, each across
5
+ * `ts` / `mts` / `cts` / `js` / `mjs` / `cjs`. Mirrors `configFiles` in
6
+ * Vitest's own `constants.ts`. `vitest.workspace.*` is absent on purpose:
7
+ * Vitest no longer reads a workspace file (projects live in the config's
8
+ * `test.projects`).
9
+ *
10
+ * The single source both front ends derive their config lookups from, so the
11
+ * CLI's `check-test-path` and the MCP `run_tests` root walk cannot drift.
12
+ *
13
+ * @public
14
+ */
15
+ const VITEST_CONFIG_FILES = ["vitest.config", "vite.config"].flatMap((name) => [
16
+ "ts",
17
+ "mts",
18
+ "cts",
19
+ "js",
20
+ "mjs",
21
+ "cjs"
22
+ ].map((ext) => `${name}.${ext}`));
23
+
24
+ export { VITEST_CONFIG_FILES };
package/version.js CHANGED
@@ -11,6 +11,6 @@
11
11
  * survives into the published artifact.
12
12
  * @public
13
13
  */
14
- const CURRENT_SDK_VERSION = "8.0.0";
14
+ const CURRENT_SDK_VERSION = "9.0.1";
15
15
 
16
16
  export { CURRENT_SDK_VERSION };
@@ -1,44 +0,0 @@
1
- // src/utils/compress-lines.ts
2
- /**
3
- * Compress an array of line numbers into a compact range string.
4
- *
5
- * Consecutive line numbers are collapsed into `start-end` ranges,
6
- * separated by commas. Duplicate values are removed and the input
7
- * is sorted ascending before processing.
8
- *
9
- * @param lines - Array of line numbers to compress
10
- * @returns Compressed range string, or empty string for empty input
11
- *
12
- * @example
13
- * ```typescript
14
- * import { compressLines } from "@vitest-agent/sdk/utils";
15
- *
16
- * compressLines([1, 2, 3, 5, 10, 11, 12]);
17
- * // Returns: "1-3,5,10-12"
18
- *
19
- * compressLines([42]);
20
- * // Returns: "42"
21
- *
22
- * compressLines([]);
23
- * // Returns: ""
24
- * ```
25
- *
26
- * @public
27
- */
28
- function compressLines(lines) {
29
- if (lines.length === 0) return "";
30
- const sorted = [...new Set(lines)].sort((a, b) => a - b);
31
- const ranges = [];
32
- let start = sorted[0];
33
- let end = sorted[0];
34
- for (let i = 1; i < sorted.length; i++) if (sorted[i] === end + 1) end = sorted[i];
35
- else {
36
- ranges.push(start === end ? `${start}` : `${start}-${end}`);
37
- start = sorted[i];
38
- end = sorted[i];
39
- }
40
- ranges.push(start === end ? `${start}` : `${start}-${end}`);
41
- return ranges.join(",");
42
- }
43
-
44
- export { compressLines };
@@ -1,57 +0,0 @@
1
- // src/utils/detect-pm.ts
2
- const LOCKFILE_MAP = [
3
- ["pnpm-lock.yaml", "pnpm"],
4
- ["package-lock.json", "npm"],
5
- ["yarn.lock", "yarn"],
6
- ["bun.lock", "bun"]
7
- ];
8
- const RUN_COMMANDS = {
9
- pnpm: "pnpm vitest run",
10
- npm: "npx vitest run",
11
- yarn: "yarn vitest run",
12
- bun: "bun vitest run"
13
- };
14
- /**
15
- * Detect the package manager used in a project by inspecting the root directory.
16
- *
17
- * @remarks
18
- * Detection order:
19
- * 1. The `packageManager` field in `package.json` (e.g., `"pnpm\@10.32.1"`)
20
- * 2. Lockfile presence: `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lock`
21
- *
22
- * Returns `null` when no package manager can be determined.
23
- *
24
- * @param root - Absolute path to the project root directory
25
- * @param fs - Filesystem adapter for reading files and checking existence
26
- * @returns The detected package manager, or `null`
27
- *
28
- * @internal
29
- */
30
- async function detectPackageManager(root, fs) {
31
- try {
32
- const raw = await fs.readFile(`${root}/package.json`);
33
- const pkg = JSON.parse(raw);
34
- if (pkg.packageManager) {
35
- const name = pkg.packageManager.split("@")[0];
36
- if (name in RUN_COMMANDS) return name;
37
- }
38
- } catch {}
39
- for (const [lockfile, pm] of LOCKFILE_MAP) if (await fs.exists(`${root}/${lockfile}`)) return pm;
40
- return null;
41
- }
42
- /**
43
- * Return the vitest run command for the given package manager.
44
- *
45
- * Falls back to `"npx vitest run"` when `pm` is `null`.
46
- *
47
- * @param pm - Detected package manager, or `null` for unknown
48
- * @returns Shell command string for running vitest
49
- *
50
- * @internal
51
- */
52
- function getRunCommand(pm) {
53
- if (!pm) return "npx vitest run";
54
- return RUN_COMMANDS[pm];
55
- }
56
-
57
- export { detectPackageManager, getRunCommand };
@@ -1,222 +0,0 @@
1
- import { ansi } from "./ansi.js";
2
- import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
3
-
4
- // src/utils/format-console.ts
5
- /**
6
- * Return the metric with the lowest percentage from a `FileCoverageReport`.
7
- *
8
- * @remarks
9
- * Used to surface the single worst coverage metric per file in console output,
10
- * keeping the display compact. Labels are abbreviated: `"Stmts"`, `"Branch"`,
11
- * `"Funcs"`, `"Lines"`.
12
- *
13
- * @param f - File coverage report to analyze
14
- * @returns Object with `label` and `pct` of the worst metric
15
- *
16
- * @internal
17
- */
18
- function getWorstMetric(f) {
19
- return [
20
- {
21
- label: "Stmts",
22
- pct: f.summary.statements
23
- },
24
- {
25
- label: "Branch",
26
- pct: f.summary.branches
27
- },
28
- {
29
- label: "Funcs",
30
- pct: f.summary.functions
31
- },
32
- {
33
- label: "Lines",
34
- pct: f.summary.lines
35
- }
36
- ].reduce((worst, m) => m.pct < worst.pct ? m : worst);
37
- }
38
- /**
39
- * Strip the working directory prefix from a file path to produce a
40
- * project-relative path for display.
41
- *
42
- * @param filePath - Absolute or relative file path
43
- * @param cwd - Working directory to strip
44
- * @returns Project-relative path string
45
- *
46
- * @internal
47
- */
48
- function relativePath(filePath, cwd) {
49
- if (filePath.startsWith(cwd)) {
50
- const rel = filePath.slice(cwd.length);
51
- return rel.startsWith("/") ? rel.slice(1) : rel;
52
- }
53
- return filePath;
54
- }
55
- /**
56
- * Determine the output tier based on test results and coverage state.
57
- *
58
- * - `"red"` -- failures, unhandled errors, or files below threshold
59
- * - `"yellow"` -- all tests pass but coverage targets exist (room to improve)
60
- * - `"green"` -- all tests pass, all coverage targets met
61
- *
62
- * @internal
63
- */
64
- function determineTier(report) {
65
- if (report.summary.failed > 0 || report.unhandledErrors.length > 0) return "red";
66
- if (report.coverage) {
67
- if (report.coverage.lowCoverage.length > 0) return "red";
68
- if (report.coverage.targets && report.coverage.belowTarget && report.coverage.belowTarget.length > 0) return "yellow";
69
- }
70
- return "green";
71
- }
72
- /**
73
- * Format an {@link (AgentReport:variable)} as compact, actionable console markdown.
74
- *
75
- * @remarks
76
- * The output is structured for LLM agent consumption with four sections:
77
- *
78
- * 1. **Compact header** with pass/fail counts and duration
79
- * 2. **Failed test details** with error messages and diffs (the core value)
80
- * 3. **Coverage gaps** showing only files below threshold, worst metric first
81
- * 4. **Next steps** with re-run commands and cache file pointer
82
- *
83
- * When all tests pass, the output collapses to a single success line
84
- * with an optional cache file reference.
85
- *
86
- * Returns an empty string when `consoleOutput` is `"silent"`.
87
- *
88
- * @param report - The report to format
89
- * @param options - Formatting options controlling verbosity and color
90
- * @returns Formatted markdown string, or empty string for silent mode
91
- *
92
- * @internal
93
- */
94
- function formatConsoleMarkdown(report, options) {
95
- const { consoleOutput, coverageConsoleLimit, noColor, cwd } = options;
96
- if (consoleOutput === "silent") return "";
97
- const ao = { noColor };
98
- const lines = [];
99
- const hasFailures = report.summary.failed > 0;
100
- const duration = Math.round(report.summary.duration);
101
- const projectLabel = report.project ? ` (${report.project})` : "";
102
- if (hasFailures) lines.push(`## ${ansi("✗", "red", ao)} Vitest${projectLabel} \u2014 ${report.summary.failed} failed, ${report.summary.passed} passed (${duration}ms)`);
103
- else lines.push(`## ${ansi("✓", "green", ao)} Vitest${projectLabel} \u2014 ${report.summary.passed} passed (${duration}ms)`);
104
- lines.push("");
105
- if (options.trendSummary && report.coverage) {
106
- const { direction, runCount, firstMetric } = options.trendSummary;
107
- const tier = determineTier(report);
108
- if (tier === "green") {
109
- const trendNote = `Coverage: all targets met | trending ${direction} over ${runCount} runs`;
110
- lines[0] = `${lines[0]} — ${trendNote}`;
111
- } else if (tier === "yellow") {
112
- const belowTargetCount = report.coverage.belowTarget?.length ?? 0;
113
- let summary = `Coverage: ${belowTargetCount} file${belowTargetCount !== 1 ? "s" : ""} below target`;
114
- summary += ` | trending ${direction} over ${runCount} runs`;
115
- if (firstMetric) {
116
- summary += ` (${firstMetric.name}: ${firstMetric.from}% -> ${firstMetric.to}%`;
117
- if (firstMetric.target) summary += `, target: ${firstMetric.target}%`;
118
- summary += ")";
119
- }
120
- lines.push(summary);
121
- lines.push("");
122
- } else {
123
- let summary = "Coverage:";
124
- if (report.coverage.lowCoverage.length > 0) summary += ` ${report.coverage.lowCoverage.length} file${report.coverage.lowCoverage.length !== 1 ? "s" : ""} below threshold`;
125
- summary += ` | trending ${direction} over ${runCount} runs`;
126
- if (firstMetric) summary += ` (${firstMetric.name}: ${firstMetric.from}% -> ${firstMetric.to}%)`;
127
- lines.push(summary);
128
- lines.push("");
129
- }
130
- }
131
- if (hasFailures) for (const mod of report.failed) {
132
- const file = relativePath(mod.file, cwd);
133
- lines.push(`### ${ansi("✗", "red", ao)} \`${file}\``);
134
- lines.push("");
135
- if (mod.errors && mod.errors.length > 0) for (const err of mod.errors) {
136
- lines.push(ansi(err.message, "red", ao));
137
- if (err.diff) {
138
- lines.push("");
139
- lines.push("```diff");
140
- lines.push(err.diff);
141
- lines.push("```");
142
- }
143
- lines.push("");
144
- }
145
- const failedTests = mod.tests.filter((t) => t.state === "failed");
146
- for (const test of failedTests) {
147
- const label = test.classification ? ` [${test.classification}]` : "";
148
- lines.push(`- ${ansi("✗", "red", ao)} **${test.fullName}**${label}`);
149
- if (test.errors && test.errors.length > 0) for (const err of test.errors) {
150
- lines.push(` ${ansi(err.message, "dim", ao)}`);
151
- if (err.diff) {
152
- lines.push("");
153
- lines.push(" ```diff");
154
- for (const diffLine of err.diff.split("\n")) lines.push(` ${diffLine}`);
155
- lines.push(" ```");
156
- }
157
- }
158
- lines.push("");
159
- }
160
- }
161
- if (report.coverage) {
162
- const cov = report.coverage;
163
- const globalThresholds = cov.thresholds.global;
164
- if (cov.scoped) {
165
- lines.push(formatScopedCoverageNote(cov.scopedFiles?.length ?? 0, cov.totalFiles));
166
- lines.push("");
167
- }
168
- const filesToShow = cov.lowCoverage.slice(0, coverageConsoleLimit);
169
- if (filesToShow.length > 0) {
170
- lines.push(`### Coverage gaps`);
171
- lines.push("");
172
- for (const f of filesToShow) {
173
- const file = relativePath(f.file, cwd);
174
- const worst = getWorstMetric(f);
175
- const thresholdValue = globalThresholds[worst.label === "Stmts" ? "statements" : worst.label === "Branch" ? "branches" : worst.label === "Funcs" ? "functions" : "lines"];
176
- const thresholdSuffix = thresholdValue != null ? ` (threshold: ${thresholdValue}%)` : "";
177
- lines.push(`- \`${file}\` \u2014 ${worst.label}: ${worst.pct}%${thresholdSuffix} \u2014 uncovered: ${f.uncoveredLines}`);
178
- }
179
- lines.push("");
180
- }
181
- if (cov.targets && cov.belowTarget && cov.belowTarget.length > 0) {
182
- lines.push(`### Coverage improvements needed`);
183
- lines.push("");
184
- for (const f of cov.belowTarget.slice(0, coverageConsoleLimit)) {
185
- const file = relativePath(f.file, cwd);
186
- const worst = getWorstMetric(f);
187
- const metricKey = worst.label === "Stmts" ? "statements" : worst.label === "Branch" ? "branches" : worst.label === "Funcs" ? "functions" : "lines";
188
- const targetValue = cov.targets.global[metricKey];
189
- const targetSuffix = targetValue != null ? ` (target: ${targetValue}%)` : "";
190
- lines.push(`- \`${file}\` \u2014 ${worst.label}: ${worst.pct}%${targetSuffix} \u2014 uncovered: ${f.uncoveredLines}`);
191
- }
192
- lines.push("");
193
- }
194
- }
195
- if (hasFailures || report.coverage && report.coverage.lowCoverage.length > 0) {
196
- lines.push("### Next steps");
197
- lines.push("");
198
- if (hasFailures) {
199
- const allFailedTests = report.failed.flatMap((m) => m.tests.filter((t) => t.state === "failed"));
200
- const newFailures = allFailedTests.filter((t) => t.classification === "new-failure");
201
- const persistent = allFailedTests.filter((t) => t.classification === "persistent");
202
- const flaky = allFailedTests.filter((t) => t.classification === "flaky");
203
- if (newFailures.length > 0) {
204
- const files = [...new Set(report.failed.filter((m) => m.tests.some((t) => t.classification === "new-failure")).map((m) => relativePath(m.file, cwd)))];
205
- lines.push(`- Fix ${newFailures.length} new failure${newFailures.length > 1 ? "s" : ""} in ${files.map((f) => `\`${f}\``).join(", ")} (likely caused by recent changes)`);
206
- }
207
- if (persistent.length > 0) lines.push(`- ${persistent.length} persistent failure${persistent.length > 1 ? "s" : ""} (pre-existing, may not be yours)`);
208
- if (flaky.length > 0) lines.push(`- ${flaky.length} flaky test${flaky.length > 1 ? "s" : ""} (may pass on retry)`);
209
- for (const file of report.failedFiles) lines.push(`- Re-run: \`vitest run ${relativePath(file, cwd)}\``);
210
- if (options.mcp) {
211
- lines.push("- Use `test_history` for failure trends");
212
- lines.push("- Use `test_errors` to search errors by type");
213
- lines.push("- Use `test_coverage` for coverage gap analysis");
214
- lines.push("- Use `note({ action: \"create\" })` to record debugging findings");
215
- }
216
- }
217
- lines.push("");
218
- }
219
- return lines.join("\n");
220
- }
221
-
222
- export { determineTier, formatConsoleMarkdown, getWorstMetric, relativePath };