@quenty/cli-output-helpers 1.17.2 → 1.18.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/outputHelper.d.ts +11 -0
  3. package/dist/outputHelper.d.ts.map +1 -1
  4. package/dist/outputHelper.js +18 -0
  5. package/dist/outputHelper.js.map +1 -1
  6. package/dist/reporting/aggregated-batch-reporter.d.ts +31 -0
  7. package/dist/reporting/aggregated-batch-reporter.d.ts.map +1 -0
  8. package/dist/reporting/aggregated-batch-reporter.js +40 -0
  9. package/dist/reporting/aggregated-batch-reporter.js.map +1 -0
  10. package/dist/reporting/github/formatting.d.ts.map +1 -1
  11. package/dist/reporting/github/formatting.js +37 -32
  12. package/dist/reporting/github/formatting.js.map +1 -1
  13. package/dist/reporting/grouped-reporter.d.ts.map +1 -1
  14. package/dist/reporting/grouped-reporter.js +7 -28
  15. package/dist/reporting/grouped-reporter.js.map +1 -1
  16. package/dist/reporting/index.d.ts +2 -0
  17. package/dist/reporting/index.d.ts.map +1 -1
  18. package/dist/reporting/index.js +2 -0
  19. package/dist/reporting/index.js.map +1 -1
  20. package/dist/reporting/reporter.d.ts +17 -0
  21. package/dist/reporting/reporter.d.ts.map +1 -1
  22. package/dist/reporting/reporter.js.map +1 -1
  23. package/dist/reporting/result-status.d.ts +101 -0
  24. package/dist/reporting/result-status.d.ts.map +1 -0
  25. package/dist/reporting/result-status.js +138 -0
  26. package/dist/reporting/result-status.js.map +1 -0
  27. package/dist/reporting/result-status.test.d.ts +2 -0
  28. package/dist/reporting/result-status.test.d.ts.map +1 -0
  29. package/dist/reporting/result-status.test.js +108 -0
  30. package/dist/reporting/result-status.test.js.map +1 -0
  31. package/dist/reporting/spinner-reporter.d.ts +1 -0
  32. package/dist/reporting/spinner-reporter.d.ts.map +1 -1
  33. package/dist/reporting/spinner-reporter.js +18 -33
  34. package/dist/reporting/spinner-reporter.js.map +1 -1
  35. package/dist/reporting/summary-table-reporter.d.ts +7 -2
  36. package/dist/reporting/summary-table-reporter.d.ts.map +1 -1
  37. package/dist/reporting/summary-table-reporter.js +16 -27
  38. package/dist/reporting/summary-table-reporter.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/outputHelper.ts +22 -0
  41. package/src/reporting/aggregated-batch-reporter.ts +45 -0
  42. package/src/reporting/github/formatting.ts +52 -41
  43. package/src/reporting/grouped-reporter.ts +20 -42
  44. package/src/reporting/index.ts +13 -0
  45. package/src/reporting/reporter.ts +18 -0
  46. package/src/reporting/result-status.test.ts +149 -0
  47. package/src/reporting/result-status.ts +227 -0
  48. package/src/reporting/spinner-reporter.ts +40 -39
  49. package/src/reporting/summary-table-reporter.ts +29 -36
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"summary-table-reporter.js","sourceRoot":"","sources":["../../src/reporting/summary-table-reporter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"summary-table-reporter.js","sourceRoot":"","sources":["../../src/reporting/summary-table-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAsB,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAElE,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,GAEb,MAAM,oBAAoB,CAAC;AAgB5B;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,YAAY;IAC5C,MAAM,CAAgB;IACtB,aAAa,CAAS;IACtB,aAAa,CAAS;IACtB,YAAY,CAAS;IACrB,kBAAkB,CAAU;IAEpC,YAAY,KAAoB,EAAE,OAAqC;QACrE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,YAAY,IAAI,QAAQ,CAAC;QACvD,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,YAAY,IAAI,QAAQ,CAAC;QACvD,IAAI,CAAC,YAAY,GAAG,OAAO,EAAE,WAAW,IAAI,QAAQ,CAAC;QACrD,IAAI,CAAC,kBAAkB,GAAG,OAAO,EAAE,iBAAiB,IAAI,KAAK,CAAC;IAChE,CAAC;IAEQ,KAAK,CAAC,SAAS;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM;aACxB,cAAc,EAAE;aAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;aACpB,MAAM,CAAC,CAAC,CAAC,EAAsB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACtD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEjC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QAExD,yEAAyE;QACzE,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CACzD,CAAC;QAEF,MAAM,OAAO,GAAiC;YAC5C;gBACE,MAAM,EAAE,SAAS;gBACjB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW;gBAC3B,QAAQ,EAAE,EAAE;aACb;YACD;gBACE,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC;gBAChD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC;gBACnE,QAAQ,EAAE,EAAE;aACb;YACD;gBACE,MAAM,EAAE,UAAU;gBAClB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC;gBAC5C,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;aACzC;SACF,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAE3C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,MAAM,UAAU,GAAG,YAAY,CAAC,aAAa,CAAC,GAAG,MAAM,SAAS,CAAC,CAAC;QAClE,MAAM,UAAU,GACd,QAAQ,CAAC,MAAM,GAAG,CAAC;YACjB,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,QAAQ,CAAC,MAAM,SAAS,CAAC;YACvD,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,SAAS,CAAC;QAClC,MAAM,SAAS,GAAG,YAAY,CAAC,SAAS,CACtC,MAAM,gBAAgB,CAAC,UAAU,CAAC,EAAE,CACrC,CAAC;QACF,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;QAC3D,OAAO,CAAC,GAAG,CACT,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,IAAI,IAAI,CAAC,YAAY,KAAK,UAAU,KAAK,UAAU,IAAI,SAAS,EAAE,CAC5F,CAAC;QAEF,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,YAAY,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;YAC/D,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,MAAqB;QACpC,OAAO,mBAAmB,CAAC,MAAM,EAAE;YACjC,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,iBAAiB,EAAE,IAAI,CAAC,kBAAkB;SAC3C,CAAC,CAAC;IACL,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/cli-output-helpers",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "Helpers to generate Nevermore package and game templates",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -59,5 +59,5 @@
59
59
  "engines": {
60
60
  "node": ">=16"
61
61
  },
62
- "gitHead": "2808f7b0f98a18163c6819aeb5fd9064496bc63a"
62
+ "gitHead": "f75f93d5b2ac51cf7da3b7599788dcce31fd8a29"
63
63
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { AsyncLocalStorage } from 'async_hooks';
3
+ import { isCI } from './cli-utils.js';
3
4
 
4
5
  export type BoxOptions = {
5
6
  centered?: boolean;
@@ -230,6 +231,27 @@ export class OutputHelper {
230
231
  console.log(this._hasAnsi(message) ? message : this.formatWarning(message));
231
232
  }
232
233
 
234
+ /**
235
+ * Open a collapsible section, so a run of related output can be folded away.
236
+ *
237
+ * In CI this is a workflow command; elsewhere it is a plain header, since a
238
+ * `::group::` line is only noise in a terminal that cannot fold it. Always
239
+ * pair with {@link endGroup} — an unclosed group swallows everything printed
240
+ * after it.
241
+ */
242
+ public static startGroup(title: string, useGroups = isCI()): void {
243
+ console.log(
244
+ useGroups ? `::group::${title}` : this.formatDim(`── ${title} ──`)
245
+ );
246
+ }
247
+
248
+ /** Close the section opened by {@link startGroup}. */
249
+ public static endGroup(useGroups = isCI()): void {
250
+ if (useGroups) {
251
+ console.log('::endgroup::');
252
+ }
253
+ }
254
+
233
255
  /**
234
256
  * Logs hint to the console
235
257
  * @param message Message to write
@@ -0,0 +1,45 @@
1
+ import { OutputHelper } from '../outputHelper.js';
2
+ import { BaseReporter } from './reporter.js';
3
+
4
+ /**
5
+ * Batch progress for a run that is one execution rather than many.
6
+ *
7
+ * An aggregated batch has no per-package moment to report: every package starts
8
+ * in the same tick, before the task that runs them exists, and finishes in the
9
+ * same tick when it returns. A reporter built around per-package callbacks can
10
+ * only invent structure that is not there — which is what produced one empty
11
+ * group per package, and filed the shared upload under whichever package's
12
+ * async context happened to reach the promise first.
13
+ *
14
+ * So this one says nothing per package. The run is printed once, by whoever
15
+ * holds it, and the summary table recaps at the end.
16
+ */
17
+ export class AggregatedBatchReporter extends BaseReporter {
18
+ private _actionVerb: string;
19
+ private _total: number;
20
+
21
+ constructor(total: number, options?: { actionVerb?: string }) {
22
+ super();
23
+ this._total = total;
24
+ this._actionVerb = options?.actionVerb ?? 'Processing';
25
+ }
26
+
27
+ override async startAsync(): Promise<void> {
28
+ OutputHelper.info(
29
+ `${this._actionVerb} ${this._total} packages in one batch`
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Print a run that has already happened: its output, then what reading that
35
+ * output turned up.
36
+ *
37
+ * Taken as rendered lines rather than as a log to render, so the decision of
38
+ * what a line should look like stays in one place and this stays a printer.
39
+ */
40
+ printRun(renderedLines: string[]): void {
41
+ for (const line of renderedLines) {
42
+ console.log(line);
43
+ }
44
+ }
45
+ }
@@ -15,12 +15,14 @@ import {
15
15
  type IStateTracker,
16
16
  type PackageState,
17
17
  } from '../state/state-tracker.js';
18
+ import { formatProgressInline } from '../progress-format.js';
18
19
  import {
19
- formatProgressInline,
20
- formatProgressResult,
21
- isEmptyTestRun,
22
- isUnreportedTestRun,
23
- } from '../progress-format.js';
20
+ formatStatusText,
21
+ resolveResultStatus,
22
+ statusIcon,
23
+ tallyCaveats,
24
+ type ResultStatus,
25
+ } from '../result-status.js';
24
26
 
25
27
  // ── Public types ────────────────────────────────────────────────────────────
26
28
 
@@ -145,26 +147,37 @@ export function formatResultStatus(
145
147
  failureLabel: string,
146
148
  expectsTestCounts = false
147
149
  ): string {
148
- const duration = formatDurationMs(pkg.durationMs);
149
- const progressText = formatProgressResult(pkg.progressSummary);
150
- const empty = isEmptyTestRun(pkg.progressSummary);
150
+ const status = resolveResultStatus(pkg, {
151
+ successLabel,
152
+ failureLabel,
153
+ expectsTestCounts,
154
+ });
151
155
 
152
- if (pkg.success) {
153
- if (expectsTestCounts && isUnreportedTestRun(pkg.progressSummary)) {
154
- return `⚠️ **Unverified** — no test counts reported (${duration})`;
155
- }
156
- const label = progressText
157
- ? `${successLabel} ${progressText}`
158
- : successLabel;
159
- return empty ? `⚠️ ${label} (${duration})` : `✅ ${label} (${duration})`;
160
- }
156
+ // Bolded verdict, plain caveats: a reader scans a column of these for the
157
+ // verdict, and bolding the qualifier too would flatten the difference between
158
+ // them. Everything else — which states exist, what they are called, what
159
+ // counts as a warning — is the resolver's.
160
+ const verdict =
161
+ status.severity === 'failure'
162
+ ? status.failedPhase
163
+ ? `**${status.label}** at ${status.failedPhase}`
164
+ : `**${status.label}**`
165
+ : status.label;
166
+ const caveats = status.caveats.length > 0 ? ` - ${caveatText(status)}` : '';
167
+ const head = [verdict, status.progress].filter(Boolean).join(' ');
168
+
169
+ return `${statusIcon(
170
+ status.severity,
171
+ 'emoji'
172
+ )} ${head}${caveats} (${formatDurationMs(pkg.durationMs)})`;
173
+ }
161
174
 
162
- const failedPhase = pkg.failedPhase;
163
- const effectiveLabel = pkg.failureLabel ?? failureLabel;
164
- const label = failedPhase
165
- ? `**${effectiveLabel}** at ${failedPhase}`
166
- : `**${effectiveLabel}**`;
167
- return `❌ ${label} (${duration})`;
175
+ /** The caveat half of a status line, without the verdict in front of it. */
176
+ function caveatText(status: ResultStatus): string {
177
+ return formatStatusText({ ...status, label: '', progress: '' }).replace(
178
+ /^ *- */,
179
+ ''
180
+ );
168
181
  }
169
182
 
170
183
  export function getActionsRunUrl(): string | undefined {
@@ -292,7 +305,7 @@ export function formatGithubTableBody(
292
305
  statusText = formatResultStatus(
293
306
  pkg.result!,
294
307
  config.successLabel ?? 'Passed',
295
- config.failureLabel ?? 'Failed',
308
+ config.failureLabel ?? 'FAILED',
296
309
  config.expectsTestCounts ?? false
297
310
  );
298
311
  break;
@@ -314,18 +327,19 @@ export function formatGithubTableBody(
314
327
  if (allDone) {
315
328
  const passed = packages.filter((p) => p.status === 'passed').length;
316
329
  const failed = packages.filter((p) => p.status === 'failed').length;
317
- // Read the final result, not the live progress — progress is cleared once a
318
- // package resolves, so an end-of-run check against it never fires.
319
- const emptyRuns = packages.filter((p) =>
320
- isEmptyTestRun(p.result?.progressSummary ?? p.progress)
321
- ).length;
322
- const unreported = config.expectsTestCounts
323
- ? packages.filter(
324
- (p) =>
325
- p.status === 'passed' &&
326
- isUnreportedTestRun(p.result?.progressSummary)
327
- ).length
328
- : 0;
330
+ // Tallied off the same statuses the rows were drawn from, so a line here
331
+ // cannot contradict the column above it — which it could when each was
332
+ // counted separately. Read from the final result, not the live progress:
333
+ // progress is cleared once a package resolves.
334
+ const statuses = packages
335
+ .filter((p) => p.result !== undefined)
336
+ .map((p) =>
337
+ resolveResultStatus(p.result!, {
338
+ successLabel: config.successLabel ?? 'Passed',
339
+ failureLabel: config.failureLabel ?? 'FAILED',
340
+ expectsTestCounts: config.expectsTestCounts ?? false,
341
+ })
342
+ );
329
343
  const verb = config.summaryVerb ?? 'tested';
330
344
  const unit = packages.length === 1 ? 'package' : 'packages';
331
345
  footer = `**${
@@ -333,11 +347,8 @@ export function formatGithubTableBody(
333
347
  } ${unit} ${verb}, ${passed} passed, ${failed} failed** in ${formatDurationMs(
334
348
  elapsedMs
335
349
  )}`;
336
- if (emptyRuns > 0) {
337
- footer += `\n⚠️ ${emptyRuns} package(s) ran 0 tests — check test discovery`;
338
- }
339
- if (unreported > 0) {
340
- footer += `\n⚠️ ${unreported} package(s) reported no test counts — a pass here proves nothing about whether tests ran`;
350
+ for (const { message } of tallyCaveats(statuses)) {
351
+ footer += `\n⚠️ ${message}`;
341
352
  }
342
353
  } else {
343
354
  const done = packages.filter(
@@ -3,10 +3,11 @@ import { formatDurationMs, isCI } from '../cli-utils.js';
3
3
  import { type PackageResult, BaseReporter } from './reporter.js';
4
4
  import { type IStateTracker } from './state/state-tracker.js';
5
5
  import {
6
- formatProgressResult,
7
- isEmptyTestRun,
8
- isUnreportedTestRun,
9
- } from './progress-format.js';
6
+ colorStatus,
7
+ formatStatusText,
8
+ resolveResultStatus,
9
+ statusIcon,
10
+ } from './result-status.js';
10
11
 
11
12
  export interface GroupedReporterOptions {
12
13
  showLogs: boolean;
@@ -79,45 +80,22 @@ export class GroupedReporter extends BaseReporter {
79
80
 
80
81
  const showLogs = this._options.showLogs || !result.success;
81
82
  const duration = formatDurationMs(result.durationMs);
82
- const successLabel = this._options.successLabel ?? 'Passed';
83
- const failureLabel =
84
- result.failureLabel ?? this._options.failureLabel ?? 'FAILED';
85
83
 
86
- const progressText = formatProgressResult(result.progressSummary);
87
- const empty = isEmptyTestRun(result.progressSummary);
88
-
89
- const unverified =
90
- (this._options.expectsTestCounts ?? false) &&
91
- isUnreportedTestRun(result.progressSummary);
92
-
93
- if (result.success) {
94
- const label = unverified
95
- ? 'Unverified — no test counts reported'
96
- : progressText
97
- ? `${successLabel} ${progressText}`
98
- : successLabel;
99
- const formatted =
100
- empty || unverified
101
- ? OutputHelper.formatWarning(`${label} ⚠`)
102
- : OutputHelper.formatSuccess(label);
103
- const icon =
104
- empty || unverified
105
- ? OutputHelper.formatWarning('⚠')
106
- : OutputHelper.formatSuccess('✓');
107
- console.log(
108
- ` ${icon} ${formatted} ${OutputHelper.formatDim(`(${duration})`)}`
109
- );
110
- } else {
111
- const failedPhase = result.failedPhase;
112
- const label = failedPhase
113
- ? `${failureLabel} at ${failedPhase}`
114
- : failureLabel;
115
- console.log(
116
- ` ${OutputHelper.formatError('✗')} ${OutputHelper.formatError(
117
- label
118
- )} ${OutputHelper.formatDim(`(${duration})`)}`
119
- );
120
- }
84
+ const status = resolveResultStatus(result, {
85
+ successLabel: this._options.successLabel,
86
+ failureLabel: this._options.failureLabel,
87
+ expectsTestCounts: this._options.expectsTestCounts,
88
+ });
89
+
90
+ console.log(
91
+ ` ${colorStatus(
92
+ statusIcon(status.severity, 'ascii'),
93
+ status.severity
94
+ )} ${colorStatus(
95
+ formatStatusText(status),
96
+ status.severity
97
+ )} ${OutputHelper.formatDim(`(${duration})`)}`
98
+ );
121
99
 
122
100
  if (showLogs) {
123
101
  if (result.logs) {
@@ -26,6 +26,7 @@ export {
26
26
  WatchResultReporter,
27
27
  type WatchResultReporterOptions,
28
28
  } from './watch-result-reporter.js';
29
+ export { AggregatedBatchReporter } from './aggregated-batch-reporter.js';
29
30
  export { CompositeResultReporter } from './composite-result-reporter.js';
30
31
  export {
31
32
  buildResultReporter,
@@ -52,6 +53,18 @@ export {
52
53
  isEmptyTestRun,
53
54
  summarizeFailure,
54
55
  } from './progress-format.js';
56
+ export {
57
+ colorStatus,
58
+ formatStatusText,
59
+ resolveResultStatus,
60
+ statusIcon,
61
+ tallyCaveats,
62
+ type ResolveResultStatusOptions,
63
+ type ResultCaveat,
64
+ type ResultSeverity,
65
+ type ResultStatus,
66
+ type ResultStatusInput,
67
+ } from './result-status.js';
55
68
 
56
69
  // State tracking
57
70
  export {
@@ -5,6 +5,16 @@
5
5
  * and end in a terminal state (passed / failed).
6
6
  */
7
7
 
8
+ /**
9
+ * Something that makes a result less than it appears.
10
+ *
11
+ * All three are compatible with a pass, which is the point: a run that passed
12
+ * without proving anything must not render as one that did. Declared here
13
+ * rather than beside the resolver that reads them, so a result type can carry
14
+ * one without depending on how it is drawn.
15
+ */
16
+ export type ResultCaveat = 'no-counts' | 'logs-lost' | 'empty-run';
17
+
8
18
  /** Execution phases a package can move through. */
9
19
  export type JobPhase =
10
20
  | 'waiting'
@@ -71,6 +81,14 @@ export interface PackageResult {
71
81
  failedPhase?: JobPhase;
72
82
  /** Per-result override of the reporter's default failure label. */
73
83
  failureLabel?: string;
84
+ /**
85
+ * What qualifies this result, for facts the reporters cannot derive.
86
+ *
87
+ * `empty-run` and `no-counts` come from the counts themselves and are worked
88
+ * out by `resolveResultStatus`; anything here is something only the producer
89
+ * knows, such as a run whose log output the engine dropped.
90
+ */
91
+ caveats?: ResultCaveat[];
74
92
  }
75
93
 
76
94
  /** Summary of a complete batch run. */
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatStatusText,
4
+ resolveResultStatus,
5
+ statusIcon,
6
+ tallyCaveats,
7
+ type ResultStatusInput,
8
+ } from './result-status.js';
9
+
10
+ function counts(passed: number, total: number): ResultStatusInput {
11
+ return {
12
+ success: true,
13
+ progressSummary: {
14
+ kind: 'test-counts',
15
+ passed,
16
+ failed: total - passed,
17
+ total,
18
+ },
19
+ };
20
+ }
21
+
22
+ describe('resolveResultStatus', () => {
23
+ it('calls a clean pass a success', () => {
24
+ const status = resolveResultStatus(counts(68, 68));
25
+
26
+ expect(status.severity).toBe('success');
27
+ expect(formatStatusText(status)).toBe('Passed (68/68)');
28
+ });
29
+
30
+ it('keeps a failure a failure however it is qualified', () => {
31
+ // A caveat narrows what a result proves; it never softens a failure into a
32
+ // warning, or a red row would go amber for having lost its logs.
33
+ const status = resolveResultStatus({
34
+ ...counts(3, 4),
35
+ success: false,
36
+ caveats: ['logs-lost'],
37
+ });
38
+
39
+ expect(status.severity).toBe('failure');
40
+ expect(formatStatusText(status)).toBe('FAILED (3/4) - logs lost');
41
+ });
42
+
43
+ it('demotes a pass that carries a caveat to a warning', () => {
44
+ const status = resolveResultStatus({
45
+ ...counts(35, 35),
46
+ caveats: ['logs-lost'],
47
+ });
48
+
49
+ expect(status.severity).toBe('warning');
50
+ expect(formatStatusText(status)).toBe('Passed (35/35) - logs lost');
51
+ });
52
+
53
+ it('flags a pass that ran no tests', () => {
54
+ const status = resolveResultStatus(counts(0, 0));
55
+
56
+ expect(status.severity).toBe('warning');
57
+ expect(status.caveats).toEqual(['empty-run']);
58
+ });
59
+
60
+ it('calls a pass with no counts unverified, when counts were expected', () => {
61
+ // The claim being avoided: a green row with nothing behind it is
62
+ // indistinguishable from one that tested something.
63
+ const status = resolveResultStatus(
64
+ { success: true },
65
+ { expectsTestCounts: true }
66
+ );
67
+
68
+ expect(status.severity).toBe('warning');
69
+ expect(formatStatusText(status)).toBe(
70
+ 'Unverified - no test counts reported'
71
+ );
72
+ });
73
+
74
+ it('says nothing about missing counts where none were expected', () => {
75
+ // A deploy has no test counts and is not suspect for lacking them.
76
+ const status = resolveResultStatus({ success: true });
77
+
78
+ expect(status.severity).toBe('success');
79
+ expect(status.caveats).toEqual([]);
80
+ });
81
+
82
+ it('reports every caveat that applies, not just the first', () => {
83
+ const status = resolveResultStatus(
84
+ { ...counts(0, 0), caveats: ['logs-lost'] },
85
+ { expectsTestCounts: true }
86
+ );
87
+
88
+ expect(status.caveats).toEqual(['logs-lost', 'empty-run']);
89
+ expect(formatStatusText(status)).toBe(
90
+ 'Passed (0/0) - logs lost, ran 0 tests'
91
+ );
92
+ });
93
+
94
+ it('names the phase a failure happened in', () => {
95
+ const status = resolveResultStatus({
96
+ success: false,
97
+ failedPhase: 'uploading',
98
+ });
99
+
100
+ expect(formatStatusText(status)).toBe('FAILED at uploading');
101
+ });
102
+
103
+ it('takes the labels the caller uses for its own kind of run', () => {
104
+ const status = resolveResultStatus(
105
+ { success: true },
106
+ { successLabel: 'Deployed' }
107
+ );
108
+
109
+ expect(formatStatusText(status)).toBe('Deployed');
110
+ });
111
+ });
112
+
113
+ describe('tallyCaveats', () => {
114
+ it('counts each caveat across a run', () => {
115
+ const statuses = [
116
+ resolveResultStatus({ ...counts(5, 5), caveats: ['logs-lost'] }),
117
+ resolveResultStatus({ ...counts(9, 9), caveats: ['logs-lost'] }),
118
+ resolveResultStatus(counts(0, 0)),
119
+ ];
120
+
121
+ expect(tallyCaveats(statuses)).toEqual([
122
+ {
123
+ caveat: 'logs-lost',
124
+ count: 2,
125
+ message: expect.stringContaining('2 package(s) lost their log output'),
126
+ },
127
+ {
128
+ caveat: 'empty-run',
129
+ count: 1,
130
+ message: '1 package(s) ran 0 tests — check test discovery',
131
+ },
132
+ ]);
133
+ });
134
+
135
+ it('says nothing when every result is clean', () => {
136
+ expect(tallyCaveats([resolveResultStatus(counts(3, 3))])).toEqual([]);
137
+ });
138
+ });
139
+
140
+ describe('statusIcon', () => {
141
+ it('uses emoji for markdown and single-width glyphs for a terminal', () => {
142
+ // A variation-selector emoji is two columns wide, which breaks the aligned
143
+ // status column in the summary table.
144
+ expect(statusIcon('warning', 'emoji')).toBe('⚠️');
145
+ expect(statusIcon('warning', 'ascii')).toBe('⚠');
146
+ expect(statusIcon('success', 'ascii')).toBe('✓');
147
+ expect(statusIcon('failure', 'ascii')).toBe('✗');
148
+ });
149
+ });