@vitest-agent/ui 1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dispatcher/cells/single-file-fail.js +28 -0
  4. package/dispatcher/cells/single-file-pass.js +19 -0
  5. package/dispatcher/cells/single-file-threshold.js +22 -0
  6. package/dispatcher/cells/single-project-fail.js +34 -0
  7. package/dispatcher/cells/single-project-pass.js +17 -0
  8. package/dispatcher/cells/single-project-threshold.js +25 -0
  9. package/dispatcher/cells/single-test-fail.js +22 -0
  10. package/dispatcher/cells/single-test-pass.js +19 -0
  11. package/dispatcher/cells/single-test-threshold.js +12 -0
  12. package/dispatcher/cells/workspace-fail.js +27 -0
  13. package/dispatcher/cells/workspace-pass.js +25 -0
  14. package/dispatcher/cells/workspace-threshold.js +28 -0
  15. package/dispatcher/classify.js +48 -0
  16. package/dispatcher/dispatch.js +73 -0
  17. package/dispatcher/footer.js +55 -0
  18. package/dispatcher/helpers.js +186 -0
  19. package/dispatcher/ink-helpers.js +79 -0
  20. package/format-duration.js +34 -0
  21. package/index.d.ts +1843 -0
  22. package/index.js +40 -0
  23. package/package.json +59 -0
  24. package/pubsub/Channel.js +30 -0
  25. package/pubsub/Publisher.js +37 -0
  26. package/pubsub/Subscriber.js +77 -0
  27. package/pubsub/index.js +5 -0
  28. package/reducer.js +260 -0
  29. package/render-agent.js +137 -0
  30. package/render-ink/CountColumns.js +34 -0
  31. package/render-ink/CoverageBlock.js +80 -0
  32. package/render-ink/FailureSection.js +69 -0
  33. package/render-ink/FailuresSection.js +85 -0
  34. package/render-ink/ModuleHeader.js +43 -0
  35. package/render-ink/ProjectRow.js +56 -0
  36. package/render-ink/StatusIcon.js +44 -0
  37. package/render-ink/StreamApp.js +453 -0
  38. package/render-ink/SuggestedActions.js +49 -0
  39. package/render-ink/TestRow.js +33 -0
  40. package/render-ink/TrendLine.js +42 -0
  41. package/render-ink/index.js +14 -0
  42. package/render-ink/spinner.js +62 -0
  43. package/render-ink/tag-suffix.js +18 -0
  44. package/synthesize.js +335 -0
  45. package/tsdoc-metadata.json +11 -0
package/synthesize.js ADDED
@@ -0,0 +1,335 @@
1
+ import { isTimeoutError } from "@vitest-agent/sdk";
2
+
3
+ //#region src/synthesize.ts
4
+ const ISO_ZERO = "1970-01-01T00:00:00.000Z";
5
+ const normalizeTestState = (state) => {
6
+ if (state === "passed") return "passed";
7
+ if (state === "failed") return "failed";
8
+ if (state === "skipped") return "skipped";
9
+ return "pending";
10
+ };
11
+ const formatStack = (stacks) => {
12
+ if (!stacks || stacks.length === 0) return void 0;
13
+ return stacks.map((frame) => {
14
+ if (typeof frame === "string") return frame;
15
+ return `at ${frame.method.length > 0 ? frame.method : "<anonymous>"} (${frame.file}:${frame.line}:${frame.column})`;
16
+ }).join("\n");
17
+ };
18
+ const mapTestError = (errors) => {
19
+ if (!errors || errors.length === 0) return void 0;
20
+ const first = errors[0];
21
+ if (first === void 0) return void 0;
22
+ const stack = formatStack(first.stacks);
23
+ const out = { message: first.message };
24
+ if (first.diff !== void 0) out.diff = first.diff;
25
+ if (stack !== void 0) out.stack = stack;
26
+ return out;
27
+ };
28
+ const collectSuitePath = (test) => {
29
+ const path = [];
30
+ let cursor = test.parent;
31
+ while (cursor !== void 0 && cursor.type === "suite") {
32
+ path.unshift(cursor.name);
33
+ cursor = cursor.parent;
34
+ }
35
+ return path;
36
+ };
37
+ /**
38
+ * Project a fully-completed Vitest run into the canonical
39
+ * `RunEvent` stream. The output is deterministic given a
40
+ * stable input.
41
+ *
42
+ * @param modules - the array of completed Vitest test modules
43
+ * @param options - optional metadata to thread into the event stream
44
+ * @returns an ordered array of run events
45
+ * @public
46
+ */
47
+ const synthesizeRunEvents = (modules, options = {}) => {
48
+ const runId = options.runId ?? "synthetic-run";
49
+ const configHash = options.configHash ?? "synthetic-config";
50
+ const startedAt = options.startedAt ?? ISO_ZERO;
51
+ const finishedAt = options.finishedAt ?? startedAt;
52
+ const classifications = options.classifications ?? /* @__PURE__ */ new Map();
53
+ const events = [];
54
+ events.push({
55
+ _tag: "RunStarted",
56
+ runId,
57
+ startedAt,
58
+ configHash
59
+ });
60
+ for (const mod of modules) events.push({
61
+ _tag: "ModuleQueued",
62
+ modulePath: mod.relativeModuleId
63
+ });
64
+ let totalPass = 0;
65
+ let totalFail = 0;
66
+ let totalSkip = 0;
67
+ let totalDuration = 0;
68
+ let totalTimeout = 0;
69
+ const failureBatches = [];
70
+ for (const mod of modules) {
71
+ events.push({
72
+ _tag: "ModuleStarted",
73
+ modulePath: mod.relativeModuleId,
74
+ startedAt
75
+ });
76
+ let pass = 0;
77
+ let fail = 0;
78
+ let skip = 0;
79
+ let moduleTimeout = 0;
80
+ const moduleDuration = mod.diagnostic()?.duration ?? 0;
81
+ totalDuration += moduleDuration;
82
+ const moduleTagCounts = {};
83
+ for (const test of mod.children.allTests()) {
84
+ const suitePath = collectSuitePath(test);
85
+ const result = test.result();
86
+ const diag = test.diagnostic();
87
+ const status = normalizeTestState(result?.state);
88
+ const durationMs = diag?.duration ?? 0;
89
+ for (const tag of test.tags) moduleTagCounts[tag] = (moduleTagCounts[tag] ?? 0) + 1;
90
+ events.push({
91
+ _tag: "TestStarted",
92
+ modulePath: mod.relativeModuleId,
93
+ testName: test.name,
94
+ suitePath
95
+ });
96
+ const error = mapTestError(result?.errors);
97
+ const firstErrorMsg = result?.errors?.[0]?.message;
98
+ const timedOut = status === "failed" && firstErrorMsg !== void 0 && isTimeoutError({ message: firstErrorMsg });
99
+ if (timedOut) moduleTimeout++;
100
+ events.push({
101
+ _tag: "TestFinished",
102
+ modulePath: mod.relativeModuleId,
103
+ testName: test.name,
104
+ suitePath,
105
+ status,
106
+ durationMs,
107
+ ...error !== void 0 && { error },
108
+ ...timedOut && { timedOut: true }
109
+ });
110
+ if (status === "passed") pass++;
111
+ else if (status === "failed") {
112
+ if (!timedOut) fail++;
113
+ failureBatches.push({
114
+ modulePath: mod.relativeModuleId,
115
+ testName: test.name
116
+ });
117
+ } else skip++;
118
+ }
119
+ totalTimeout += moduleTimeout;
120
+ const hasTagCounts = Object.keys(moduleTagCounts).length > 0;
121
+ events.push({
122
+ _tag: "ModuleFinished",
123
+ modulePath: mod.relativeModuleId,
124
+ passCount: pass,
125
+ failCount: fail,
126
+ skipCount: skip,
127
+ durationMs: moduleDuration,
128
+ ...moduleTimeout > 0 && { timeoutCount: moduleTimeout },
129
+ ...hasTagCounts && { tagCounts: moduleTagCounts }
130
+ });
131
+ totalPass += pass;
132
+ totalFail += fail;
133
+ totalSkip += skip;
134
+ }
135
+ if (options.coverage !== void 0) {
136
+ const cov = options.coverage;
137
+ events.push({
138
+ _tag: "CoverageReady",
139
+ metrics: cov.metrics,
140
+ thresholds: cov.thresholds,
141
+ gaps: cov.gaps
142
+ });
143
+ if (cov.violations !== void 0) for (const v of cov.violations) events.push({
144
+ _tag: "ThresholdViolation",
145
+ metric: v.metric,
146
+ expected: v.expected,
147
+ actual: v.actual
148
+ });
149
+ }
150
+ for (const failure of failureBatches) {
151
+ const classification = classifications.get(failure.testName);
152
+ if (classification !== void 0) events.push({
153
+ _tag: "FailureClassified",
154
+ modulePath: failure.modulePath,
155
+ testName: failure.testName,
156
+ classification
157
+ });
158
+ }
159
+ if (options.suggestedActions !== void 0) for (const action of options.suggestedActions) events.push({
160
+ _tag: "SuggestedAction",
161
+ severity: action.severity,
162
+ title: action.title,
163
+ detail: action.detail,
164
+ ...action.targetTool !== void 0 && { targetTool: action.targetTool }
165
+ });
166
+ events.push({
167
+ _tag: "RunFinished",
168
+ runId,
169
+ finishedAt,
170
+ passCount: totalPass,
171
+ failCount: totalFail,
172
+ skipCount: totalSkip,
173
+ durationMs: totalDuration,
174
+ ...totalTimeout > 0 && { timeoutCount: totalTimeout }
175
+ });
176
+ return events;
177
+ };
178
+ const coverageReportToBlock = (report) => {
179
+ const cov = report.coverage;
180
+ if (cov === void 0) return void 0;
181
+ const violations = [];
182
+ for (const metric of [
183
+ "lines",
184
+ "branches",
185
+ "functions",
186
+ "statements"
187
+ ]) {
188
+ const expected = cov.thresholds.global[metric];
189
+ const actual = cov.totals[metric];
190
+ if (expected !== void 0 && actual < expected) violations.push({
191
+ metric,
192
+ expected,
193
+ actual
194
+ });
195
+ }
196
+ const gaps = cov.lowCoverage.map((f) => ({
197
+ file: f.file,
198
+ missing: f.summary,
199
+ uncoveredLines: f.uncoveredLines.length > 0 ? f.uncoveredLines : void 0
200
+ }));
201
+ return {
202
+ metrics: cov.totals,
203
+ thresholds: cov.thresholds.global,
204
+ gaps,
205
+ violations
206
+ };
207
+ };
208
+ /**
209
+ * Bridge a persisted `AgentReport` into a `RunEvent` stream.
210
+ *
211
+ * `AgentReport` stores only failed modules in detail; passed-only modules
212
+ * are summarized via `summary.passed` without per-module breakdown. The
213
+ * synthesized stream reflects this — it emits per-test events for the
214
+ * failed modules and lets `RunFinished` carry the authoritative totals.
215
+ * Renderers see "N passed, M failed" in the header while the Modules
216
+ * section enumerates only the failing modules.
217
+ *
218
+ * @param report - the persisted agent report to synthesize from
219
+ * @param options - optional overrides for run metadata
220
+ * @returns an ordered array of run events
221
+ * @public
222
+ */
223
+ const synthesizeFromAgentReport = (report, options = {}) => {
224
+ const runId = options.runId ?? report.timestamp;
225
+ const startedAt = options.startedAt ?? report.timestamp;
226
+ const finishedAt = options.finishedAt ?? report.timestamp;
227
+ const configHash = options.configHash ?? "agent-report";
228
+ const events = [];
229
+ events.push({
230
+ _tag: "RunStarted",
231
+ runId,
232
+ startedAt,
233
+ configHash
234
+ });
235
+ for (const mod of report.failed) events.push({
236
+ _tag: "ModuleQueued",
237
+ modulePath: mod.file
238
+ });
239
+ let totalTimeoutCount = 0;
240
+ for (const mod of report.failed) {
241
+ events.push({
242
+ _tag: "ModuleStarted",
243
+ modulePath: mod.file,
244
+ startedAt
245
+ });
246
+ let pass = 0;
247
+ let fail = 0;
248
+ let skip = 0;
249
+ let moduleTimeoutCount = 0;
250
+ const moduleDuration = mod.duration ?? 0;
251
+ for (const test of mod.tests) {
252
+ const suitePath = test.fullName !== test.name && test.fullName.endsWith(` > ${test.name}`) ? test.fullName.slice(0, -` > ${test.name}`.length).split(" > ").filter((s) => s.length > 0) : [];
253
+ events.push({
254
+ _tag: "TestStarted",
255
+ modulePath: mod.file,
256
+ testName: test.name,
257
+ suitePath
258
+ });
259
+ const firstError = test.errors?.[0];
260
+ const error = firstError !== void 0 ? {
261
+ message: firstError.message,
262
+ ...firstError.diff !== void 0 && { diff: firstError.diff },
263
+ ...firstError.stack !== void 0 && { stack: firstError.stack }
264
+ } : void 0;
265
+ const timedOut = test.state === "failed" && firstError !== void 0 && isTimeoutError({ message: firstError.message });
266
+ if (timedOut) moduleTimeoutCount++;
267
+ events.push({
268
+ _tag: "TestFinished",
269
+ modulePath: mod.file,
270
+ testName: test.name,
271
+ suitePath,
272
+ status: test.state,
273
+ durationMs: test.duration ?? 0,
274
+ ...error !== void 0 && { error },
275
+ ...timedOut && { timedOut: true }
276
+ });
277
+ if (test.state === "passed") pass++;
278
+ else if (test.state === "failed") {
279
+ if (!timedOut) fail++;
280
+ } else skip++;
281
+ }
282
+ totalTimeoutCount += moduleTimeoutCount;
283
+ events.push({
284
+ _tag: "ModuleFinished",
285
+ modulePath: mod.file,
286
+ passCount: pass,
287
+ failCount: fail,
288
+ skipCount: skip,
289
+ durationMs: moduleDuration,
290
+ ...moduleTimeoutCount > 0 && { timeoutCount: moduleTimeoutCount }
291
+ });
292
+ }
293
+ const coverage = coverageReportToBlock(report);
294
+ if (coverage !== void 0) {
295
+ events.push({
296
+ _tag: "CoverageReady",
297
+ metrics: coverage.metrics,
298
+ thresholds: coverage.thresholds,
299
+ gaps: coverage.gaps
300
+ });
301
+ if (coverage.violations !== void 0) for (const v of coverage.violations) events.push({
302
+ _tag: "ThresholdViolation",
303
+ metric: v.metric,
304
+ expected: v.expected,
305
+ actual: v.actual
306
+ });
307
+ }
308
+ for (const mod of report.failed) for (const test of mod.tests) if (test.state === "failed" && test.classification !== void 0) events.push({
309
+ _tag: "FailureClassified",
310
+ modulePath: mod.file,
311
+ testName: test.name,
312
+ classification: test.classification
313
+ });
314
+ if (options.suggestedActions !== void 0) for (const action of options.suggestedActions) events.push({
315
+ _tag: "SuggestedAction",
316
+ severity: action.severity,
317
+ title: action.title,
318
+ detail: action.detail,
319
+ ...action.targetTool !== void 0 && { targetTool: action.targetTool }
320
+ });
321
+ events.push({
322
+ _tag: "RunFinished",
323
+ runId,
324
+ finishedAt,
325
+ passCount: report.summary.passed,
326
+ failCount: report.summary.failed,
327
+ skipCount: report.summary.skipped,
328
+ durationMs: report.summary.duration,
329
+ ...totalTimeoutCount > 0 && { timeoutCount: totalTimeoutCount }
330
+ });
331
+ return events;
332
+ };
333
+
334
+ //#endregion
335
+ export { synthesizeFromAgentReport, synthesizeRunEvents };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }