@vitest-agent/sdk 7.1.0 → 9.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/errors/ProjectIdentityError.js +25 -1
- package/index.d.ts +701 -751
- package/index.js +8 -14
- package/package.json +1 -1
- package/schemas/5.0/run.json +113 -222
- package/schemas/AgentReport.js +12 -12
- package/schemas/Common.js +22 -1
- package/schemas/ConsoleLeaks.js +6 -5
- package/schemas/Coverage.js +6 -5
- package/schemas/RenderState.js +8 -1
- package/schemas/RunDiagnostics.js +61 -0
- package/schemas/RunEvent.js +8 -1
- package/schemas/RunReportFile.js +3 -1
- package/schemas/StrayOutput.js +5 -4
- package/schemas/Thresholds.js +5 -4
- package/tsdoc-metadata.json +1 -1
- package/utils/ansi.js +1 -35
- package/utils/console-leaks.js +27 -1
- package/utils/run-diagnostics-actions.js +58 -0
- package/utils/vitest-config-files.js +24 -0
- package/version.js +1 -1
- package/utils/compress-lines.js +0 -44
- package/utils/detect-pm.js +0 -57
- package/utils/format-console.js +0 -222
- package/utils/format-fatal-error.js +0 -46
- package/utils/format-gfm.js +0 -160
- package/utils/hyperlink.js +0 -10
- package/utils/normalize-workspace-key.js +0 -33
- package/utils/probe-host-metadata.js +0 -98
- package/utils/safe-filename.js +0 -33
package/utils/format-console.js
DELETED
|
@@ -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 };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Cause } from "effect";
|
|
2
|
-
|
|
3
|
-
// src/utils/format-fatal-error.ts
|
|
4
|
-
const ISSUE_URL = "https://github.com/spencerbeggs/vitest-agent/issues";
|
|
5
|
-
const FIBER_FAILURE_CAUSE_KEY = Symbol.for("effect/Runtime/FiberFailure/Cause");
|
|
6
|
-
/**
|
|
7
|
-
* Format an unknown error into a human-readable string with issue URL.
|
|
8
|
-
*
|
|
9
|
-
* Extraction order:
|
|
10
|
-
* 1. Effect Cause — use `Cause.pretty()` for full formatting
|
|
11
|
-
* 2. Effect FiberFailure — extract Cause and use `Cause.pretty()`
|
|
12
|
-
* 3. Plain Error — extract `.message` and `.stack`
|
|
13
|
-
* 4. Unknown — `String(err)`
|
|
14
|
-
*
|
|
15
|
-
* @public
|
|
16
|
-
*/
|
|
17
|
-
function formatFatalError(err) {
|
|
18
|
-
let detail;
|
|
19
|
-
if (Cause.isCause(err)) detail = Cause.pretty(err);
|
|
20
|
-
else if (err != null && typeof err === "object" && FIBER_FAILURE_CAUSE_KEY in err) {
|
|
21
|
-
const cause = err[FIBER_FAILURE_CAUSE_KEY];
|
|
22
|
-
if (Cause.isCause(cause)) detail = Cause.pretty(cause);
|
|
23
|
-
else detail = String(err);
|
|
24
|
-
} else if (err instanceof Error) try {
|
|
25
|
-
detail = err.stack ?? err.message;
|
|
26
|
-
} catch {
|
|
27
|
-
let ctorName = "Error";
|
|
28
|
-
try {
|
|
29
|
-
ctorName = err.constructor?.name ?? "Error";
|
|
30
|
-
} catch {}
|
|
31
|
-
detail = `<unserializable ${ctorName}>`;
|
|
32
|
-
}
|
|
33
|
-
else if (err !== null && typeof err === "object") try {
|
|
34
|
-
detail = JSON.stringify(err, null, 2);
|
|
35
|
-
} catch {
|
|
36
|
-
try {
|
|
37
|
-
detail = String(err);
|
|
38
|
-
} catch {
|
|
39
|
-
detail = "<unserializable error>";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else detail = String(err);
|
|
43
|
-
return `${detail}\nPlease report at ${ISSUE_URL}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export { formatFatalError };
|
package/utils/format-gfm.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// src/utils/format-gfm.ts
|
|
2
|
-
/**
|
|
3
|
-
* Format a number as a percentage string.
|
|
4
|
-
*
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
function pct(n) {
|
|
8
|
-
return `${n}%`;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Find the worst (lowest) coverage metric for a file.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
function worstCoverage(f) {
|
|
16
|
-
return [
|
|
17
|
-
{
|
|
18
|
-
label: "Stmts",
|
|
19
|
-
pct: f.summary.statements
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
label: "Branch",
|
|
23
|
-
pct: f.summary.branches
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: "Funcs",
|
|
27
|
-
pct: f.summary.functions
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
label: "Lines",
|
|
31
|
-
pct: f.summary.lines
|
|
32
|
-
}
|
|
33
|
-
].reduce((worst, m) => m.pct < worst.pct ? m : worst);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Format the body of a single project's GFM output.
|
|
37
|
-
*
|
|
38
|
-
* @internal
|
|
39
|
-
*/
|
|
40
|
-
function formatProjectBody(report) {
|
|
41
|
-
const lines = [];
|
|
42
|
-
if (report.failed.length > 0) {
|
|
43
|
-
lines.push("## Failed Tests");
|
|
44
|
-
lines.push("");
|
|
45
|
-
for (const mod of report.failed) {
|
|
46
|
-
lines.push(`### \`${mod.file}\``);
|
|
47
|
-
lines.push("");
|
|
48
|
-
if (mod.errors && mod.errors.length > 0) for (const err of mod.errors) {
|
|
49
|
-
lines.push(`> ${err.message}`);
|
|
50
|
-
if (err.diff) {
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push("```diff");
|
|
53
|
-
lines.push(err.diff);
|
|
54
|
-
lines.push("```");
|
|
55
|
-
}
|
|
56
|
-
lines.push("");
|
|
57
|
-
}
|
|
58
|
-
const failedTests = mod.tests.filter((t) => t.state === "failed");
|
|
59
|
-
for (const test of failedTests) {
|
|
60
|
-
lines.push(`**${test.fullName}**`);
|
|
61
|
-
lines.push("");
|
|
62
|
-
if (test.errors && test.errors.length > 0) for (const err of test.errors) {
|
|
63
|
-
lines.push(`> ${err.message}`);
|
|
64
|
-
if (err.diff) {
|
|
65
|
-
lines.push("");
|
|
66
|
-
lines.push("```diff");
|
|
67
|
-
lines.push(err.diff);
|
|
68
|
-
lines.push("```");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
lines.push("");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (report.coverage) {
|
|
76
|
-
const cov = report.coverage;
|
|
77
|
-
lines.push("## Coverage");
|
|
78
|
-
lines.push("");
|
|
79
|
-
lines.push("| Metric | Coverage |");
|
|
80
|
-
lines.push("| --- | --- |");
|
|
81
|
-
lines.push(`| Statements | ${pct(cov.totals.statements)} |`);
|
|
82
|
-
lines.push(`| Branches | ${pct(cov.totals.branches)} |`);
|
|
83
|
-
lines.push(`| Functions | ${pct(cov.totals.functions)} |`);
|
|
84
|
-
lines.push(`| Lines | ${pct(cov.totals.lines)} |`);
|
|
85
|
-
lines.push("");
|
|
86
|
-
if (cov.lowCoverage.length > 0) {
|
|
87
|
-
const thresholdParts = [];
|
|
88
|
-
const g = cov.thresholds.global;
|
|
89
|
-
if (g.lines !== void 0) thresholdParts.push(`lines: ${g.lines}%`);
|
|
90
|
-
if (g.functions !== void 0) thresholdParts.push(`functions: ${g.functions}%`);
|
|
91
|
-
if (g.branches !== void 0) thresholdParts.push(`branches: ${g.branches}%`);
|
|
92
|
-
if (g.statements !== void 0) thresholdParts.push(`statements: ${g.statements}%`);
|
|
93
|
-
const thresholdDisplay = thresholdParts.length > 0 ? thresholdParts.join(", ") : "configured thresholds";
|
|
94
|
-
lines.push("> [!WARNING]");
|
|
95
|
-
lines.push(`> ${cov.lowCoverage.length} file(s) below coverage threshold (${thresholdDisplay}):`);
|
|
96
|
-
lines.push(">");
|
|
97
|
-
for (const f of cov.lowCoverage) {
|
|
98
|
-
const worst = worstCoverage(f);
|
|
99
|
-
lines.push(`> - \`${f.file}\` — ${worst.label} ${pct(worst.pct)}`);
|
|
100
|
-
}
|
|
101
|
-
lines.push("");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return lines;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Format one or more {@link (AgentReport:variable) | AgentReports} as a GitHub-Flavored
|
|
108
|
-
* Markdown string for writing to `GITHUB_STEP_SUMMARY`.
|
|
109
|
-
*
|
|
110
|
-
* @remarks
|
|
111
|
-
* GFM-specific features used in the output:
|
|
112
|
-
*
|
|
113
|
-
* - **Summary table** with pass/fail counts across all projects
|
|
114
|
-
* - **Collapsible `<details>` blocks** for per-project results in monorepo setups
|
|
115
|
-
* - **Tables** for coverage metrics
|
|
116
|
-
* - **Diff-fenced code blocks** for expected/received comparison output
|
|
117
|
-
* - **GitHub Alert callouts** (`[!WARNING]`) for coverage threshold violations
|
|
118
|
-
*
|
|
119
|
-
* For a single report, the project body is rendered directly without
|
|
120
|
-
* a `<details>` wrapper. For multiple reports (monorepo), each project
|
|
121
|
-
* gets its own collapsible section with a status icon in the summary line.
|
|
122
|
-
*
|
|
123
|
-
* @param reports - One or more agent reports to format
|
|
124
|
-
* @returns GFM-formatted string ready for appending to `GITHUB_STEP_SUMMARY`
|
|
125
|
-
*
|
|
126
|
-
* @public
|
|
127
|
-
*/
|
|
128
|
-
function formatGfm(reports) {
|
|
129
|
-
const lines = [];
|
|
130
|
-
const headerIcon = reports.some((r) => r.summary.failed > 0) ? "❌" : "✅";
|
|
131
|
-
lines.push(`# ${headerIcon} Vitest Results`);
|
|
132
|
-
lines.push("");
|
|
133
|
-
const totalPassed = reports.reduce((acc, r) => acc + r.summary.passed, 0);
|
|
134
|
-
const totalFailed = reports.reduce((acc, r) => acc + r.summary.failed, 0);
|
|
135
|
-
const totalTests = reports.reduce((acc, r) => acc + r.summary.total, 0);
|
|
136
|
-
lines.push("| | Count |");
|
|
137
|
-
lines.push("| --- | --- |");
|
|
138
|
-
lines.push(`| Passed | **${totalPassed}** |`);
|
|
139
|
-
lines.push(`| Failed | **${totalFailed}** |`);
|
|
140
|
-
lines.push(`| Total | **${totalTests}** |`);
|
|
141
|
-
lines.push("");
|
|
142
|
-
if (reports.length > 1) for (const report of reports) {
|
|
143
|
-
const projectLabel = report.project ?? "default";
|
|
144
|
-
const statusIcon = report.summary.failed > 0 ? "❌" : "✅";
|
|
145
|
-
lines.push(`<details>`);
|
|
146
|
-
lines.push(`<summary>${statusIcon} ${projectLabel}</summary>`);
|
|
147
|
-
lines.push("");
|
|
148
|
-
const bodyLines = formatProjectBody(report);
|
|
149
|
-
lines.push(...bodyLines);
|
|
150
|
-
lines.push("</details>");
|
|
151
|
-
lines.push("");
|
|
152
|
-
}
|
|
153
|
-
else if (reports.length === 1) {
|
|
154
|
-
const bodyLines = formatProjectBody(reports[0]);
|
|
155
|
-
lines.push(...bodyLines);
|
|
156
|
-
}
|
|
157
|
-
return lines.join("\n");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export { formatGfm };
|
package/utils/hyperlink.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// src/utils/normalize-workspace-key.ts
|
|
2
|
-
/**
|
|
3
|
-
* Normalize a workspace name into a filesystem-safe directory segment.
|
|
4
|
-
*
|
|
5
|
-
* The workspace key drives the per-project subdirectory under the XDG data
|
|
6
|
-
* directory (e.g. `$XDG_DATA_HOME/vitest-agent/<key>/data.db`). The
|
|
7
|
-
* key must be deterministic for a given input and safe to use as a single
|
|
8
|
-
* path segment on macOS, Linux, and Windows.
|
|
9
|
-
*
|
|
10
|
-
* Rules (applied in order):
|
|
11
|
-
*
|
|
12
|
-
* 1. Replace `/` with `__` so scoped names like `@org/pkg` collapse to
|
|
13
|
-
* `@org__pkg` rather than introducing a subdirectory boundary. Scoped
|
|
14
|
-
* npm names contain at most one `/` (between scope and name), and
|
|
15
|
-
* leading-`@` requires the scope/name form, so `@org__pkg` is not a
|
|
16
|
-
* valid unscoped npm name -- the `__` token is unambiguous in
|
|
17
|
-
* practice.
|
|
18
|
-
* 2. Replace any character outside `[A-Za-z0-9._@-]` with `_`. This catches
|
|
19
|
-
* Windows-reserved characters (`\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`),
|
|
20
|
-
* control characters, whitespace, and anything else that varies in
|
|
21
|
-
* interpretation across filesystems.
|
|
22
|
-
* 3. Collapse runs of underscores produced by the substitution above into a
|
|
23
|
-
* single `_` so the output stays compact.
|
|
24
|
-
*
|
|
25
|
-
* @param name - The raw `name` field from a workspace's `package.json`.
|
|
26
|
-
* @returns A filesystem-safe single path segment.
|
|
27
|
-
* @public
|
|
28
|
-
*/
|
|
29
|
-
function normalizeWorkspaceKey(name) {
|
|
30
|
-
return name.replaceAll("/", "__").replace(/[^A-Za-z0-9._@-]/g, "_").replace(/_{3,}/g, "__");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export { normalizeWorkspaceKey };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// src/utils/probe-host-metadata.ts
|
|
2
|
-
const isUseful = (value) => value !== void 0 && value.length > 0;
|
|
3
|
-
/**
|
|
4
|
-
* Walk the priority chain of env-var probes. Returns the first match
|
|
5
|
-
* along with any decorating metadata (`term_program`, `ci_provider`,
|
|
6
|
-
* etc.) for that source. When no probe matches, `source`/`value`/
|
|
7
|
-
* `metadata` are all `null`.
|
|
8
|
-
*
|
|
9
|
-
* The probe order matches the plan's table exactly:
|
|
10
|
-
*
|
|
11
|
-
* 1. `TMUX_PANE` (tmux reattach-stable)
|
|
12
|
-
* 2. `WT_SESSION` (Windows Terminal pane)
|
|
13
|
-
* 3. `WEZTERM_PANE` (WezTerm pane)
|
|
14
|
-
* 4. `KITTY_WINDOW_ID` (kitty window)
|
|
15
|
-
* 5. `TERM_SESSION_ID` (iTerm2, Terminal.app)
|
|
16
|
-
* 6. `VSCODE_INJECTION` + `process.ppid` (handled by the Live layer
|
|
17
|
-
* since it needs `process.ppid`, not env)
|
|
18
|
-
* 7. `GITHUB_RUN_ID` + `GITHUB_RUN_ATTEMPT`
|
|
19
|
-
* 8. `BUILDKITE_JOB_ID` / `CIRCLE_BUILD_NUM` / `GITLAB_CI_JOB_ID`
|
|
20
|
-
* 9. `process.ppid` walk (handled by the Live layer)
|
|
21
|
-
* @public
|
|
22
|
-
*/
|
|
23
|
-
const probeHostMetadataFromEnv = (env) => {
|
|
24
|
-
const decoration = (extra) => {
|
|
25
|
-
const baseDecoration = {};
|
|
26
|
-
if (isUseful(env.TERM_PROGRAM)) baseDecoration.term_program = env.TERM_PROGRAM;
|
|
27
|
-
if (isUseful(env.TERM_PROGRAM_VERSION)) baseDecoration.term_program_version = env.TERM_PROGRAM_VERSION;
|
|
28
|
-
return {
|
|
29
|
-
...baseDecoration,
|
|
30
|
-
...extra
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
if (isUseful(env.TMUX_PANE)) return {
|
|
34
|
-
source: "TMUX_PANE",
|
|
35
|
-
value: env.TMUX_PANE,
|
|
36
|
-
metadata: decoration(isUseful(env.TMUX) ? { tmux_socket: env.TMUX } : {})
|
|
37
|
-
};
|
|
38
|
-
if (isUseful(env.WT_SESSION)) return {
|
|
39
|
-
source: "WT_SESSION",
|
|
40
|
-
value: env.WT_SESSION,
|
|
41
|
-
metadata: decoration({})
|
|
42
|
-
};
|
|
43
|
-
if (isUseful(env.WEZTERM_PANE)) return {
|
|
44
|
-
source: "WEZTERM_PANE",
|
|
45
|
-
value: env.WEZTERM_PANE,
|
|
46
|
-
metadata: decoration({})
|
|
47
|
-
};
|
|
48
|
-
if (isUseful(env.KITTY_WINDOW_ID)) return {
|
|
49
|
-
source: "KITTY_WINDOW_ID",
|
|
50
|
-
value: env.KITTY_WINDOW_ID,
|
|
51
|
-
metadata: decoration({})
|
|
52
|
-
};
|
|
53
|
-
if (isUseful(env.TERM_SESSION_ID)) return {
|
|
54
|
-
source: "TERM_SESSION_ID",
|
|
55
|
-
value: env.TERM_SESSION_ID,
|
|
56
|
-
metadata: decoration({})
|
|
57
|
-
};
|
|
58
|
-
if (isUseful(env.GITHUB_RUN_ID)) return {
|
|
59
|
-
source: "GITHUB_RUN_ID",
|
|
60
|
-
value: env.GITHUB_RUN_ID,
|
|
61
|
-
metadata: decoration({
|
|
62
|
-
ci: true,
|
|
63
|
-
ci_provider: "github",
|
|
64
|
-
...isUseful(env.GITHUB_RUN_ATTEMPT) ? { github_run_attempt: env.GITHUB_RUN_ATTEMPT } : {}
|
|
65
|
-
})
|
|
66
|
-
};
|
|
67
|
-
if (isUseful(env.BUILDKITE_JOB_ID)) return {
|
|
68
|
-
source: "BUILDKITE_JOB_ID",
|
|
69
|
-
value: env.BUILDKITE_JOB_ID,
|
|
70
|
-
metadata: decoration({
|
|
71
|
-
ci: true,
|
|
72
|
-
ci_provider: "buildkite"
|
|
73
|
-
})
|
|
74
|
-
};
|
|
75
|
-
if (isUseful(env.CIRCLE_BUILD_NUM)) return {
|
|
76
|
-
source: "CIRCLE_BUILD_NUM",
|
|
77
|
-
value: env.CIRCLE_BUILD_NUM,
|
|
78
|
-
metadata: decoration({
|
|
79
|
-
ci: true,
|
|
80
|
-
ci_provider: "circle"
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
if (isUseful(env.GITLAB_CI_JOB_ID)) return {
|
|
84
|
-
source: "GITLAB_CI_JOB_ID",
|
|
85
|
-
value: env.GITLAB_CI_JOB_ID,
|
|
86
|
-
metadata: decoration({
|
|
87
|
-
ci: true,
|
|
88
|
-
ci_provider: "gitlab"
|
|
89
|
-
})
|
|
90
|
-
};
|
|
91
|
-
return {
|
|
92
|
-
source: null,
|
|
93
|
-
value: null,
|
|
94
|
-
metadata: null
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export { probeHostMetadataFromEnv };
|
package/utils/safe-filename.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// src/utils/safe-filename.ts
|
|
2
|
-
/**
|
|
3
|
-
* Convert a project name to a filesystem-safe filename.
|
|
4
|
-
*
|
|
5
|
-
* Replaces `/` and `:` characters with `__` (double underscore).
|
|
6
|
-
* Returns `"default"` for empty strings, which is used as the
|
|
7
|
-
* fallback project name for single-repo configurations.
|
|
8
|
-
*
|
|
9
|
-
* @param name - Project name to sanitize
|
|
10
|
-
* @returns Filesystem-safe filename string
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { safeFilename } from "@vitest-agent/sdk/utils";
|
|
15
|
-
*
|
|
16
|
-
* safeFilename("\@savvy-web/my-lib:unit");
|
|
17
|
-
* // Returns: "\@savvy-web__my-lib__unit"
|
|
18
|
-
*
|
|
19
|
-
* safeFilename("core");
|
|
20
|
-
* // Returns: "core"
|
|
21
|
-
*
|
|
22
|
-
* safeFilename("");
|
|
23
|
-
* // Returns: "default"
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @public
|
|
27
|
-
*/
|
|
28
|
-
function safeFilename(name) {
|
|
29
|
-
if (!name) return "default";
|
|
30
|
-
return name.replace(/[/:]/g, "__");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export { safeFilename };
|