@vitest-agent/ui 4.0.1 → 5.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/README.md +3 -3
- package/dispatcher/cells/single-file-fail.js +1 -5
- package/dispatcher/cells/single-file-pass.js +1 -5
- package/dispatcher/cells/single-file-threshold.js +3 -8
- package/dispatcher/cells/single-project-fail.js +1 -5
- package/dispatcher/cells/single-project-pass.js +1 -5
- package/dispatcher/cells/single-project-threshold.js +4 -8
- package/dispatcher/cells/single-test-fail.js +1 -5
- package/dispatcher/cells/single-test-pass.js +1 -5
- package/dispatcher/cells/workspace-fail.js +1 -5
- package/dispatcher/cells/workspace-pass.js +1 -5
- package/dispatcher/cells/workspace-threshold.js +4 -8
- package/dispatcher/classify.js +6 -1
- package/dispatcher/dispatch.js +20 -25
- package/dispatcher/helpers.js +52 -3
- package/index.d.ts +148 -316
- package/index.js +5 -18
- package/{render-ink → ink}/CountColumns.js +2 -2
- package/{render-ink → ink}/CoverageBlock.js +2 -2
- package/{render-ink → ink}/FailureSection.js +2 -2
- package/{render-ink → ink}/FailuresSection.js +2 -2
- package/{render-ink → ink}/ModuleHeader.js +1 -1
- package/{render-ink → ink}/ProjectRow.js +1 -1
- package/{render-ink → ink}/StatusIcon.js +2 -2
- package/{render-ink → ink}/StreamApp.js +14 -5
- package/{render-ink → ink}/SuggestedActions.js +2 -2
- package/{render-ink → ink}/TagColumns.js +1 -1
- package/{render-ink → ink}/TestRow.js +1 -1
- package/{render-ink → ink}/TrendLine.js +2 -2
- package/ink/dispatch-ink.js +61 -0
- package/{dispatcher → ink}/ink-helpers.js +1 -1
- package/ink.d.ts +789 -0
- package/ink.js +15 -0
- package/package.json +12 -7
- package/reducer.js +4 -2
- package/render-agent.js +4 -1
- package/{render-ink/spinner.js → spinner.js} +1 -1
- package/synthesize.js +22 -7
- package/render-ink/index.js +0 -15
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ Pure rendering primitives for vitest-agent. Owns the `RunEvent` reducer, the sha
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Reducer** — pure `(state, event) => state` fold over the `RunEvent` discriminated union; `reduceRenderStateAll` for one-shot replay
|
|
14
|
-
- **Dispatcher matrix** — `dispatch`
|
|
15
|
-
- **Ink components** — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames
|
|
14
|
+
- **Dispatcher matrix** — `dispatch` routes a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
|
|
15
|
+
- **Ink components** (`@vitest-agent/ui/ink`) — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames, plus `dispatchInk` / `inkDispatcherTable`, the Ink half of the matrix. The package root never loads React or Ink; only this subpath does
|
|
16
16
|
- **Synthesizers** — `synthesizeRunEvents` from live Vitest modules; `synthesizeFromAgentReport` from a stored `AgentReport`
|
|
17
17
|
- **Footer builder** — `buildFooter` assembles the L1 MCP-tool-pointer footer; `dominantClassification` picks the most actionable failure class
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ npm install @vitest-agent/ui
|
|
|
24
24
|
pnpm add @vitest-agent/ui
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
React and Ink are
|
|
27
|
+
React and Ink are optional peers, needed only for the `@vitest-agent/ui/ink` subpath:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npm install react ink
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { formatFailure, soleModulePath } from "../helpers.js";
|
|
4
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
4
|
|
|
6
5
|
//#region src/dispatcher/cells/single-file-fail.ts
|
|
7
6
|
const DEFAULT_WIDTH = 80;
|
|
@@ -20,10 +19,7 @@ const renderAgent = (inputs) => {
|
|
|
20
19
|
}
|
|
21
20
|
return `${sections.join("\n")}\n${buildFooter(inputs)}`;
|
|
22
21
|
};
|
|
23
|
-
const renderSingleFileFail = {
|
|
24
|
-
agent: renderAgent,
|
|
25
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
26
|
-
};
|
|
22
|
+
const renderSingleFileFail = { agent: renderAgent };
|
|
27
23
|
|
|
28
24
|
//#endregion
|
|
29
25
|
export { renderSingleFileFail };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { soleModulePath } from "../helpers.js";
|
|
4
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
4
|
import { Fmt } from "@effected/cli";
|
|
6
5
|
|
|
7
6
|
//#region src/dispatcher/cells/single-file-pass.ts
|
|
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
|
|
|
11
10
|
const { passCount, durationMs } = inputs.state.totals;
|
|
12
11
|
return `${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})\n${buildFooter(inputs)}`;
|
|
13
12
|
};
|
|
14
|
-
const renderSingleFilePass = {
|
|
15
|
-
agent: renderAgent,
|
|
16
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
17
|
-
};
|
|
13
|
+
const renderSingleFilePass = { agent: renderAgent };
|
|
18
14
|
|
|
19
15
|
//#endregion
|
|
20
16
|
export { renderSingleFilePass };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
|
-
import {
|
|
4
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
3
|
+
import { formatCoverageSummaryLines, soleModulePath } from "../helpers.js";
|
|
5
4
|
import { Fmt } from "@effected/cli";
|
|
6
5
|
|
|
7
6
|
//#region src/dispatcher/cells/single-file-threshold.ts
|
|
@@ -10,14 +9,10 @@ const renderAgent = (inputs) => {
|
|
|
10
9
|
if (modulePath === void 0) return "";
|
|
11
10
|
const { passCount, durationMs } = inputs.state.totals;
|
|
12
11
|
const lines = [`${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})`];
|
|
13
|
-
|
|
14
|
-
if (coverage !== null) lines.push(coverage);
|
|
12
|
+
lines.push(...formatCoverageSummaryLines(inputs.state));
|
|
15
13
|
return `${lines.join("\n")}\n${buildFooter(inputs)}`;
|
|
16
14
|
};
|
|
17
|
-
const renderSingleFileThreshold = {
|
|
18
|
-
agent: renderAgent,
|
|
19
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
20
|
-
};
|
|
15
|
+
const renderSingleFileThreshold = { agent: renderAgent };
|
|
21
16
|
|
|
22
17
|
//#endregion
|
|
23
18
|
export { renderSingleFileThreshold };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatFailure, formatTotals } from "../helpers.js";
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
3
|
|
|
5
4
|
//#region src/dispatcher/cells/single-project-fail.ts
|
|
6
5
|
const DEFAULT_WIDTH = 80;
|
|
@@ -25,10 +24,7 @@ const renderAgent = (inputs) => {
|
|
|
25
24
|
if (moduleLines.length > 1) sections.push(moduleLines);
|
|
26
25
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
27
26
|
};
|
|
28
|
-
const renderSingleProjectFail = {
|
|
29
|
-
agent: renderAgent,
|
|
30
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
31
|
-
};
|
|
27
|
+
const renderSingleProjectFail = { agent: renderAgent };
|
|
32
28
|
|
|
33
29
|
//#endregion
|
|
34
30
|
export { renderSingleProjectFail };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatTotals } from "../helpers.js";
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
3
|
import { Fmt } from "@effected/cli";
|
|
5
4
|
|
|
6
5
|
//#region src/dispatcher/cells/single-project-pass.ts
|
|
@@ -10,10 +9,7 @@ const renderAgent = (inputs) => {
|
|
|
10
9
|
const collected = inputs.state.collectedModules ?? inputs.state.moduleOrder.length;
|
|
11
10
|
return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${Fmt.plural(collected, "module")} all-passed.`]).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
12
11
|
};
|
|
13
|
-
const renderSingleProjectPass = {
|
|
14
|
-
agent: renderAgent,
|
|
15
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
16
|
-
};
|
|
12
|
+
const renderSingleProjectPass = { agent: renderAgent };
|
|
17
13
|
|
|
18
14
|
//#endregion
|
|
19
15
|
export { renderSingleProjectPass };
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
-
import { formatBelowTargetTable,
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
2
|
+
import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, formatTrendLine } from "../helpers.js";
|
|
4
3
|
|
|
5
4
|
//#region src/dispatcher/cells/single-project-threshold.ts
|
|
6
5
|
const BELOW_TARGET_LIMIT = 5;
|
|
7
6
|
const renderAgent = (inputs) => {
|
|
8
7
|
const sections = [[formatTotals(inputs.state)]];
|
|
9
|
-
const coverage =
|
|
8
|
+
const coverage = formatCoverageSummaryLines(inputs.state);
|
|
10
9
|
const trend = formatTrendLine(inputs.trend);
|
|
11
10
|
const summaryLines = [];
|
|
12
|
-
|
|
11
|
+
summaryLines.push(...coverage);
|
|
13
12
|
if (trend !== null) summaryLines.push(trend);
|
|
14
13
|
if (summaryLines.length > 0) sections.push(summaryLines);
|
|
15
14
|
const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
|
|
16
15
|
if (below.length > 0) sections.push([...below]);
|
|
17
16
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
18
17
|
};
|
|
19
|
-
const renderSingleProjectThreshold = {
|
|
20
|
-
agent: renderAgent,
|
|
21
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
22
|
-
};
|
|
18
|
+
const renderSingleProjectThreshold = { agent: renderAgent };
|
|
23
19
|
|
|
24
20
|
//#endregion
|
|
25
21
|
export { renderSingleProjectThreshold };
|
|
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { statusGlyph } from "../../theme.js";
|
|
3
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
4
4
|
import { formatFailure, formatTestName, soleTest } from "../helpers.js";
|
|
5
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
6
5
|
|
|
7
6
|
//#region src/dispatcher/cells/single-test-fail.ts
|
|
8
7
|
const DEFAULT_WIDTH = 80;
|
|
@@ -14,10 +13,7 @@ const renderAgent = (inputs) => {
|
|
|
14
13
|
const failure = inputs.state.failures[0];
|
|
15
14
|
return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
|
|
16
15
|
};
|
|
17
|
-
const renderSingleTestFail = {
|
|
18
|
-
agent: renderAgent,
|
|
19
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
20
|
-
};
|
|
16
|
+
const renderSingleTestFail = { agent: renderAgent };
|
|
21
17
|
|
|
22
18
|
//#endregion
|
|
23
19
|
export { renderSingleTestFail };
|
|
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { statusGlyph } from "../../theme.js";
|
|
3
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
4
4
|
import { formatTestName, soleTest } from "../helpers.js";
|
|
5
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
6
5
|
|
|
7
6
|
//#region src/dispatcher/cells/single-test-pass.ts
|
|
8
7
|
const renderAgent = (inputs) => {
|
|
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
|
|
|
11
10
|
const duration = test.durationMs ?? 0;
|
|
12
11
|
return `${statusGlyph("success")} ${formatTestName(test)} (${formatDisplayDuration(duration)})\n${buildFooter(inputs)}`;
|
|
13
12
|
};
|
|
14
|
-
const renderSingleTestPass = {
|
|
15
|
-
agent: renderAgent,
|
|
16
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
17
|
-
};
|
|
13
|
+
const renderSingleTestPass = { agent: renderAgent };
|
|
18
14
|
|
|
19
15
|
//#endregion
|
|
20
16
|
export { renderSingleTestPass };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatFailure, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
3
|
|
|
5
4
|
//#region src/dispatcher/cells/workspace-fail.ts
|
|
6
5
|
const DEFAULT_WIDTH = 80;
|
|
@@ -18,10 +17,7 @@ const renderAgent = (inputs) => {
|
|
|
18
17
|
if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
|
|
19
18
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
20
19
|
};
|
|
21
|
-
const renderWorkspaceFail = {
|
|
22
|
-
agent: renderAgent,
|
|
23
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
24
|
-
};
|
|
20
|
+
const renderWorkspaceFail = { agent: renderAgent };
|
|
25
21
|
|
|
26
22
|
//#endregion
|
|
27
23
|
export { renderWorkspaceFail };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatCoverageJudgmentLine, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
3
|
|
|
5
4
|
//#region src/dispatcher/cells/workspace-pass.ts
|
|
6
5
|
const renderAgent = (inputs) => {
|
|
@@ -16,10 +15,7 @@ const renderAgent = (inputs) => {
|
|
|
16
15
|
if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
|
|
17
16
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
18
17
|
};
|
|
19
|
-
const renderWorkspacePass = {
|
|
20
|
-
agent: renderAgent,
|
|
21
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
22
|
-
};
|
|
18
|
+
const renderWorkspacePass = { agent: renderAgent };
|
|
23
19
|
|
|
24
20
|
//#endregion
|
|
25
21
|
export { renderWorkspacePass };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
-
import { formatBelowTargetTable,
|
|
3
|
-
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
2
|
+
import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
|
|
4
3
|
|
|
5
4
|
//#region src/dispatcher/cells/workspace-threshold.ts
|
|
6
5
|
const BELOW_TARGET_LIMIT = 10;
|
|
@@ -9,8 +8,8 @@ const renderAgent = (inputs) => {
|
|
|
9
8
|
const projects = formatProjectsTable(inputs.projects);
|
|
10
9
|
if (projects.length > 0) sections.push([...projects]);
|
|
11
10
|
const summaryLines = [];
|
|
12
|
-
const coverage =
|
|
13
|
-
|
|
11
|
+
const coverage = formatCoverageSummaryLines(inputs.state);
|
|
12
|
+
summaryLines.push(...coverage);
|
|
14
13
|
const trend = formatTrendLine(inputs.trend);
|
|
15
14
|
if (trend !== null) summaryLines.push(trend);
|
|
16
15
|
if (summaryLines.length > 0) sections.push(summaryLines);
|
|
@@ -19,10 +18,7 @@ const renderAgent = (inputs) => {
|
|
|
19
18
|
if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
|
|
20
19
|
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
21
20
|
};
|
|
22
|
-
const renderWorkspaceThreshold = {
|
|
23
|
-
agent: renderAgent,
|
|
24
|
-
ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
|
|
25
|
-
};
|
|
21
|
+
const renderWorkspaceThreshold = { agent: renderAgent };
|
|
26
22
|
|
|
27
23
|
//#endregion
|
|
28
24
|
export { renderWorkspaceThreshold };
|
package/dispatcher/classify.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* UI rewrite spec §7 open question 1:
|
|
8
8
|
*
|
|
9
9
|
* 1. More than one project → `workspace`.
|
|
10
|
+
* 1b. `collectedModules` greater than one → `single-project` (the reduced
|
|
11
|
+
* `modules` map may hold only the failing modules of a larger run).
|
|
10
12
|
* 2. One module with exactly one test → `single-test`.
|
|
11
13
|
* 3. One module with more than one test → `single-file`.
|
|
12
14
|
* 4. Otherwise → `single-project`.
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
*/
|
|
19
21
|
const classifyRunShape = (state, projects) => {
|
|
20
22
|
if (projects.length > 1) return "workspace";
|
|
23
|
+
if (state.collectedModules !== void 0 && state.collectedModules > 1) return "single-project";
|
|
21
24
|
const moduleEntries = Object.values(state.modules);
|
|
22
25
|
if (moduleEntries.length === 1) {
|
|
23
26
|
const sole = moduleEntries[0];
|
|
@@ -50,7 +53,9 @@ const classifyOutcome = (state) => {
|
|
|
50
53
|
if (state.totals.failCount > 0) return "some-fail";
|
|
51
54
|
if (state.unhandledErrors.length > 0) return "some-fail";
|
|
52
55
|
if (state.totals.timeoutCount > 0) return "some-fail";
|
|
53
|
-
if (state.coverage !== null
|
|
56
|
+
if (state.coverage !== null) {
|
|
57
|
+
if (state.coverage.violations.length > 0 || (state.coverage.globShortfalls?.length ?? 0) > 0) return "threshold-violation";
|
|
58
|
+
}
|
|
54
59
|
return "all-pass";
|
|
55
60
|
};
|
|
56
61
|
|
package/dispatcher/dispatch.js
CHANGED
|
@@ -10,9 +10,7 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
|
|
|
10
10
|
import { renderWorkspaceFail } from "./cells/workspace-fail.js";
|
|
11
11
|
import { renderWorkspacePass } from "./cells/workspace-pass.js";
|
|
12
12
|
import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
|
|
13
|
-
import { formatScopedCoverageNote } from "@vitest-agent/sdk";
|
|
14
|
-
import { Box, Text } from "ink";
|
|
15
|
-
import { createElement } from "react";
|
|
13
|
+
import { formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
|
|
16
14
|
|
|
17
15
|
//#region src/dispatcher/dispatch.ts
|
|
18
16
|
/**
|
|
@@ -56,11 +54,27 @@ const dispatcherTable = {
|
|
|
56
54
|
const dispatch = (inputs, opts) => {
|
|
57
55
|
const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
|
|
58
56
|
const note = scopedCoverageNoteFor(inputs);
|
|
59
|
-
|
|
57
|
+
const withNote = note !== null ? `${body}\n${note}` : body;
|
|
58
|
+
const stray = strayOutputNoteFor(inputs);
|
|
59
|
+
return stray !== null ? `${withNote}\n${stray}\n` : withNote;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The stray-output note for a run that wrote past Vitest's console capture,
|
|
63
|
+
* or `null` when it wrote nothing stray. `dispatch` and `dispatchInk` append
|
|
64
|
+
* it after the cell's output (and the scoped-coverage note); cells never emit
|
|
65
|
+
* it themselves.
|
|
66
|
+
*
|
|
67
|
+
* @param inputs - the classified dispatch inputs
|
|
68
|
+
* @returns the note's lines joined, or `null`
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
const strayOutputNoteFor = (inputs) => {
|
|
72
|
+
const stray = inputs.state.strayOutput;
|
|
73
|
+
return stray === void 0 ? null : formatStrayOutputNote(stray).join("\n");
|
|
60
74
|
};
|
|
61
75
|
/**
|
|
62
76
|
* Build the scoped-coverage note for this run, or `null` on a full
|
|
63
|
-
* (non-scoped) run. Shared by {@link dispatch} and
|
|
77
|
+
* (non-scoped) run. Shared by {@link dispatch} and the Ink entry's `dispatchInk` so
|
|
64
78
|
* both render paths surface the same information (issue #160 gap 1) —
|
|
65
79
|
* Vitest's coverage thresholds are meaningless against a subset of the
|
|
66
80
|
* project's test files, so every cell's own threshold-flavored coverage
|
|
@@ -73,25 +87,6 @@ const scopedCoverageNoteFor = (inputs) => {
|
|
|
73
87
|
if (cov === null || cov.scoped !== true) return null;
|
|
74
88
|
return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
|
|
75
89
|
};
|
|
76
|
-
/**
|
|
77
|
-
* Dispatch to the cell that matches `(inputs.shape, inputs.outcome)`
|
|
78
|
-
* and return its Ink-half React tree. Returns `null` when the matched
|
|
79
|
-
* cell does not expose an `ink` half — callers should fall back to
|
|
80
|
-
* the agent string in that case.
|
|
81
|
-
*
|
|
82
|
-
* @param inputs - the classified dispatch inputs
|
|
83
|
-
* @param opts - cell rendering options
|
|
84
|
-
* @returns the Ink React element, or `null` when the cell has no Ink half
|
|
85
|
-
* @public
|
|
86
|
-
*/
|
|
87
|
-
const dispatchInk = (inputs, opts) => {
|
|
88
|
-
const cell = dispatcherTable[inputs.shape][inputs.outcome];
|
|
89
|
-
if (cell.ink === void 0) return null;
|
|
90
|
-
const element = cell.ink(inputs, opts);
|
|
91
|
-
const note = scopedCoverageNoteFor(inputs);
|
|
92
|
-
if (note === null) return element;
|
|
93
|
-
return createElement(Box, { flexDirection: "column" }, element, createElement(Text, null, note));
|
|
94
|
-
};
|
|
95
90
|
|
|
96
91
|
//#endregion
|
|
97
|
-
export { dispatch,
|
|
92
|
+
export { dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor };
|
package/dispatcher/helpers.js
CHANGED
|
@@ -5,6 +5,12 @@ import { Doc, Fmt, Render } from "@effected/cli";
|
|
|
5
5
|
|
|
6
6
|
//#region src/dispatcher/helpers.ts
|
|
7
7
|
/**
|
|
8
|
+
* Format a coverage percentage given on istanbul's 0–100 scale. One
|
|
9
|
+
* decimal place for non-integer values, none for integers, with a
|
|
10
|
+
* trailing percent sign (`Fmt.percent` with `scale: 100`).
|
|
11
|
+
*/
|
|
12
|
+
const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
|
|
13
|
+
/**
|
|
8
14
|
* Format the `Tests:` header line —
|
|
9
15
|
* `<pass>/<total> passed[, <fail> failed][, <timeout> timed out][, <skip> skipped] (Xms)`.
|
|
10
16
|
* `total` folds in `timeoutCount` — a timed-out test is a real collected
|
|
@@ -70,18 +76,61 @@ const formatFailure = (f, width) => {
|
|
|
70
76
|
})).split("\n");
|
|
71
77
|
};
|
|
72
78
|
/**
|
|
79
|
+
* The `Glob aggregates below threshold:` block — one
|
|
80
|
+
* `- <pattern>: lines 60% < 90%, branches 55% < 80%` line per shortfall,
|
|
81
|
+
* listing only the metrics that miss their minimum. Returns an empty array
|
|
82
|
+
* when there are no shortfalls. Shared by the dispatcher cells and
|
|
83
|
+
* `renderAgent` so both agent surfaces use one line format.
|
|
84
|
+
*/
|
|
85
|
+
const formatGlobShortfallLines = (shortfalls) => {
|
|
86
|
+
if (shortfalls === void 0 || shortfalls.length === 0) return [];
|
|
87
|
+
const metricOrder = [
|
|
88
|
+
"lines",
|
|
89
|
+
"branches",
|
|
90
|
+
"functions",
|
|
91
|
+
"statements"
|
|
92
|
+
];
|
|
93
|
+
const lines = ["Glob aggregates below threshold:"];
|
|
94
|
+
for (const g of shortfalls) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (const m of metricOrder) {
|
|
97
|
+
const min = g.thresholds[m];
|
|
98
|
+
const actual = g.summary[m];
|
|
99
|
+
if (min !== void 0 && actual < min) parts.push(`${m} ${formatPercent(actual)} < ${formatPercent(min)}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(`- ${g.pattern}: ${parts.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
return lines;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
73
106
|
* One coverage judgment line — `Coverage: ✓ all metrics meet thresholds`
|
|
74
107
|
* for a clean run, `Coverage: ✗ <N> files below minimum thresholds (...)`
|
|
75
|
-
* for a violation
|
|
108
|
+
* for a per-file violation, `Coverage: ✗ <N> glob aggregates below
|
|
109
|
+
* threshold` when only glob aggregate shortfalls exist. Returns `null`
|
|
110
|
+
* when the run carries no coverage block.
|
|
76
111
|
*/
|
|
77
112
|
const formatCoverageJudgmentLine = (state) => {
|
|
78
113
|
const cov = state.coverage;
|
|
79
114
|
if (cov === null) return null;
|
|
80
|
-
|
|
115
|
+
const shortfallCount = cov.globShortfalls?.length ?? 0;
|
|
116
|
+
if (cov.violations.length === 0) {
|
|
117
|
+
if (shortfallCount > 0) return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(shortfallCount, "glob aggregate")} below threshold`;
|
|
118
|
+
return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
|
|
119
|
+
}
|
|
81
120
|
const metrics = cov.violations.map((v) => v.metric).join(", ");
|
|
82
121
|
const fileCount = countLowCoverageFiles(cov.gaps);
|
|
83
122
|
return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(fileCount, "file")} below minimum thresholds (${metrics})`;
|
|
84
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* The coverage judgment line followed by the glob aggregate shortfall
|
|
126
|
+
* block, as the lines a threshold cell appends to its summary. Empty when
|
|
127
|
+
* the run carries no coverage block.
|
|
128
|
+
*/
|
|
129
|
+
const formatCoverageSummaryLines = (state) => {
|
|
130
|
+
const judgment = formatCoverageJudgmentLine(state);
|
|
131
|
+
if (judgment === null) return [];
|
|
132
|
+
return [judgment, ...formatGlobShortfallLines(state.coverage?.globShortfalls)];
|
|
133
|
+
};
|
|
85
134
|
const countLowCoverageFiles = (gaps) => {
|
|
86
135
|
const seen = /* @__PURE__ */ new Set();
|
|
87
136
|
for (const g of gaps) seen.add(g.file);
|
|
@@ -191,4 +240,4 @@ const pctCell = (n) => {
|
|
|
191
240
|
};
|
|
192
241
|
|
|
193
242
|
//#endregion
|
|
194
|
-
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|
|
243
|
+
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|