@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dispatcher/cells/single-file-fail.js +28 -0
- package/dispatcher/cells/single-file-pass.js +19 -0
- package/dispatcher/cells/single-file-threshold.js +22 -0
- package/dispatcher/cells/single-project-fail.js +34 -0
- package/dispatcher/cells/single-project-pass.js +17 -0
- package/dispatcher/cells/single-project-threshold.js +25 -0
- package/dispatcher/cells/single-test-fail.js +22 -0
- package/dispatcher/cells/single-test-pass.js +19 -0
- package/dispatcher/cells/single-test-threshold.js +12 -0
- package/dispatcher/cells/workspace-fail.js +27 -0
- package/dispatcher/cells/workspace-pass.js +25 -0
- package/dispatcher/cells/workspace-threshold.js +28 -0
- package/dispatcher/classify.js +48 -0
- package/dispatcher/dispatch.js +73 -0
- package/dispatcher/footer.js +55 -0
- package/dispatcher/helpers.js +186 -0
- package/dispatcher/ink-helpers.js +79 -0
- package/format-duration.js +34 -0
- package/index.d.ts +1843 -0
- package/index.js +40 -0
- package/package.json +59 -0
- package/pubsub/Channel.js +30 -0
- package/pubsub/Publisher.js +37 -0
- package/pubsub/Subscriber.js +77 -0
- package/pubsub/index.js +5 -0
- package/reducer.js +260 -0
- package/render-agent.js +137 -0
- package/render-ink/CountColumns.js +34 -0
- package/render-ink/CoverageBlock.js +80 -0
- package/render-ink/FailureSection.js +69 -0
- package/render-ink/FailuresSection.js +85 -0
- package/render-ink/ModuleHeader.js +43 -0
- package/render-ink/ProjectRow.js +56 -0
- package/render-ink/StatusIcon.js +44 -0
- package/render-ink/StreamApp.js +453 -0
- package/render-ink/SuggestedActions.js +49 -0
- package/render-ink/TestRow.js +33 -0
- package/render-ink/TrendLine.js +42 -0
- package/render-ink/index.js +14 -0
- package/render-ink/spinner.js +62 -0
- package/render-ink/tag-suffix.js +18 -0
- package/synthesize.js +335 -0
- 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
|
+
}
|