@vitest-agent/ui 5.0.9 → 5.1.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.
- package/dispatcher/cells/single-file-fail.js +2 -2
- package/dispatcher/cells/single-project-fail.js +2 -2
- package/dispatcher/cells/single-project-threshold.js +2 -2
- package/dispatcher/cells/single-test-fail.js +2 -2
- package/dispatcher/cells/workspace-fail.js +2 -2
- package/dispatcher/cells/workspace-threshold.js +2 -2
- package/dispatcher/dispatch.js +37 -4
- package/dispatcher/helpers.js +44 -16
- package/index.d.ts +240 -190
- package/index.js +1 -1
- package/ink/CoverageBlock.js +3 -1
- package/ink/FailureSection.js +3 -1
- package/ink/FailuresSection.js +5 -3
- package/ink/FileLink.js +24 -0
- package/ink/StreamApp.js +34 -16
- package/ink/dispatch-ink.js +11 -4
- package/ink.d.ts +129 -64
- package/ink.js +2 -1
- package/package.json +4 -4
- package/reducer.js +2 -1
- package/render-agent.js +10 -14
- package/synthesize.js +2 -1
- package/tsdoc-metadata.json +1 -1
|
@@ -4,7 +4,7 @@ import { formatFailure, soleModulePath } from "../helpers.js";
|
|
|
4
4
|
|
|
5
5
|
// src/dispatcher/cells/single-file-fail.ts
|
|
6
6
|
const DEFAULT_WIDTH = 80;
|
|
7
|
-
const renderAgent = (inputs) => {
|
|
7
|
+
const renderAgent = (inputs, opts) => {
|
|
8
8
|
const modulePath = soleModulePath(inputs.state);
|
|
9
9
|
if (modulePath === void 0) return "";
|
|
10
10
|
const { passCount, failCount, skipCount, timeoutCount, durationMs } = inputs.state.totals;
|
|
@@ -15,7 +15,7 @@ const renderAgent = (inputs) => {
|
|
|
15
15
|
const sections = [`${modulePath}: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`];
|
|
16
16
|
if (inputs.state.failures.length > 0) {
|
|
17
17
|
sections.push("Failures:");
|
|
18
|
-
for (const failure of inputs.state.failures) sections.push(...formatFailure(failure, DEFAULT_WIDTH));
|
|
18
|
+
for (const failure of inputs.state.failures) sections.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
|
|
19
19
|
}
|
|
20
20
|
return `${sections.join("\n")}\n${buildFooter(inputs)}`;
|
|
21
21
|
};
|
|
@@ -3,11 +3,11 @@ import { formatFailure, formatTotals } from "../helpers.js";
|
|
|
3
3
|
|
|
4
4
|
// src/dispatcher/cells/single-project-fail.ts
|
|
5
5
|
const DEFAULT_WIDTH = 80;
|
|
6
|
-
const renderAgent = (inputs) => {
|
|
6
|
+
const renderAgent = (inputs, opts) => {
|
|
7
7
|
const sections = [[formatTotals(inputs.state)]];
|
|
8
8
|
if (inputs.state.failures.length > 0) {
|
|
9
9
|
const failureLines = ["Failures:"];
|
|
10
|
-
for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH));
|
|
10
|
+
for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
|
|
11
11
|
sections.push(failureLines);
|
|
12
12
|
}
|
|
13
13
|
const moduleLines = ["Modules:"];
|
|
@@ -3,7 +3,7 @@ import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, forma
|
|
|
3
3
|
|
|
4
4
|
// src/dispatcher/cells/single-project-threshold.ts
|
|
5
5
|
const BELOW_TARGET_LIMIT = 5;
|
|
6
|
-
const renderAgent = (inputs) => {
|
|
6
|
+
const renderAgent = (inputs, opts) => {
|
|
7
7
|
const sections = [[formatTotals(inputs.state)]];
|
|
8
8
|
const coverage = formatCoverageSummaryLines(inputs.state);
|
|
9
9
|
const trend = formatTrendLine(inputs.trend);
|
|
@@ -11,7 +11,7 @@ const renderAgent = (inputs) => {
|
|
|
11
11
|
summaryLines.push(...coverage);
|
|
12
12
|
if (trend !== null) summaryLines.push(trend);
|
|
13
13
|
if (summaryLines.length > 0) sections.push(summaryLines);
|
|
14
|
-
const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
|
|
14
|
+
const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT, opts.link);
|
|
15
15
|
if (below.length > 0) sections.push([...below]);
|
|
16
16
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
17
17
|
};
|
|
@@ -5,13 +5,13 @@ import { formatFailure, formatTestName, soleTest } from "../helpers.js";
|
|
|
5
5
|
|
|
6
6
|
// src/dispatcher/cells/single-test-fail.ts
|
|
7
7
|
const DEFAULT_WIDTH = 80;
|
|
8
|
-
const renderAgent = (inputs) => {
|
|
8
|
+
const renderAgent = (inputs, opts) => {
|
|
9
9
|
const test = soleTest(inputs.state);
|
|
10
10
|
if (test === void 0) return "";
|
|
11
11
|
const duration = test.durationMs ?? 0;
|
|
12
12
|
const header = `${statusGlyph("failure")} ${formatTestName(test)} (${formatDisplayDuration(duration)})`;
|
|
13
13
|
const failure = inputs.state.failures[0];
|
|
14
|
-
return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
|
|
14
|
+
return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH, opts.link).join("\n")}\n`}${buildFooter(inputs)}`;
|
|
15
15
|
};
|
|
16
16
|
const renderSingleTestFail = { agent: renderAgent };
|
|
17
17
|
|
|
@@ -3,13 +3,13 @@ import { formatFailure, formatProjectsTable, formatTrendLine, formatWorkspaceTot
|
|
|
3
3
|
|
|
4
4
|
// src/dispatcher/cells/workspace-fail.ts
|
|
5
5
|
const DEFAULT_WIDTH = 80;
|
|
6
|
-
const renderAgent = (inputs) => {
|
|
6
|
+
const renderAgent = (inputs, opts) => {
|
|
7
7
|
const sections = [];
|
|
8
8
|
const projects = formatProjectsTable(inputs.projects);
|
|
9
9
|
if (projects.length > 0) sections.push([...projects]);
|
|
10
10
|
if (inputs.state.failures.length > 0) {
|
|
11
11
|
const failureLines = ["Failures:"];
|
|
12
|
-
for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH));
|
|
12
|
+
for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
|
|
13
13
|
sections.push(failureLines);
|
|
14
14
|
}
|
|
15
15
|
const trend = formatTrendLine(inputs.trend);
|
|
@@ -3,7 +3,7 @@ import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable
|
|
|
3
3
|
|
|
4
4
|
// src/dispatcher/cells/workspace-threshold.ts
|
|
5
5
|
const BELOW_TARGET_LIMIT = 10;
|
|
6
|
-
const renderAgent = (inputs) => {
|
|
6
|
+
const renderAgent = (inputs, opts) => {
|
|
7
7
|
const sections = [];
|
|
8
8
|
const projects = formatProjectsTable(inputs.projects);
|
|
9
9
|
if (projects.length > 0) sections.push([...projects]);
|
|
@@ -13,7 +13,7 @@ const renderAgent = (inputs) => {
|
|
|
13
13
|
const trend = formatTrendLine(inputs.trend);
|
|
14
14
|
if (trend !== null) summaryLines.push(trend);
|
|
15
15
|
if (summaryLines.length > 0) sections.push(summaryLines);
|
|
16
|
-
const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
|
|
16
|
+
const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT, opts.link);
|
|
17
17
|
if (below.length > 0) sections.push([...below]);
|
|
18
18
|
if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
|
|
19
19
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
package/dispatcher/dispatch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatSuggestedActionLines } from "./helpers.js";
|
|
1
2
|
import { renderSingleFileFail } from "./cells/single-file-fail.js";
|
|
2
3
|
import { renderSingleFilePass } from "./cells/single-file-pass.js";
|
|
3
4
|
import { renderSingleFileThreshold } from "./cells/single-file-threshold.js";
|
|
@@ -10,7 +11,7 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
|
|
|
10
11
|
import { renderWorkspaceFail } from "./cells/workspace-fail.js";
|
|
11
12
|
import { renderWorkspacePass } from "./cells/workspace-pass.js";
|
|
12
13
|
import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
|
|
13
|
-
import { formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
|
|
14
|
+
import { formatConsoleLeaksNote, formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
|
|
14
15
|
|
|
15
16
|
// src/dispatcher/dispatch.ts
|
|
16
17
|
/**
|
|
@@ -55,8 +56,40 @@ const dispatch = (inputs, opts) => {
|
|
|
55
56
|
const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
|
|
56
57
|
const note = scopedCoverageNoteFor(inputs);
|
|
57
58
|
const withNote = note !== null ? `${body}\n${note}` : body;
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const trailers = [
|
|
60
|
+
suggestedActionsNoteFor(inputs),
|
|
61
|
+
strayOutputNoteFor(inputs),
|
|
62
|
+
consoleLeaksNoteFor(inputs)
|
|
63
|
+
].filter((trailer) => trailer !== null);
|
|
64
|
+
return trailers.length === 0 ? withNote : `${withNote}\n${trailers.join("\n\n")}\n`;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The suggested-actions list for the run, or `null` when there is nothing to
|
|
68
|
+
* suggest. `dispatch` and `dispatchInk` append it after the cell's output;
|
|
69
|
+
* cells never emit it themselves.
|
|
70
|
+
*
|
|
71
|
+
* @param inputs - the classified dispatch inputs
|
|
72
|
+
* @returns the list's lines joined, or `null`
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
const suggestedActionsNoteFor = (inputs) => {
|
|
76
|
+
const lines = formatSuggestedActionLines(inputs.state.suggestedActions);
|
|
77
|
+
return lines.length === 0 ? null : lines.join("\n");
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The console-leaks note for a run whose passing tests wrote `console.*`
|
|
81
|
+
* output, or `null` when none did. Appended after the cell's output like the
|
|
82
|
+
* stray-output note; cells never emit it themselves.
|
|
83
|
+
*
|
|
84
|
+
* @param inputs - the classified dispatch inputs
|
|
85
|
+
* @returns the note's lines joined, or `null`
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
const consoleLeaksNoteFor = (inputs) => {
|
|
89
|
+
const leaks = inputs.state.consoleLeaks;
|
|
90
|
+
if (leaks === void 0) return null;
|
|
91
|
+
const lines = formatConsoleLeaksNote(leaks);
|
|
92
|
+
return lines.length === 0 ? null : lines.join("\n");
|
|
60
93
|
};
|
|
61
94
|
/**
|
|
62
95
|
* The stray-output note for a run that wrote past Vitest's console capture,
|
|
@@ -88,4 +121,4 @@ const scopedCoverageNoteFor = (inputs) => {
|
|
|
88
121
|
return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
|
|
89
122
|
};
|
|
90
123
|
|
|
91
|
-
export { dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor };
|
|
124
|
+
export { consoleLeaksNoteFor, dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor, suggestedActionsNoteFor };
|
package/dispatcher/helpers.js
CHANGED
|
@@ -57,23 +57,30 @@ const formatTestName = (test) => {
|
|
|
57
57
|
* by default to keep the agent string compact.
|
|
58
58
|
*
|
|
59
59
|
* @remarks
|
|
60
|
-
* A kit document rendered
|
|
61
|
-
* item whose title is
|
|
62
|
-
*
|
|
60
|
+
* A kit document rendered at colour `none` (so no paint, whatever the
|
|
61
|
+
* audience): one compact list item whose title line is never wrapped and
|
|
62
|
+
* opens with the test file as a `Doc.link` (written through `link`, so a
|
|
63
|
+
* human at a terminal with hyperlink support gets an OSC 8 link to the
|
|
64
|
+
* file and everyone else the path alone), whose first message line is a
|
|
65
|
+
* truncated `Doc.line`, and whose diff is `Doc.diffText` with `truncate`.
|
|
63
66
|
* Each content line is cut to `width - 2` (floor 20) after the item indent.
|
|
64
67
|
* The kit sanitizes the text, so a tab becomes a space and an escape
|
|
65
|
-
* sequence is removed.
|
|
68
|
+
* sequence is removed — which is why the link is a `Doc.link` the renderer
|
|
69
|
+
* writes, never an escape pasted into the text.
|
|
66
70
|
*/
|
|
67
|
-
const formatFailure = (f, width) => {
|
|
71
|
+
const formatFailure = (f, width, link = (_target, label) => label) => {
|
|
68
72
|
const suite = f.suitePath.length > 0 ? `${f.suitePath.join(" > ")} > ` : "";
|
|
69
73
|
const classification = f.classification !== null ? ` [${f.classification}]` : "";
|
|
70
|
-
const parts = [Doc.
|
|
74
|
+
const parts = [Doc.line([Doc.link({ file: f.modulePath }, f.modulePath, { suffix: false }), Doc.text(` > ${suite}${f.testName}${classification}`)], { wrap: false })];
|
|
71
75
|
if (f.error?.message !== void 0) parts.push(Doc.line(f.error.message.split("\n", 1)[0] ?? "", { truncate: true }));
|
|
72
76
|
if (f.error?.diff !== void 0) parts.push(Doc.diffText(f.error.diff, { truncate: true }));
|
|
73
|
-
return Render.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
return Render.ansi([Doc.list([Doc.section(void 0, parts)], { compact: true })], {
|
|
78
|
+
...Render.contextOf({
|
|
79
|
+
audience: "human",
|
|
80
|
+
width: Math.max(22, width)
|
|
81
|
+
}),
|
|
82
|
+
link: (target, label) => "file" in target ? link(target, label) : label
|
|
83
|
+
}).split("\n");
|
|
77
84
|
};
|
|
78
85
|
/**
|
|
79
86
|
* The `Glob aggregates below threshold:` block — one
|
|
@@ -200,9 +207,10 @@ const TABLE_COL_FILE_MIN = 60;
|
|
|
200
207
|
* (never narrower than {@link TABLE_COL_FILE_MIN}), so a printed path is
|
|
201
208
|
* never truncated (issue #237 follow-up) — only the omitted rows (past
|
|
202
209
|
* `limit`) are unaffected, since their width never entered the
|
|
203
|
-
* calculation.
|
|
210
|
+
* calculation. Each path is written through `link` after the column is
|
|
211
|
+
* padded, so a hyperlink wraps the path alone and never the padding.
|
|
204
212
|
*/
|
|
205
|
-
const formatBelowTargetTable = (belowTarget, limit) => {
|
|
213
|
+
const formatBelowTargetTable = (belowTarget, limit, link = (_target, label) => label) => {
|
|
206
214
|
if (belowTarget.length === 0) return [];
|
|
207
215
|
const top = belowTarget.slice(0, limit);
|
|
208
216
|
const omitted = belowTarget.length - top.length;
|
|
@@ -213,7 +221,7 @@ const formatBelowTargetTable = (belowTarget, limit) => {
|
|
|
213
221
|
buildTableHeader(fileColWidth),
|
|
214
222
|
buildTableSeparator(fileColWidth)
|
|
215
223
|
];
|
|
216
|
-
const rows = top.map((file) => buildTableRow(file, fileColWidth));
|
|
224
|
+
const rows = top.map((file) => buildTableRow(file, fileColWidth, link));
|
|
217
225
|
const footer = [];
|
|
218
226
|
if (omitted > 0) footer.push(`… ${omitted} more (use the test_coverage MCP tool for the full list)`);
|
|
219
227
|
return [
|
|
@@ -228,8 +236,8 @@ const buildTableSeparator = (fileColWidth) => {
|
|
|
228
236
|
const buildTableHeader = (fileColWidth) => {
|
|
229
237
|
return ` ${"File".padEnd(fileColWidth - 1)}| % Stmts | % Branch| % Funcs | % Lines | Uncovered Line #s`;
|
|
230
238
|
};
|
|
231
|
-
const buildTableRow = (file, fileColWidth) => {
|
|
232
|
-
return `${` ${file.file.
|
|
239
|
+
const buildTableRow = (file, fileColWidth, link) => {
|
|
240
|
+
return `${` ${link({ file: file.file }, file.file)}${" ".repeat(Math.max(0, fileColWidth - 1 - file.file.length))}`}|${pctCell(file.summary.statements)}|${pctCell(file.summary.branches)}|${pctCell(file.summary.functions)}|${pctCell(file.summary.lines)}|${` ${file.uncoveredLines}`}`;
|
|
233
241
|
};
|
|
234
242
|
const pctCell = (n) => {
|
|
235
243
|
const text = `${Math.round(n)}`;
|
|
@@ -238,5 +246,25 @@ const pctCell = (n) => {
|
|
|
238
246
|
const right = pad - left;
|
|
239
247
|
return `${" ".repeat(left + 1)}${text}${" ".repeat(right)}`;
|
|
240
248
|
};
|
|
249
|
+
const SEVERITY_LABEL = {
|
|
250
|
+
info: "info",
|
|
251
|
+
warn: "warn",
|
|
252
|
+
blocker: "blocker"
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* The suggested-actions block as a compact list: `Actions:` then, per action,
|
|
256
|
+
* `- <severity>: <title> (tool: <targetTool>)` with the detail indented under
|
|
257
|
+
* it. Empty when there is nothing to suggest. Shared by `renderAgent` and the
|
|
258
|
+
* dispatcher, so the one-shot and cell paths print the same list (issue #544).
|
|
259
|
+
*/
|
|
260
|
+
const formatSuggestedActionLines = (actions) => {
|
|
261
|
+
if (actions.length === 0) return [];
|
|
262
|
+
const lines = ["Actions:"];
|
|
263
|
+
for (const action of actions) {
|
|
264
|
+
const tool = action.targetTool !== void 0 ? ` (tool: ${action.targetTool})` : "";
|
|
265
|
+
lines.push(`- ${SEVERITY_LABEL[action.severity]}: ${action.title}${tool}`, ` ${action.detail}`);
|
|
266
|
+
}
|
|
267
|
+
return lines;
|
|
268
|
+
};
|
|
241
269
|
|
|
242
|
-
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|
|
270
|
+
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatSuggestedActionLines, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|