@vitest-agent/ui 4.0.1 → 4.1.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.
@@ -1,6 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
- import { formatCoverageJudgmentLine, soleModulePath } from "../helpers.js";
3
+ import { formatCoverageSummaryLines, soleModulePath } from "../helpers.js";
4
4
  import { renderAgentStringAsInk } from "../ink-helpers.js";
5
5
  import { Fmt } from "@effected/cli";
6
6
 
@@ -10,8 +10,7 @@ const renderAgent = (inputs) => {
10
10
  if (modulePath === void 0) return "";
11
11
  const { passCount, durationMs } = inputs.state.totals;
12
12
  const lines = [`${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})`];
13
- const coverage = formatCoverageJudgmentLine(inputs.state);
14
- if (coverage !== null) lines.push(coverage);
13
+ lines.push(...formatCoverageSummaryLines(inputs.state));
15
14
  return `${lines.join("\n")}\n${buildFooter(inputs)}`;
16
15
  };
17
16
  const renderSingleFileThreshold = {
@@ -1,15 +1,15 @@
1
1
  import { buildFooter } from "../footer.js";
2
- import { formatBelowTargetTable, formatCoverageJudgmentLine, formatTotals, formatTrendLine } from "../helpers.js";
2
+ import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, formatTrendLine } from "../helpers.js";
3
3
  import { renderAgentStringAsInk } from "../ink-helpers.js";
4
4
 
5
5
  //#region src/dispatcher/cells/single-project-threshold.ts
6
6
  const BELOW_TARGET_LIMIT = 5;
7
7
  const renderAgent = (inputs) => {
8
8
  const sections = [[formatTotals(inputs.state)]];
9
- const coverage = formatCoverageJudgmentLine(inputs.state);
9
+ const coverage = formatCoverageSummaryLines(inputs.state);
10
10
  const trend = formatTrendLine(inputs.trend);
11
11
  const summaryLines = [];
12
- if (coverage !== null) summaryLines.push(coverage);
12
+ summaryLines.push(...coverage);
13
13
  if (trend !== null) summaryLines.push(trend);
14
14
  if (summaryLines.length > 0) sections.push(summaryLines);
15
15
  const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
@@ -1,5 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
- import { formatBelowTargetTable, formatCoverageJudgmentLine, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
2
+ import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
3
  import { renderAgentStringAsInk } from "../ink-helpers.js";
4
4
 
5
5
  //#region src/dispatcher/cells/workspace-threshold.ts
@@ -9,8 +9,8 @@ const renderAgent = (inputs) => {
9
9
  const projects = formatProjectsTable(inputs.projects);
10
10
  if (projects.length > 0) sections.push([...projects]);
11
11
  const summaryLines = [];
12
- const coverage = formatCoverageJudgmentLine(inputs.state);
13
- if (coverage !== null) summaryLines.push(coverage);
12
+ const coverage = formatCoverageSummaryLines(inputs.state);
13
+ summaryLines.push(...coverage);
14
14
  const trend = formatTrendLine(inputs.trend);
15
15
  if (trend !== null) summaryLines.push(trend);
16
16
  if (summaryLines.length > 0) sections.push(summaryLines);
@@ -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 && state.coverage.violations.length > 0) return "threshold-violation";
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
 
@@ -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. Returns `null` when the run carries no coverage block.
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
- if (cov.violations.length === 0) return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
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 };
package/index.d.ts CHANGED
@@ -31,6 +31,35 @@ declare const FileCoverageReport: Schema.Struct<{
31
31
  }>;
32
32
  /** @public */
33
33
  type FileCoverageReport = typeof FileCoverageReport.Type;
34
+ /**
35
+ * A threshold glob whose AGGREGATE coverage (summed covered/total across
36
+ * every file the glob matches, the same way Vitest evaluates a glob
37
+ * threshold) is below at least one of the glob's metric numbers.
38
+ *
39
+ * This is independent of the per-file lists: with an object `perFile` every
40
+ * matched file can pass while the glob as a whole still fails.
41
+ * @public
42
+ */
43
+ declare const GlobShortfall: Schema.Struct<{
44
+ /** The threshold glob pattern as configured. */
45
+ readonly pattern: Schema.String;
46
+ /** Aggregate coverage percentages over the files the glob matches. */
47
+ readonly summary: Schema.Struct<{
48
+ readonly statements: Schema.Number;
49
+ readonly branches: Schema.Number;
50
+ readonly functions: Schema.Number;
51
+ readonly lines: Schema.Number;
52
+ }>;
53
+ /** The glob's own metric numbers that were set (`perFile` excluded). */
54
+ readonly thresholds: Schema.Struct<{
55
+ readonly lines: Schema.optional<Schema.Number>;
56
+ readonly functions: Schema.optional<Schema.Number>;
57
+ readonly branches: Schema.optional<Schema.Number>;
58
+ readonly statements: Schema.optional<Schema.Number>;
59
+ }>;
60
+ }>;
61
+ /** @public */
62
+ type GlobShortfall = typeof GlobShortfall.Type;
34
63
  //#endregion
35
64
  //#region src/schemas/RenderState.d.ts
36
65
  /**
@@ -153,6 +182,21 @@ export declare const CoverageRenderState: Schema.Struct<{
153
182
  readonly scoped: Schema.optional<Schema.Boolean>;
154
183
  readonly scopedFiles: Schema.optional<Schema.Number>;
155
184
  readonly totalFiles: Schema.optional<Schema.Number>;
185
+ readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
186
+ readonly pattern: Schema.String;
187
+ readonly summary: Schema.Struct<{
188
+ readonly statements: Schema.Number;
189
+ readonly branches: Schema.Number;
190
+ readonly functions: Schema.Number;
191
+ readonly lines: Schema.Number;
192
+ }>;
193
+ readonly thresholds: Schema.Struct<{
194
+ readonly lines: Schema.optional<Schema.Number>;
195
+ readonly functions: Schema.optional<Schema.Number>;
196
+ readonly branches: Schema.optional<Schema.Number>;
197
+ readonly statements: Schema.optional<Schema.Number>;
198
+ }>;
199
+ }>>>;
156
200
  }>;
157
201
  /** @public */
158
202
  export type CoverageRenderState = typeof CoverageRenderState.Type;
@@ -274,6 +318,21 @@ export declare const RenderState: Schema.Struct<{
274
318
  readonly scoped: Schema.optional<Schema.Boolean>;
275
319
  readonly scopedFiles: Schema.optional<Schema.Number>;
276
320
  readonly totalFiles: Schema.optional<Schema.Number>;
321
+ readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
322
+ readonly pattern: Schema.String;
323
+ readonly summary: Schema.Struct<{
324
+ readonly statements: Schema.Number;
325
+ readonly branches: Schema.Number;
326
+ readonly functions: Schema.Number;
327
+ readonly lines: Schema.Number;
328
+ }>;
329
+ readonly thresholds: Schema.Struct<{
330
+ readonly lines: Schema.optional<Schema.Number>;
331
+ readonly functions: Schema.optional<Schema.Number>;
332
+ readonly branches: Schema.optional<Schema.Number>;
333
+ readonly statements: Schema.optional<Schema.Number>;
334
+ }>;
335
+ }>>>;
277
336
  }>>;
278
337
  readonly trend: Schema.NullOr<Schema.Struct<{
279
338
  readonly direction: Schema.Literals<readonly ["improving", "regressing", "stable"]>;
@@ -606,6 +665,21 @@ declare const AgentReport: Schema.Struct<{
606
665
  readonly uncoveredLines: Schema.String;
607
666
  }>>>;
608
667
  readonly belowTargetFiles: Schema.optional<Schema.$Array<Schema.String>>;
668
+ readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
669
+ readonly pattern: Schema.String;
670
+ readonly summary: Schema.Struct<{
671
+ readonly statements: Schema.Number;
672
+ readonly branches: Schema.Number;
673
+ readonly functions: Schema.Number;
674
+ readonly lines: Schema.Number;
675
+ }>;
676
+ readonly thresholds: Schema.Struct<{
677
+ readonly lines: Schema.optional<Schema.Number>;
678
+ readonly functions: Schema.optional<Schema.Number>;
679
+ readonly branches: Schema.optional<Schema.Number>;
680
+ readonly statements: Schema.optional<Schema.Number>;
681
+ }>;
682
+ }>>>;
609
683
  }>>;
610
684
  readonly tagCounts: Schema.optional<Schema.$Record<Schema.String, Schema.Struct<{
611
685
  readonly passed: Schema.optional<Schema.Number>;
@@ -825,6 +899,21 @@ export declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunSt
825
899
  readonly scoped: Schema.optional<Schema.Boolean>;
826
900
  readonly scopedFiles: Schema.optional<Schema.Number>;
827
901
  readonly totalFiles: Schema.optional<Schema.Number>;
902
+ readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
903
+ readonly pattern: Schema.String;
904
+ readonly summary: Schema.Struct<{
905
+ readonly statements: Schema.Number;
906
+ readonly branches: Schema.Number;
907
+ readonly functions: Schema.Number;
908
+ readonly lines: Schema.Number;
909
+ }>;
910
+ readonly thresholds: Schema.Struct<{
911
+ readonly lines: Schema.optional<Schema.Number>;
912
+ readonly functions: Schema.optional<Schema.Number>;
913
+ readonly branches: Schema.optional<Schema.Number>;
914
+ readonly statements: Schema.optional<Schema.Number>;
915
+ }>;
916
+ }>>>;
828
917
  }>, Schema.TaggedStruct<"ThresholdViolation", {
829
918
  readonly metric: Schema.Literals<readonly ["lines", "branches", "functions", "statements"]>;
830
919
  readonly expected: Schema.Number;
@@ -1074,6 +1163,8 @@ interface Cell {
1074
1163
  * UI rewrite spec §7 open question 1:
1075
1164
  *
1076
1165
  * 1. More than one project → `workspace`.
1166
+ * 1b. `collectedModules` greater than one → `single-project` (the reduced
1167
+ * `modules` map may hold only the failing modules of a larger run).
1077
1168
  * 2. One module with exactly one test → `single-test`.
1078
1169
  * 3. One module with more than one test → `single-file`.
1079
1170
  * 4. Otherwise → `single-project`.
@@ -1593,6 +1684,16 @@ export declare const TrendLine: FC<TrendLineProps>;
1593
1684
  * @public
1594
1685
  */
1595
1686
  export declare const SUITE_LOAD_FAILURE_LABEL = "test suite failed to load";
1687
+ /**
1688
+ * Synthetic test name for a suite-level failure in a module that loaded
1689
+ * and collected one or more test cases — a failing `beforeAll` or
1690
+ * `afterAll` hook, for example. The file imported fine, so
1691
+ * {@link SUITE_LOAD_FAILURE_LABEL} would mislead; this label is used
1692
+ * instead when the module has collected tests.
1693
+ *
1694
+ * @public
1695
+ */
1696
+ export declare const SUITE_FAILURE_LABEL = "test suite failed";
1596
1697
  /**
1597
1698
  * Optional metadata threaded through the synthesized event stream.
1598
1699
  *
@@ -1636,6 +1737,8 @@ interface SynthesizedCoverage {
1636
1737
  readonly scopedFiles?: number;
1637
1738
  /** Total test-file count for the project, when known. */
1638
1739
  readonly totalFiles?: number;
1740
+ /** Threshold globs whose aggregate coverage is below their numbers (issue #391). */
1741
+ readonly globShortfalls?: ReadonlyArray<GlobShortfall>;
1639
1742
  }
1640
1743
  /**
1641
1744
  * Project a fully-completed Vitest run into the canonical
@@ -1768,5 +1871,5 @@ export declare const statusInkStyle: (name: VitestAgentStatusName) => InkTextSty
1768
1871
  */
1769
1872
  export declare const CURRENT_UI_VERSION: string;
1770
1873
  //#endregion
1771
- export { type AgentCellFn, type AgentReport, type Cell, type CellOptions, type CountColumnsProps, type CoverageBlockProps, FileCoverageReport as CoverageFile, type CoverageTotals, type DispatchInputs, type FailureSectionProps, type FailuresSectionProps, type InkCellFn, type InkTextStyle, type MetricThresholds, type ModuleHeaderProps, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, type RunEventByTag, type RunOutcome, type RunShape, type StatusIconKind, type StatusIconProps, type StreamAppProps, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, type TagColumnsProps, type TestClassification, type TestRowProps, type TrendLineProps, type TrendSummary, type VitestAgentStatusName, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite };
1874
+ export { type AgentCellFn, type AgentReport, type Cell, type CellOptions, type CountColumnsProps, type CoverageBlockProps, FileCoverageReport as CoverageFile, type CoverageTotals, type DispatchInputs, type FailureSectionProps, type FailuresSectionProps, type GlobShortfall, type InkCellFn, type InkTextStyle, type MetricThresholds, type ModuleHeaderProps, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, type RunEventByTag, type RunOutcome, type RunShape, type StatusIconKind, type StatusIconProps, type StreamAppProps, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, type TagColumnsProps, type TestClassification, type TestRowProps, type TrendLineProps, type TrendSummary, type VitestAgentStatusName, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite };
1772
1875
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -19,7 +19,7 @@ import { TrendLine } from "./render-ink/TrendLine.js";
19
19
  import { StreamApp } from "./render-ink/StreamApp.js";
20
20
  import { SuggestedActions } from "./render-ink/SuggestedActions.js";
21
21
  import "./render-ink/index.js";
22
- import { SUITE_LOAD_FAILURE_LABEL, synthesizeFromAgentReport, synthesizeRunEvents } from "./synthesize.js";
22
+ import { SUITE_FAILURE_LABEL, SUITE_LOAD_FAILURE_LABEL, synthesizeFromAgentReport, synthesizeRunEvents } from "./synthesize.js";
23
23
  import { ActionSeverity, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, FailureRecord, ModuleRecord, RenderState, RenderTotals, RunEvent, SuggestedActionRecord, TestRecord, initialRenderState } from "@vitest-agent/sdk";
24
24
 
25
25
  //#region src/index.ts
@@ -30,7 +30,7 @@ import { ActionSeverity, CoverageFile, CoverageGap, CoverageMetric, CoverageRend
30
30
  *
31
31
  * @public
32
32
  */
33
- const CURRENT_UI_VERSION = "4.0.1";
33
+ const CURRENT_UI_VERSION = "4.1.0";
34
34
 
35
35
  //#endregion
36
- export { ActionSeverity, CURRENT_UI_VERSION, CountColumns, CoverageBlock, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, DURATION_CELL_WIDTH, FailureRecord, FailureSection, FailuresSection, ModuleHeader, ModuleRecord, ProjectRow, RenderState, RenderTotals, RunEvent, SPINNER_FRAMES, SPINNER_FRAME_MS, SUITE_LOAD_FAILURE_LABEL, StatusIcon, StreamApp, SuggestedActionRecord, SuggestedActions, TagColumns, TestRecord, TestRow, TrendLine, VitestAgentStatus, VitestAgentTokens, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, formatDisplayDuration, initialRenderState, inkStyle, reduceRenderState, reduceRenderStateAll, renderAgent, spinnerFrame, statusGlyph, statusInkStyle, synthesizeFromAgentReport, synthesizeRunEvents, tagUnion };
36
+ export { ActionSeverity, CURRENT_UI_VERSION, CountColumns, CoverageBlock, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, DURATION_CELL_WIDTH, FailureRecord, FailureSection, FailuresSection, ModuleHeader, ModuleRecord, ProjectRow, RenderState, RenderTotals, RunEvent, SPINNER_FRAMES, SPINNER_FRAME_MS, SUITE_FAILURE_LABEL, SUITE_LOAD_FAILURE_LABEL, StatusIcon, StreamApp, SuggestedActionRecord, SuggestedActions, TagColumns, TestRecord, TestRow, TrendLine, VitestAgentStatus, VitestAgentTokens, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, formatDisplayDuration, initialRenderState, inkStyle, reduceRenderState, reduceRenderStateAll, renderAgent, spinnerFrame, statusGlyph, statusInkStyle, synthesizeFromAgentReport, synthesizeRunEvents, tagUnion };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/ui",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "description": "Shared event-sourced renderer for vitest-agent. React Ink view for humans, plain-string view for agents, both fed by the same reducer over a RunEvent stream.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "dependencies": {
42
- "@vitest-agent/sdk": "6.0.0",
42
+ "@vitest-agent/sdk": "6.1.0",
43
43
  "effect": "^4.0.0"
44
44
  },
45
45
  "peerDependencies": {
package/reducer.js CHANGED
@@ -178,7 +178,8 @@ const reduceRenderState = (state, event) => {
178
178
  violations: state.coverage?.violations ?? [],
179
179
  ...e.scoped !== void 0 ? { scoped: e.scoped } : {},
180
180
  ...e.scopedFiles !== void 0 ? { scopedFiles: e.scopedFiles } : {},
181
- ...e.totalFiles !== void 0 ? { totalFiles: e.totalFiles } : {}
181
+ ...e.totalFiles !== void 0 ? { totalFiles: e.totalFiles } : {},
182
+ ...e.globShortfalls !== void 0 ? { globShortfalls: e.globShortfalls } : {}
182
183
  }
183
184
  }),
184
185
  ThresholdViolation: (e) => {
package/render-agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { formatTotalsLine } from "./counts.js";
2
- import { formatFailure as formatFailure$1 } from "./dispatcher/helpers.js";
2
+ import { formatFailure as formatFailure$1, formatGlobShortfallLines } from "./dispatcher/helpers.js";
3
3
  import { Fmt } from "@effected/cli";
4
4
 
5
5
  //#region src/render-agent.ts
@@ -91,6 +91,7 @@ const formatCoverageSection = (state, maxGaps) => {
91
91
  lines.push("Violations:");
92
92
  for (const v of cov.violations) lines.push(`- ${v.metric}: ${formatPercent(v.actual)} < ${formatPercent(v.expected)}`);
93
93
  }
94
+ lines.push(...formatGlobShortfallLines(cov.globShortfalls));
94
95
  if (maxGaps > 0 && cov.gaps.length > 0) {
95
96
  const topN = [...cov.gaps].sort((a, b) => b.missing.lines - a.missing.lines).slice(0, maxGaps);
96
97
  lines.push("Gaps:");
package/synthesize.js CHANGED
@@ -10,6 +10,16 @@ import { isTimeoutError } from "@vitest-agent/sdk";
10
10
  * @public
11
11
  */
12
12
  const SUITE_LOAD_FAILURE_LABEL = "test suite failed to load";
13
+ /**
14
+ * Synthetic test name for a suite-level failure in a module that loaded
15
+ * and collected one or more test cases — a failing `beforeAll` or
16
+ * `afterAll` hook, for example. The file imported fine, so
17
+ * {@link SUITE_LOAD_FAILURE_LABEL} would mislead; this label is used
18
+ * instead when the module has collected tests.
19
+ *
20
+ * @public
21
+ */
22
+ const SUITE_FAILURE_LABEL = "test suite failed";
13
23
  const ISO_ZERO = "1970-01-01T00:00:00.000Z";
14
24
  const normalizeTestState = (state) => {
15
25
  if (state === "passed") return "passed";
@@ -150,7 +160,8 @@ const synthesizeRunEvents = (modules, options = {}) => {
150
160
  gaps: cov.gaps,
151
161
  ...cov.scoped !== void 0 ? { scoped: cov.scoped } : {},
152
162
  ...cov.scopedFiles !== void 0 ? { scopedFiles: cov.scopedFiles } : {},
153
- ...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {}
163
+ ...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {},
164
+ ...cov.globShortfalls !== void 0 ? { globShortfalls: cov.globShortfalls } : {}
154
165
  });
155
166
  if (cov.violations !== void 0) for (const v of cov.violations) events.push({
156
167
  _tag: "ThresholdViolation",
@@ -218,7 +229,8 @@ const coverageReportToBlock = (report) => {
218
229
  violations,
219
230
  ...cov.scoped ? { scoped: cov.scoped } : {},
220
231
  ...cov.scopedFiles !== void 0 ? { scopedFiles: cov.scopedFiles.length } : {},
221
- ...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {}
232
+ ...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {},
233
+ ...cov.globShortfalls !== void 0 ? { globShortfalls: cov.globShortfalls } : {}
222
234
  };
223
235
  };
224
236
  /**
@@ -300,16 +312,17 @@ const synthesizeFromAgentReport = (report, options = {}) => {
300
312
  fail = 1;
301
313
  suiteFailureCount++;
302
314
  const moduleError = mod.errors?.[0];
315
+ const suiteLabel = mod.tests.length > 0 ? SUITE_FAILURE_LABEL : SUITE_LOAD_FAILURE_LABEL;
303
316
  events.push({
304
317
  _tag: "TestStarted",
305
318
  modulePath: mod.file,
306
- testName: SUITE_LOAD_FAILURE_LABEL,
319
+ testName: suiteLabel,
307
320
  suitePath: []
308
321
  });
309
322
  events.push({
310
323
  _tag: "TestFinished",
311
324
  modulePath: mod.file,
312
- testName: SUITE_LOAD_FAILURE_LABEL,
325
+ testName: suiteLabel,
313
326
  suitePath: [],
314
327
  status: "failed",
315
328
  durationMs: 0,
@@ -340,7 +353,8 @@ const synthesizeFromAgentReport = (report, options = {}) => {
340
353
  gaps: coverage.gaps,
341
354
  ...coverage.scoped !== void 0 ? { scoped: coverage.scoped } : {},
342
355
  ...coverage.scopedFiles !== void 0 ? { scopedFiles: coverage.scopedFiles } : {},
343
- ...coverage.totalFiles !== void 0 ? { totalFiles: coverage.totalFiles } : {}
356
+ ...coverage.totalFiles !== void 0 ? { totalFiles: coverage.totalFiles } : {},
357
+ ...coverage.globShortfalls !== void 0 ? { globShortfalls: coverage.globShortfalls } : {}
344
358
  });
345
359
  if (coverage.violations !== void 0) for (const v of coverage.violations) events.push({
346
360
  _tag: "ThresholdViolation",
@@ -378,4 +392,4 @@ const synthesizeFromAgentReport = (report, options = {}) => {
378
392
  };
379
393
 
380
394
  //#endregion
381
- export { SUITE_LOAD_FAILURE_LABEL, synthesizeFromAgentReport, synthesizeRunEvents };
395
+ export { SUITE_FAILURE_LABEL, SUITE_LOAD_FAILURE_LABEL, synthesizeFromAgentReport, synthesizeRunEvents };