@vitest-agent/ui 2.5.3 → 4.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 +1 -2
- package/counts.js +61 -0
- package/dispatcher/cells/single-file-pass.js +2 -1
- package/dispatcher/cells/single-file-threshold.js +2 -1
- package/dispatcher/cells/single-project-pass.js +2 -1
- package/dispatcher/cells/single-test-fail.js +2 -1
- package/dispatcher/cells/single-test-pass.js +2 -1
- package/dispatcher/helpers.js +38 -49
- package/dispatcher/ink-helpers.js +32 -56
- package/format-duration.js +10 -10
- package/index.d.ts +98 -343
- package/index.js +4 -7
- package/package.json +7 -3
- package/render-agent.js +25 -37
- package/render-ink/CountColumns.js +20 -19
- package/render-ink/CoverageBlock.js +13 -7
- package/render-ink/FailureSection.js +12 -5
- package/render-ink/FailuresSection.js +16 -3
- package/render-ink/ProjectRow.js +2 -1
- package/render-ink/StatusIcon.js +24 -25
- package/render-ink/StreamApp.js +76 -55
- package/render-ink/SuggestedActions.js +7 -5
- package/render-ink/TagColumns.js +2 -1
- package/render-ink/TrendLine.js +9 -9
- package/render-ink/index.js +2 -2
- package/render-ink/spinner.js +14 -43
- package/theme.js +82 -0
- package/pubsub/Channel.js +0 -30
- package/pubsub/Publisher.js +0 -37
- package/pubsub/Subscriber.js +0 -77
- package/pubsub/index.js +0 -5
package/README.md
CHANGED
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
> **Part of the [vitest-agent](https://vitest-agent.dev) ecosystem.** Most users want **[@vitest-agent/plugin](https://www.npmjs.com/package/@vitest-agent/plugin)**, which pulls this package in automatically. Install `@vitest-agent/ui` directly only if you build custom rendering on the primitives.
|
|
8
8
|
|
|
9
|
-
Pure rendering primitives for vitest-agent. Owns the `RunEvent` reducer, the shape-tailored 12-cell dispatcher matrix, two render paths (agent string and React Ink tree),
|
|
9
|
+
Pure rendering primitives for vitest-agent. Owns the `RunEvent` reducer, the shape-tailored 12-cell dispatcher matrix, two render paths (agent string and React Ink tree), and the synthesizers that bridge live Vitest data and stored reports into the event taxonomy. React and Ink are required peer dependencies.
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Reducer** — pure `(state, event) => state` fold over the `RunEvent` discriminated union; `reduceRenderStateAll` for one-shot replay
|
|
14
14
|
- **Dispatcher matrix** — `dispatch` and `dispatchInk` route a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
|
|
15
15
|
- **Ink components** — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames
|
|
16
|
-
- **PubSub channel** — `RunEventChannel` Effect tag and helpers (`accumulateUntilFinished`, `forEachRenderState`, `renderStateStream`) for live event transport
|
|
17
16
|
- **Synthesizers** — `synthesizeRunEvents` from live Vitest modules; `synthesizeFromAgentReport` from a stored `AgentReport`
|
|
18
17
|
- **Footer builder** — `buildFooter` assembles the L1 MCP-tool-pointer footer; `dominantClassification` picks the most actionable failure class
|
|
19
18
|
|
package/counts.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { VitestAgentStatus } from "./theme.js";
|
|
2
|
+
import { Doc, Render } from "@effected/cli";
|
|
3
|
+
|
|
4
|
+
//#region src/counts.ts
|
|
5
|
+
/** The test counters a totals line folds into its `n/total`. */
|
|
6
|
+
const TEST_KEYS = /* @__PURE__ */ new Set([
|
|
7
|
+
"pass",
|
|
8
|
+
"fail",
|
|
9
|
+
"timeout",
|
|
10
|
+
"skip"
|
|
11
|
+
]);
|
|
12
|
+
/** An agent is always escape-free; colour none, no links, no width. */
|
|
13
|
+
const AGENT_CONTEXT = Render.contextOf({ audience: "agent" });
|
|
14
|
+
const sumTests = (counters) => counters.reduce((sum, c) => TEST_KEYS.has(c.key) ? sum + c.n : sum, 0);
|
|
15
|
+
/**
|
|
16
|
+
* Render one totals line as plain agent text.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const formatTotalsLine = (input) => {
|
|
21
|
+
const unhandled = input.unhandledErrors ?? 0;
|
|
22
|
+
const counters = [
|
|
23
|
+
Doc.counter(VitestAgentStatus, "success", {
|
|
24
|
+
key: "pass",
|
|
25
|
+
label: "passed",
|
|
26
|
+
n: input.passCount,
|
|
27
|
+
showZero: true
|
|
28
|
+
}),
|
|
29
|
+
Doc.counter(VitestAgentStatus, "failure", {
|
|
30
|
+
key: "fail",
|
|
31
|
+
label: "failed",
|
|
32
|
+
n: input.failCount
|
|
33
|
+
}),
|
|
34
|
+
Doc.counter(VitestAgentStatus, "timeout", {
|
|
35
|
+
key: "timeout",
|
|
36
|
+
label: "timed out",
|
|
37
|
+
n: input.timeoutCount
|
|
38
|
+
}),
|
|
39
|
+
Doc.counter(VitestAgentStatus, "skip", {
|
|
40
|
+
key: "skip",
|
|
41
|
+
label: "skipped",
|
|
42
|
+
n: input.skipCount
|
|
43
|
+
}),
|
|
44
|
+
Doc.counter(VitestAgentStatus, "failure", {
|
|
45
|
+
key: "unhandled",
|
|
46
|
+
label: unhandled === 1 ? "unhandled error" : "unhandled errors",
|
|
47
|
+
n: unhandled
|
|
48
|
+
})
|
|
49
|
+
];
|
|
50
|
+
return Render.plain([Doc.counts({
|
|
51
|
+
label: input.label,
|
|
52
|
+
counters,
|
|
53
|
+
total: sumTests,
|
|
54
|
+
durationMs: input.durationMs,
|
|
55
|
+
layout: "inline",
|
|
56
|
+
...input.suffix !== void 0 ? { suffix: input.suffix } : {}
|
|
57
|
+
})], AGENT_CONTEXT);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { formatTotalsLine };
|
|
@@ -2,13 +2,14 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { soleModulePath } from "../helpers.js";
|
|
4
4
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
|
+
import { Fmt } from "@effected/cli";
|
|
5
6
|
|
|
6
7
|
//#region src/dispatcher/cells/single-file-pass.ts
|
|
7
8
|
const renderAgent = (inputs) => {
|
|
8
9
|
const modulePath = soleModulePath(inputs.state);
|
|
9
10
|
if (modulePath === void 0) return "";
|
|
10
11
|
const { passCount, durationMs } = inputs.state.totals;
|
|
11
|
-
return `${modulePath}: ${passCount
|
|
12
|
+
return `${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})\n${buildFooter(inputs)}`;
|
|
12
13
|
};
|
|
13
14
|
const renderSingleFilePass = {
|
|
14
15
|
agent: renderAgent,
|
|
@@ -2,13 +2,14 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { formatCoverageJudgmentLine, soleModulePath } from "../helpers.js";
|
|
4
4
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
|
+
import { Fmt } from "@effected/cli";
|
|
5
6
|
|
|
6
7
|
//#region src/dispatcher/cells/single-file-threshold.ts
|
|
7
8
|
const renderAgent = (inputs) => {
|
|
8
9
|
const modulePath = soleModulePath(inputs.state);
|
|
9
10
|
if (modulePath === void 0) return "";
|
|
10
11
|
const { passCount, durationMs } = inputs.state.totals;
|
|
11
|
-
const lines = [`${modulePath}: ${passCount
|
|
12
|
+
const lines = [`${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})`];
|
|
12
13
|
const coverage = formatCoverageJudgmentLine(inputs.state);
|
|
13
14
|
if (coverage !== null) lines.push(coverage);
|
|
14
15
|
return `${lines.join("\n")}\n${buildFooter(inputs)}`;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatTotals } from "../helpers.js";
|
|
3
3
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
|
+
import { Fmt } from "@effected/cli";
|
|
4
5
|
|
|
5
6
|
//#region src/dispatcher/cells/single-project-pass.ts
|
|
6
7
|
const renderAgent = (inputs) => {
|
|
7
8
|
const { passCount, failCount, skipCount, timeoutCount } = inputs.state.totals;
|
|
8
9
|
if (passCount + failCount + skipCount + timeoutCount === 0) return `${[formatTotals(inputs.state), "0 tests collected. A zero-test run usually means a wrong working directory, a filter that matched nothing, or a load-time error — verify before trusting it."].join("\n\n")}\n${buildFooter(inputs)}`;
|
|
9
10
|
const collected = inputs.state.collectedModules ?? inputs.state.moduleOrder.length;
|
|
10
|
-
return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${collected
|
|
11
|
+
return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${Fmt.plural(collected, "module")} all-passed.`]).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
11
12
|
};
|
|
12
13
|
const renderSingleProjectPass = {
|
|
13
14
|
agent: renderAgent,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
+
import { statusGlyph } from "../../theme.js";
|
|
2
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
4
|
import { formatFailure, formatTestName, soleTest } from "../helpers.js";
|
|
4
5
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
@@ -9,7 +10,7 @@ const renderAgent = (inputs) => {
|
|
|
9
10
|
const test = soleTest(inputs.state);
|
|
10
11
|
if (test === void 0) return "";
|
|
11
12
|
const duration = test.durationMs ?? 0;
|
|
12
|
-
const header =
|
|
13
|
+
const header = `${statusGlyph("failure")} ${formatTestName(test)} (${formatDisplayDuration(duration)})`;
|
|
13
14
|
const failure = inputs.state.failures[0];
|
|
14
15
|
return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
|
|
15
16
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
+
import { statusGlyph } from "../../theme.js";
|
|
2
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
4
|
import { formatTestName, soleTest } from "../helpers.js";
|
|
4
5
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
@@ -8,7 +9,7 @@ const renderAgent = (inputs) => {
|
|
|
8
9
|
const test = soleTest(inputs.state);
|
|
9
10
|
if (test === void 0) return "";
|
|
10
11
|
const duration = test.durationMs ?? 0;
|
|
11
|
-
return
|
|
12
|
+
return `${statusGlyph("success")} ${formatTestName(test)} (${formatDisplayDuration(duration)})\n${buildFooter(inputs)}`;
|
|
12
13
|
};
|
|
13
14
|
const renderSingleTestPass = {
|
|
14
15
|
agent: renderAgent,
|
package/dispatcher/helpers.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
+
import { statusGlyph } from "../theme.js";
|
|
2
|
+
import { formatTotalsLine } from "../counts.js";
|
|
1
3
|
import { formatDisplayDuration } from "../format-duration.js";
|
|
4
|
+
import { Doc, Fmt, Render } from "@effected/cli";
|
|
2
5
|
|
|
3
6
|
//#region src/dispatcher/helpers.ts
|
|
4
7
|
/**
|
|
5
|
-
* Truncate a line to a maximum width with an ellipsis suffix.
|
|
6
|
-
*/
|
|
7
|
-
const truncate = (line, max) => {
|
|
8
|
-
if (line.length <= max) return line;
|
|
9
|
-
const slice = max - 1;
|
|
10
|
-
if (slice <= 0) return "…";
|
|
11
|
-
return `${line.slice(0, slice)}…`;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
8
|
* Format the `Tests:` header line —
|
|
15
9
|
* `<pass>/<total> passed[, <fail> failed][, <timeout> timed out][, <skip> skipped] (Xms)`.
|
|
16
10
|
* `total` folds in `timeoutCount` — a timed-out test is a real collected
|
|
17
11
|
* test, not a pass (issue #224).
|
|
18
12
|
*/
|
|
19
13
|
const formatTotals = (state) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return `Tests: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})${suffix}`;
|
|
14
|
+
const modules = state.collectedModules;
|
|
15
|
+
return formatTotalsLine({
|
|
16
|
+
label: "Tests",
|
|
17
|
+
...state.totals,
|
|
18
|
+
...modules !== void 0 && modules > 0 ? { suffix: `across ${modules} files` } : {}
|
|
19
|
+
});
|
|
27
20
|
};
|
|
28
21
|
/**
|
|
29
22
|
* Locate the sole {@link TestRecord} in a `single-test` shape state.
|
|
@@ -56,17 +49,25 @@ const formatTestName = (test) => {
|
|
|
56
49
|
* Render one failure block — `- <path > suite > name> [classification]`
|
|
57
50
|
* followed by the indented message and diff. Stack traces are omitted
|
|
58
51
|
* by default to keep the agent string compact.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* A kit document rendered plain for the agent audience: one compact list
|
|
55
|
+
* item whose title is verbatim (never wrapped), whose first message line is
|
|
56
|
+
* a truncated `Doc.line`, and whose diff is `Doc.diffText` with `truncate`.
|
|
57
|
+
* Each content line is cut to `width - 2` (floor 20) after the item indent.
|
|
58
|
+
* The kit sanitizes the text, so a tab becomes a space and an escape
|
|
59
|
+
* sequence is removed.
|
|
59
60
|
*/
|
|
60
61
|
const formatFailure = (f, width) => {
|
|
61
62
|
const suite = f.suitePath.length > 0 ? `${f.suitePath.join(" > ")} > ` : "";
|
|
62
63
|
const classification = f.classification !== null ? ` [${f.classification}]` : "";
|
|
63
|
-
const
|
|
64
|
-
if (f.error?.message !== void 0) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const parts = [Doc.verbatim(`${f.modulePath} > ${suite}${f.testName}${classification}`)];
|
|
65
|
+
if (f.error?.message !== void 0) parts.push(Doc.line(f.error.message.split("\n", 1)[0] ?? "", { truncate: true }));
|
|
66
|
+
if (f.error?.diff !== void 0) parts.push(Doc.diffText(f.error.diff, { truncate: true }));
|
|
67
|
+
return Render.plain([Doc.list([Doc.section(void 0, parts)], { compact: true })], Render.contextOf({
|
|
68
|
+
audience: "agent",
|
|
69
|
+
width: Math.max(22, width)
|
|
70
|
+
})).split("\n");
|
|
70
71
|
};
|
|
71
72
|
/**
|
|
72
73
|
* One coverage judgment line — `Coverage: ✓ all metrics meet thresholds`
|
|
@@ -76,10 +77,10 @@ const formatFailure = (f, width) => {
|
|
|
76
77
|
const formatCoverageJudgmentLine = (state) => {
|
|
77
78
|
const cov = state.coverage;
|
|
78
79
|
if (cov === null) return null;
|
|
79
|
-
if (cov.violations.length === 0) return
|
|
80
|
+
if (cov.violations.length === 0) return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
|
|
80
81
|
const metrics = cov.violations.map((v) => v.metric).join(", ");
|
|
81
82
|
const fileCount = countLowCoverageFiles(cov.gaps);
|
|
82
|
-
return `Coverage:
|
|
83
|
+
return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(fileCount, "file")} below minimum thresholds (${metrics})`;
|
|
83
84
|
};
|
|
84
85
|
const countLowCoverageFiles = (gaps) => {
|
|
85
86
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -92,8 +93,7 @@ const countLowCoverageFiles = (gaps) => {
|
|
|
92
93
|
*/
|
|
93
94
|
const formatTrendLine = (trend) => {
|
|
94
95
|
if (trend === null) return null;
|
|
95
|
-
|
|
96
|
-
return `Trend: ${trend.direction} (${runs})`;
|
|
96
|
+
return `Trend: ${trend.direction} (${Fmt.plural(trend.runCount, "run")})`;
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
99
|
* Format a compact projects-table line for one project. Pads the name
|
|
@@ -104,7 +104,7 @@ const formatTrendLine = (trend) => {
|
|
|
104
104
|
const formatProjectRow = (project, nameWidth) => {
|
|
105
105
|
const timeoutCount = project.timeoutCount ?? 0;
|
|
106
106
|
const total = project.passCount + project.failCount + project.skipCount + timeoutCount;
|
|
107
|
-
const glyph = project.failCount > 0 || timeoutCount > 0 ? "
|
|
107
|
+
const glyph = statusGlyph(project.failCount > 0 || timeoutCount > 0 ? "failure" : "success");
|
|
108
108
|
const countParts = [`${project.passCount}/${total} passed`];
|
|
109
109
|
if (project.failCount > 0) countParts.push(`${project.failCount} failed`);
|
|
110
110
|
if (timeoutCount > 0) countParts.push(`${timeoutCount} timed out`);
|
|
@@ -132,26 +132,15 @@ const formatProjectsTable = (projects) => {
|
|
|
132
132
|
/**
|
|
133
133
|
* Format the `Total:` footer for a workspace run.
|
|
134
134
|
*/
|
|
135
|
-
const formatWorkspaceTotal = (projects) => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
skip += p.skipCount;
|
|
145
|
-
timeout += p.timeoutCount ?? 0;
|
|
146
|
-
durationMs += p.durationMs;
|
|
147
|
-
}
|
|
148
|
-
const total = pass + fail + skip + timeout;
|
|
149
|
-
const parts = [`${pass}/${total} passed`];
|
|
150
|
-
if (fail > 0) parts.push(`${fail} failed`);
|
|
151
|
-
if (timeout > 0) parts.push(`${timeout} timed out`);
|
|
152
|
-
if (skip > 0) parts.push(`${skip} skipped`);
|
|
153
|
-
return `Total: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
|
|
154
|
-
};
|
|
135
|
+
const formatWorkspaceTotal = (projects) => formatTotalsLine({
|
|
136
|
+
label: "Total",
|
|
137
|
+
passCount: sumOf(projects, (p) => p.passCount),
|
|
138
|
+
failCount: sumOf(projects, (p) => p.failCount),
|
|
139
|
+
timeoutCount: sumOf(projects, (p) => p.timeoutCount ?? 0),
|
|
140
|
+
skipCount: sumOf(projects, (p) => p.skipCount),
|
|
141
|
+
durationMs: sumOf(projects, (p) => p.durationMs)
|
|
142
|
+
});
|
|
143
|
+
const sumOf = (projects, pick) => projects.reduce((sum, p) => sum + pick(p), 0);
|
|
155
144
|
const TABLE_COL_FILE_MIN = 60;
|
|
156
145
|
/**
|
|
157
146
|
* Format the `Files below aspirational target:` block — a pipe-delimited
|
|
@@ -202,4 +191,4 @@ const pctCell = (n) => {
|
|
|
202
191
|
};
|
|
203
192
|
|
|
204
193
|
//#endregion
|
|
205
|
-
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest
|
|
194
|
+
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
+
import { inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
|
|
4
5
|
//#region src/dispatcher/ink-helpers.tsx
|
|
5
6
|
/**
|
|
6
|
-
* Ink rendering helpers shared by the dispatcher cells.
|
|
7
|
-
*
|
|
8
|
-
* Most cells re-express their agent-half string in a simple
|
|
9
|
-
* `<Box flexDirection="column">` with one `<Text>` per line plus
|
|
10
|
-
* targeted color coding on glyph characters (✓ green, ✗ red,
|
|
11
|
-
* Trend regressing/improving in matching colors).
|
|
12
|
-
*
|
|
13
|
-
* @packageDocumentation
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
7
|
* Render an agent-string output as a column of Ink Text rows, applying
|
|
17
8
|
* color to the leading status glyph on each line.
|
|
18
9
|
*/
|
|
@@ -23,54 +14,39 @@ const renderAgentStringAsInk = (agentString) => {
|
|
|
23
14
|
children: lines.map((line, idx) => /* @__PURE__ */ jsx(Text, { children: colorize(line) }, `${idx}-${line}`))
|
|
24
15
|
});
|
|
25
16
|
};
|
|
26
|
-
const
|
|
27
|
-
const
|
|
17
|
+
const SUCCESS_GLYPH = statusGlyph("success");
|
|
18
|
+
const FAILURE_GLYPH = statusGlyph("failure");
|
|
19
|
+
/** Paint the leading status glyph of a line in its vocabulary style. */
|
|
20
|
+
const glyphLine = (line, glyph, status) => {
|
|
21
|
+
const idx = line.indexOf(glyph);
|
|
22
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
23
|
+
line.slice(0, idx),
|
|
24
|
+
/* @__PURE__ */ jsx(Text, {
|
|
25
|
+
...statusInkStyle(status),
|
|
26
|
+
children: glyph
|
|
27
|
+
}),
|
|
28
|
+
line.slice(idx + glyph.length)
|
|
29
|
+
] });
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Whole-line styles keyed on the agent string's line prefixes. A
|
|
33
|
+
* regressing trend is a `warning`; a coverage threshold failure is a
|
|
34
|
+
* `failure` (the kit's drift decisions).
|
|
35
|
+
*/
|
|
36
|
+
const LINE_STYLES = [
|
|
37
|
+
["Trend: regressing", "warning"],
|
|
38
|
+
["Trend: improving", "success"],
|
|
39
|
+
[`Coverage: ${SUCCESS_GLYPH}`, "success"],
|
|
40
|
+
[`Coverage: ${FAILURE_GLYPH}`, "failure"],
|
|
41
|
+
["Failures:", "emphasis"],
|
|
42
|
+
["Use `", "muted"]
|
|
43
|
+
];
|
|
28
44
|
const colorize = (line) => {
|
|
29
45
|
const trimmed = line.trimStart();
|
|
30
|
-
if (trimmed.startsWith(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/* @__PURE__ */ jsx(Text, {
|
|
35
|
-
color: "green",
|
|
36
|
-
children: PASS_GLYPH
|
|
37
|
-
}),
|
|
38
|
-
line.slice(idx + 1)
|
|
39
|
-
] });
|
|
40
|
-
}
|
|
41
|
-
if (trimmed.startsWith(FAIL_GLYPH)) {
|
|
42
|
-
const idx = line.indexOf(FAIL_GLYPH);
|
|
43
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
44
|
-
line.slice(0, idx),
|
|
45
|
-
/* @__PURE__ */ jsx(Text, {
|
|
46
|
-
color: "red",
|
|
47
|
-
children: FAIL_GLYPH
|
|
48
|
-
}),
|
|
49
|
-
line.slice(idx + 1)
|
|
50
|
-
] });
|
|
51
|
-
}
|
|
52
|
-
if (line.startsWith("Trend: regressing")) return /* @__PURE__ */ jsx(Text, {
|
|
53
|
-
color: "yellow",
|
|
54
|
-
children: line
|
|
55
|
-
});
|
|
56
|
-
if (line.startsWith("Trend: improving")) return /* @__PURE__ */ jsx(Text, {
|
|
57
|
-
color: "green",
|
|
58
|
-
children: line
|
|
59
|
-
});
|
|
60
|
-
if (line.startsWith("Coverage: ✓")) return /* @__PURE__ */ jsx(Text, {
|
|
61
|
-
color: "green",
|
|
62
|
-
children: line
|
|
63
|
-
});
|
|
64
|
-
if (line.startsWith("Coverage: ✗")) return /* @__PURE__ */ jsx(Text, {
|
|
65
|
-
color: "red",
|
|
66
|
-
children: line
|
|
67
|
-
});
|
|
68
|
-
if (line.startsWith("Failures:")) return /* @__PURE__ */ jsx(Text, {
|
|
69
|
-
bold: true,
|
|
70
|
-
children: line
|
|
71
|
-
});
|
|
72
|
-
if (line.startsWith("Use `")) return /* @__PURE__ */ jsx(Text, {
|
|
73
|
-
dimColor: true,
|
|
46
|
+
if (trimmed.startsWith(SUCCESS_GLYPH)) return glyphLine(line, SUCCESS_GLYPH, "success");
|
|
47
|
+
if (trimmed.startsWith(FAILURE_GLYPH)) return glyphLine(line, FAILURE_GLYPH, "failure");
|
|
48
|
+
for (const [prefix, token] of LINE_STYLES) if (line.startsWith(prefix)) return /* @__PURE__ */ jsx(Text, {
|
|
49
|
+
...inkStyle(token),
|
|
74
50
|
children: line
|
|
75
51
|
});
|
|
76
52
|
return line;
|
package/format-duration.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Fmt } from "@effected/cli";
|
|
2
|
+
|
|
1
3
|
//#region src/format-duration.ts
|
|
2
4
|
/**
|
|
3
5
|
* Shared display formatter for run / module / test durations.
|
|
@@ -6,29 +8,27 @@
|
|
|
6
8
|
* (`14.87745800000016`). Rendering those verbatim is noise. This is
|
|
7
9
|
* the single formatter every render path calls — `render-agent.ts`,
|
|
8
10
|
* the `render-ink/` components, the dispatcher cells, and `StreamApp`
|
|
9
|
-
* — so a duration looks the same wherever it appears.
|
|
11
|
+
* — so a duration looks the same wherever it appears. It delegates to
|
|
12
|
+
* `@effected/cli`'s `Fmt.duration`, so it also matches every other kit
|
|
13
|
+
* CLI's output.
|
|
10
14
|
*
|
|
11
15
|
* Display only. Full-precision durations continue to persist to the
|
|
12
16
|
* database unchanged; nothing in the trend / baseline / classification
|
|
13
17
|
* pipeline reads a duration through a formatter.
|
|
14
18
|
*/
|
|
15
|
-
const SECOND_MS = 1e3;
|
|
16
19
|
/**
|
|
17
20
|
* Format a duration in milliseconds for display.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Whole milliseconds under a second (`250ms`), seconds to one decimal
|
|
23
|
+
* under a minute with a trailing `.0` dropped (`1.2s`, `2s`), then
|
|
24
|
+
* `1m 3s` and `1h 2m`. A value that rounds up to the next unit is
|
|
25
|
+
* written in that unit (`999.6` → `1s`).
|
|
23
26
|
*
|
|
24
27
|
* @param ms - duration in milliseconds
|
|
25
28
|
* @returns formatted duration string
|
|
26
29
|
* @public
|
|
27
30
|
*/
|
|
28
|
-
const formatDisplayDuration = (ms) =>
|
|
29
|
-
if (ms < SECOND_MS) return `${Math.round(ms * 10) / 10}ms`;
|
|
30
|
-
return `${Math.round(ms / SECOND_MS * 10) / 10}s`;
|
|
31
|
-
};
|
|
31
|
+
const formatDisplayDuration = (ms) => Fmt.duration(ms);
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
export { formatDisplayDuration };
|