@midscene/test 1.17.8-beta-20261106101231.0 → 1.17.8

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 (51) hide show
  1. package/dist/es/154.mjs +2 -0
  2. package/dist/es/164.mjs +1469 -0
  3. package/dist/es/164.mjs.map +1 -0
  4. package/dist/es/183.mjs +295 -0
  5. package/dist/es/183.mjs.map +1 -0
  6. package/dist/es/1~112.mjs +372 -0
  7. package/dist/es/1~112.mjs.map +1 -0
  8. package/dist/es/209.mjs +1 -0
  9. package/dist/es/299.mjs +1886 -0
  10. package/dist/es/299.mjs.map +1 -0
  11. package/dist/es/398.mjs +5 -0
  12. package/dist/es/398.mjs.map +1 -0
  13. package/dist/es/431.mjs +375 -0
  14. package/dist/es/431.mjs.map +1 -0
  15. package/dist/es/470.mjs +1 -0
  16. package/dist/es/487.mjs +1 -0
  17. package/dist/es/629.mjs +1 -0
  18. package/dist/es/793.mjs +1 -0
  19. package/dist/es/822.mjs +1 -0
  20. package/dist/es/946.mjs +1 -0
  21. package/dist/es/cli/index.mjs +3 -2196
  22. package/dist/es/cli/index.mjs.map +1 -1
  23. package/dist/es/cli/model-command.mjs +1 -296
  24. package/dist/es/index.mjs +10 -409
  25. package/dist/es/index.mjs.map +1 -1
  26. package/dist/es/midscene/index.mjs +9 -9
  27. package/dist/es/midscene/index.mjs.map +1 -1
  28. package/dist/es/runtime/index.mjs +4 -5
  29. package/dist/es/runtime/index.mjs.map +1 -1
  30. package/dist/es/test-cli.mjs +1 -3786
  31. package/dist/lib/0~703.js.map +1 -1
  32. package/dist/lib/cli/index.js +45 -58
  33. package/dist/lib/cli/index.js.map +1 -1
  34. package/dist/lib/cli/model-command.js +1 -1
  35. package/dist/lib/cli/model-command.js.map +1 -1
  36. package/dist/lib/index.js +8 -37
  37. package/dist/lib/index.js.map +1 -1
  38. package/dist/lib/midscene/index.js +1 -1
  39. package/dist/lib/midscene/index.js.map +1 -1
  40. package/dist/lib/runtime/index.js +1 -1
  41. package/dist/lib/runtime/index.js.map +1 -1
  42. package/dist/lib/test-cli.js +884 -877
  43. package/dist/lib/test-cli.js.map +1 -1
  44. package/dist/types/cli/index.d.ts +4 -0
  45. package/package.json +20 -21
  46. package/dist/es/1~4.mjs +0 -383
  47. package/dist/es/1~4.mjs.map +0 -1
  48. package/dist/es/1~731.mjs +0 -306
  49. package/dist/es/1~731.mjs.map +0 -1
  50. package/dist/es/cli/model-command.mjs.map +0 -1
  51. package/dist/es/test-cli.mjs.map +0 -1
@@ -0,0 +1,1469 @@
1
+ import string_width from "string-width";
2
+ import { DEFAULT_TEST_FILE_SELECTION, fileURLToPath, prepareTestRun, discoverTestConfig, inspect, formatModelUsage, stripVTControlCharacters, CliModelUsage, runPreparedTestProject, loadTestProject } from "./299.mjs";
3
+ import { sep, realpathSync, resolve, statSync, existsSync, isAbsolute, appendFileSync, relative, win32, writeFileSync } from "./154.mjs";
4
+ import { WorkflowExecutionFailure, sanitizeReportValue } from "./793.mjs";
5
+ import { chalk } from "./946.mjs";
6
+ import { renderTestBadgeLines } from "./431.mjs";
7
+ import { z } from "./487.mjs";
8
+ import { runWithConsoleLogHandler } from "./470.mjs";
9
+ var package_namespaceObject = {
10
+ rE: "1.17.8"
11
+ };
12
+ const formatCliError = (error)=>'string' == typeof error ? error : inspect(error, {
13
+ colors: false,
14
+ depth: null,
15
+ customInspect: false
16
+ });
17
+ function formatConciseError(error) {
18
+ const seen = new Set();
19
+ const messages = [];
20
+ let current = error;
21
+ while(current instanceof Error && !seen.has(current)){
22
+ seen.add(current);
23
+ if ('NODE_EXECUTION_ERROR' === current.code && void 0 !== current.cause) {
24
+ current = current.cause;
25
+ continue;
26
+ }
27
+ if (current.message && !messages.includes(current.message)) messages.push(current.message);
28
+ if (current instanceof AggregateError) {
29
+ for (const nested of current.errors)if (!(nested === current || seen.has(nested))) {
30
+ seen.add(nested);
31
+ messages.push(nested instanceof Error ? nested.message : String(nested));
32
+ }
33
+ }
34
+ current = current.cause;
35
+ }
36
+ if (!messages.length) return current instanceof Error ? current.message : formatCliError(current);
37
+ return messages.join('\nCaused by: ');
38
+ }
39
+ const oneLine = (value)=>stripVTControlCharacters(value).replace(/\s+/g, ' ').trim();
40
+ const shorten = (value, length)=>{
41
+ const text = oneLine(value);
42
+ return text.length <= length ? text : `${text.slice(0, length - 1)}…`;
43
+ };
44
+ function unwrapFrameLocation(location) {
45
+ if (!location.endsWith(')')) return location;
46
+ const absoluteStart = location.search(/ \((?:file:\/\/|[A-Za-z]:[\\/]|\/|\\\\)/);
47
+ if (-1 !== absoluteStart) return location.slice(absoluteStart + 2, -1);
48
+ let depth = 0;
49
+ for(let index = location.length - 1; index >= 0; index--)if (')' === location[index]) depth++;
50
+ else if ('(' === location[index] && 0 === --depth) return ' ' === location[index - 1] ? location.slice(index + 1, -1) : location;
51
+ return location;
52
+ }
53
+ function firstBusinessFrame(error, projectRoot) {
54
+ const seen = new Set();
55
+ const frames = [];
56
+ const visit = (value, depth = 0)=>{
57
+ if (!value || 'object' != typeof value || seen.has(value) || depth > 12) return;
58
+ seen.add(value);
59
+ const detail = value;
60
+ visit(detail.cause, depth + 1);
61
+ if (Array.isArray(detail.errors)) for (const child of detail.errors.slice(0, 10))visit(child, depth + 1);
62
+ if ('string' != typeof detail.stack) return;
63
+ for (const line of detail.stack.split('\n')){
64
+ if (!line.trim().startsWith('at ')) continue;
65
+ const location = unwrapFrameLocation(line.trim().slice(3).replace(/^async /, ''));
66
+ const match = location.match(/^(.*):(\d+):(\d+)$/);
67
+ if (!match) continue;
68
+ let file = match[1];
69
+ if (file.startsWith('file://')) try {
70
+ file = fileURLToPath(file);
71
+ } catch {
72
+ continue;
73
+ }
74
+ const normalized = file.replace(/\\/g, '/');
75
+ if (!(normalized.startsWith('node:') || normalized.includes('/node_modules/') || /(?:^|\/)packages\/(?:core|test|shared|web-integration)\/(?:dist|src)\//.test(normalized)) && /\.[cm]?[jt]sx?$/.test(normalized)) frames.push(`${file}:${match[2]}:${match[3]}`);
76
+ }
77
+ };
78
+ visit(error);
79
+ const chosen = frames[0];
80
+ if (!chosen || !projectRoot) return chosen;
81
+ const match = chosen.match(/^(.*):(\d+):(\d+)$/);
82
+ const file = match[1];
83
+ const paths = /^[A-Za-z]:[\\/]/.test(file) ? win32 : {
84
+ isAbsolute: isAbsolute,
85
+ relative: relative
86
+ };
87
+ const display = paths.isAbsolute(file) ? paths.relative(projectRoot, file) : file;
88
+ return `${display.replace(/\\/g, '/')}:${match[2]}:${match[3]}`;
89
+ }
90
+ function formatFailureInput(input) {
91
+ if (void 0 === input) return [];
92
+ const value = sanitizeReportValue(input).value;
93
+ const render = (value)=>shorten('string' == typeof value ? value : inspect(value, {
94
+ colors: false,
95
+ depth: 2,
96
+ maxArrayLength: 4,
97
+ maxStringLength: 180,
98
+ breakLength: 1 / 0,
99
+ customInspect: false,
100
+ getters: false
101
+ }), 160);
102
+ if (value && 'object' == typeof value && !Array.isArray(value)) {
103
+ const entries = Object.entries(value);
104
+ const rows = entries.slice(0, 4).map(([key, value])=>`${shorten(key, 48)}: ${render(value)}`);
105
+ if (entries.length > 4) rows.push(`… ${entries.length - 4} more fields (see Summary)`);
106
+ return rows;
107
+ }
108
+ return [
109
+ render(value)
110
+ ];
111
+ }
112
+ function formatFailureDetails(source, error, context = {}) {
113
+ const title = context.projectName ? `[${oneLine(context.projectName)}] ${oneLine(context.caseName ?? context.sourcePath ?? context.phase ?? source)}` : oneLine(source);
114
+ const body = [];
115
+ const field = (label, value)=>body.push(` ${label.padEnd(9)}${value}`);
116
+ const step = context.step;
117
+ if (step) {
118
+ const phase = 'steps' === step.phase ? '' : `${step.phase} `;
119
+ const index = step.stepIndex + 1;
120
+ const position = void 0 === context.stepCount ? String(index) : `${index}/${Math.max(index, context.stepCount)}`;
121
+ field('Step', `${phase}${position} · ${oneLine(step.node)}`);
122
+ } else if (context.phase) field('Phase', oneLine(context.phase));
123
+ if (context.sourcePath) {
124
+ const location = step?.meta.sourceLocation;
125
+ field('Location', `${oneLine(context.sourcePath)}${location ? `:${location.line}` : ''}`);
126
+ }
127
+ for (const [label, attempt] of [
128
+ [
129
+ 'Attempt',
130
+ context.attempt
131
+ ],
132
+ [
133
+ 'File try',
134
+ context.fileAttempt
135
+ ]
136
+ ])if (attempt && (attempt.total > 1 || attempt.index > 0)) field(label, `${attempt.index + 1}/${attempt.total}`);
137
+ if (body.length) body.push('');
138
+ const lines = stripVTControlCharacters(formatConciseError(error)).split(/\r?\n/);
139
+ body.push(` Error: ${shorten(lines[0], 320)}`, ...lines.slice(1, 6).map((line)=>` ${shorten(line, 320)}`));
140
+ if (lines.length > 6) body.push(' … (use --reporter verbose for details)');
141
+ const input = step ? formatFailureInput(step.input) : [];
142
+ if (input.length) {
143
+ body.push('');
144
+ input.forEach((line, index)=>field(0 === index ? 'Input' : '', line));
145
+ }
146
+ const frame = firstBusinessFrame(error, context.projectRoot);
147
+ if (frame) body.push('', ` at ${oneLine(frame)}`);
148
+ return {
149
+ title,
150
+ body
151
+ };
152
+ }
153
+ function _define_property(obj, key, value) {
154
+ if (key in obj) Object.defineProperty(obj, key, {
155
+ value: value,
156
+ enumerable: true,
157
+ configurable: true,
158
+ writable: true
159
+ });
160
+ else obj[key] = value;
161
+ return obj;
162
+ }
163
+ const graphemes = new Intl.Segmenter(void 0, {
164
+ granularity: 'grapheme'
165
+ });
166
+ const SYNC_START = '\u001b[?2026h';
167
+ const SYNC_END = '\u001b[?2026l';
168
+ const truncateTerminalLine = (text, columns)=>{
169
+ if (columns <= 0) return '';
170
+ if (string_width(text) <= columns) return text;
171
+ let output = '';
172
+ let width = 0;
173
+ for (const { segment } of graphemes.segment(stripVTControlCharacters(text))){
174
+ width += string_width(segment);
175
+ if (width > columns - 1) break;
176
+ output += segment;
177
+ }
178
+ return `${output}…`;
179
+ };
180
+ class TerminalProgress {
181
+ erase() {
182
+ if (!this.lines.length) return '';
183
+ const columns = Math.max(1, this.io.columns ?? 100);
184
+ const occupiedRows = this.lines.reduce((sum, line)=>sum + Math.max(1, Math.ceil(string_width(line) / columns)), 0);
185
+ const rows = Math.min(occupiedRows, Math.max(0, (this.io.rows ?? 24) - 1));
186
+ this.lines = [];
187
+ return `\r${rows ? `\u001b[${rows}A` : ''}\u001b[0J`;
188
+ }
189
+ frame(previousHeight = 0) {
190
+ const availableRows = Math.max(0, (this.io.rows ?? 24) - 2);
191
+ const columns = Math.max(0, (this.io.columns ?? 100) - 1);
192
+ if (!availableRows || !columns) return '';
193
+ const footer = this.footer.slice(0, availableRows);
194
+ const budget = availableRows - footer.length;
195
+ const overflow = this.content.length > budget;
196
+ const visible = Math.max(0, budget - (overflow ? 1 : 0));
197
+ const body = this.content.slice(0, visible);
198
+ if (overflow && budget > 0) body.push(` … ${this.content.length - visible} more rows`);
199
+ const lines = [
200
+ ...body,
201
+ ...footer
202
+ ].map((line)=>truncateTerminalLine(line, columns));
203
+ const blanks = Math.max(0, Math.min(previousHeight, availableRows) - lines.length);
204
+ this.lines = [
205
+ ...Array.from({
206
+ length: blanks
207
+ }, ()=>''),
208
+ ...lines
209
+ ];
210
+ return this.lines.length ? `${this.lines.join('\n')}\n` : '';
211
+ }
212
+ render(content, footer) {
213
+ this.update(content, footer);
214
+ const previousHeight = this.lines.length;
215
+ const erase = this.erase();
216
+ this.io.write?.(`${SYNC_START}${erase}${this.frame(previousHeight)}${SYNC_END}`);
217
+ }
218
+ update(content, footer) {
219
+ this.content = content;
220
+ this.footer = footer;
221
+ }
222
+ output(message, write) {
223
+ this.io.write?.(SYNC_START);
224
+ try {
225
+ const erase = this.erase();
226
+ if (erase) this.io.write?.(erase);
227
+ write(message.endsWith('\n') ? message : `${message}\n`);
228
+ const frame = this.frame();
229
+ if (frame) this.io.write?.(frame);
230
+ } finally{
231
+ this.io.write?.(SYNC_END);
232
+ }
233
+ }
234
+ clear() {
235
+ const erase = this.erase();
236
+ this.content = [];
237
+ this.footer = [];
238
+ if (erase) this.io.write?.(`${SYNC_START}${erase}${SYNC_END}`);
239
+ }
240
+ constructor(io){
241
+ _define_property(this, "io", void 0);
242
+ _define_property(this, "lines", void 0);
243
+ _define_property(this, "content", void 0);
244
+ _define_property(this, "footer", void 0);
245
+ this.io = io;
246
+ this.lines = [];
247
+ this.content = [];
248
+ this.footer = [];
249
+ }
250
+ }
251
+ const warning_format_oneLine = (text)=>stripVTControlCharacters(text).replace(/\s+/g, ' ').trim();
252
+ function formatWarningScope(scope) {
253
+ if (!scope) return '';
254
+ const parts = [
255
+ `[${warning_format_oneLine(scope.projectName)}]`
256
+ ];
257
+ if (scope.sourcePath) parts.push(warning_format_oneLine(scope.sourcePath));
258
+ if (scope.caseName) parts.push(warning_format_oneLine(scope.caseName));
259
+ if (scope.phase) {
260
+ const phase = 'steps' === scope.phase ? 'step' : scope.phase;
261
+ parts.push(`${phase}${void 0 === scope.stepIndex ? '' : `[${scope.stepIndex + 1}]`}${scope.node ? ` ${warning_format_oneLine(scope.node)}` : ''}`);
262
+ }
263
+ return `${parts.join(' › ')} · `;
264
+ }
265
+ function errorMessage(error) {
266
+ const seen = new Set();
267
+ let current = error;
268
+ let message = '';
269
+ let status;
270
+ while(current && 'object' == typeof current && !seen.has(current)){
271
+ seen.add(current);
272
+ const detail = current;
273
+ if ('string' == typeof detail.message) message = detail.message;
274
+ if ('number' == typeof detail.status) status = detail.status;
275
+ current = detail.cause;
276
+ }
277
+ const firstLine = warning_format_oneLine((message || String(error)).split(/\r?\n/)[0]);
278
+ return status && !firstLine.startsWith(String(status)) ? `${status}: ${firstLine}` : firstLine;
279
+ }
280
+ function formatWarning(entry) {
281
+ const details = entry.warning;
282
+ let message;
283
+ if (details?.kind === 'model-request-retry') message = `model request retry ${details.attempt + 1}/${details.maxAttempts} in ${details.delayMs / 1000}s (${warning_format_oneLine(details.modelName)}): ${errorMessage(details.error)}`;
284
+ else {
285
+ const error = entry.args?.find((arg)=>arg instanceof Error);
286
+ const prefix = 'string' == typeof entry.args?.[0] ? warning_format_oneLine(entry.args[0].split(/\r?\n/)[0]).replace(/:\s*$/, '') : '';
287
+ message = error ? `${prefix ? `${prefix}: ` : ''}${errorMessage(error)}` : warning_format_oneLine(entry.message.split(/\r?\n/)[0]);
288
+ message = message.replace(/^\[midscene:warning\]\s*/, '');
289
+ }
290
+ if (message.startsWith('waitForNetworkIdle is skipped for Playwright.')) message = 'Playwright: post-action network-idle wait skipped.';
291
+ return message.length > 240 ? `${message.slice(0, 239)}…` : message;
292
+ }
293
+ function warningKey(entry, message) {
294
+ const details = entry.warning;
295
+ if (details?.kind === 'model-request-retry') return `${entry.topic}\n${JSON.stringify(entry.scope)}\n${details.callId}\n${details.attempt}`;
296
+ const scope = entry.args?.some((arg)=>arg instanceof Error) ? JSON.stringify(entry.scope) : '';
297
+ return `${entry.topic}\n${scope}\n${message}`;
298
+ }
299
+ function terminal_reporter_define_property(obj, key, value) {
300
+ if (key in obj) Object.defineProperty(obj, key, {
301
+ value: value,
302
+ enumerable: true,
303
+ configurable: true,
304
+ writable: true
305
+ });
306
+ else obj[key] = value;
307
+ return obj;
308
+ }
309
+ const terminal_reporter_oneLine = (value)=>stripVTControlCharacters(value).replace(/\s+/g, ' ').trim();
310
+ const formatDuration = (ms)=>{
311
+ if (ms < 1000) return `${Math.round(ms)}ms`;
312
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
313
+ return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
314
+ };
315
+ const identity = (project, path, name)=>`[${terminal_reporter_oneLine(project)}] ${terminal_reporter_oneLine(path)}${name ? ` › ${terminal_reporter_oneLine(name)}` : ''}`;
316
+ const projectPhaseLabel = (phase)=>'setup' === phase ? 'setting up' : 'cleaning up';
317
+ const fileStatus = (status, cases)=>{
318
+ if ('failed' === status || cases.some((item)=>'failed' === item.status)) return 'failed';
319
+ if (cases.length && cases.every((item)=>'not-run' === item.status)) return 'not-run';
320
+ if (cases.some((item)=>'not-run' === item.status)) return 'failed';
321
+ return status ?? (cases.length ? 'success' : 'not-run');
322
+ };
323
+ class TerminalReporter {
324
+ get interactive() {
325
+ return !!this.io.isTTY && !!this.io.write && 'default' === this.mode;
326
+ }
327
+ log(message) {
328
+ if (this.interactive) this.output(`${message}\n`, (text)=>this.io.write(text));
329
+ else this.io.log(message);
330
+ this.lastOutput = Date.now();
331
+ }
332
+ output(message, write) {
333
+ this.renderProgress(false);
334
+ this.progressView.output(message, write);
335
+ this.lastOutput = Date.now();
336
+ }
337
+ progress(message) {
338
+ if ('verbose' === this.mode) this.log(message);
339
+ }
340
+ console(entry) {
341
+ if ('report' === entry.type) {
342
+ if ('verbose' === this.mode) this.log(entry.message);
343
+ return;
344
+ }
345
+ const scope = formatWarningScope(entry.scope);
346
+ if ('verbose' === this.mode) {
347
+ const kind = entry.warning?.kind === 'model-request-retry' ? 'model request retry · ' : entry.warning?.kind === 'model-request-detail' ? 'model request error · ' : '';
348
+ this.log(`WARN ${scope}${kind}${entry.message}`);
349
+ return;
350
+ }
351
+ if (entry.warning?.kind === 'model-request-detail') return;
352
+ const message = formatWarning(entry);
353
+ const key = warningKey(entry, message);
354
+ if (this.warnings.has(key)) return;
355
+ this.warnings.set(key, {
356
+ scope,
357
+ message
358
+ });
359
+ if (this.interactive) {
360
+ let attached = false;
361
+ if (entry.warning?.kind === 'model-request-retry' && entry.scope) {
362
+ const owner = entry.scope;
363
+ for (const [id, task] of this.active)if (task.file.projectName === owner.projectName && (!owner.sourcePath || task.file.sourcePath === owner.sourcePath) && (!owner.caseId || id === owner.caseId) && (!owner.runId || task.runId === owner.runId) && (!owner.phase || task.phase === owner.phase) && (void 0 === owner.stepIndex || task.stepIndex === owner.stepIndex)) {
364
+ task.warning = message;
365
+ attached = true;
366
+ }
367
+ }
368
+ if (!attached) this.latestNotice = message.startsWith('Playwright:') ? 'Playwright: network-idle wait skipped.' : `${scope}${message}`;
369
+ this.schedule();
370
+ return;
371
+ }
372
+ this.log(`${this.color.yellow('WARN')} ${this.color.dim(scope)}${message}`);
373
+ }
374
+ file(projectName, sourcePath, total = 0) {
375
+ const key = `${projectName}\0${sourcePath}`;
376
+ let file = this.files.get(key);
377
+ if (!file) {
378
+ file = {
379
+ projectName,
380
+ sourcePath,
381
+ total,
382
+ outcomes: new Map(),
383
+ attemptOutcomes: new Map(),
384
+ started: false,
385
+ printed: false,
386
+ durationMs: 0,
387
+ retries: 0,
388
+ completionOrder: 0
389
+ };
390
+ this.files.set(key, file);
391
+ }
392
+ file.total = Math.max(file.total, total);
393
+ return file;
394
+ }
395
+ event(event) {
396
+ if ('run-start' === event.type) {
397
+ this.total = event.cases;
398
+ this.totalFiles = event.documents;
399
+ this.startedAt = event.startedAt ? Date.parse(event.startedAt) : Date.now();
400
+ for (const file of event.files ?? [])this.file(file.projectName, file.sourcePath, file.cases);
401
+ const c = this.color;
402
+ this.log(`\n ${c.bgGreen.black.bold(' RUN ')} ${c.green(`midscene-test v${package_namespaceObject.rE}`)} ${c.dim(terminal_reporter_oneLine(event.projectRoot ?? process.cwd()))}`);
403
+ this.log(` ${c.dim(`${event.projects} projects · ${event.documents} files · ${event.cases} cases`)}`);
404
+ if (void 0 !== event.maxConcurrentProjects && void 0 !== event.maxConcurrentFiles) this.log(` ${c.dim(`Concurrency ${event.maxConcurrentProjects ?? 'unlimited'} projects · ${event.maxConcurrentFiles ?? 'unlimited'} files (global limits)`)}`);
405
+ if (event.configPath) this.log(` ${c.dim(`Config ${terminal_reporter_oneLine(event.configPath)}`)}`);
406
+ for (const model of event.configuredModels ?? [])this.log(` ${c.dim('Models (env)')} ${terminal_reporter_oneLine(model.name)} ${c.dim(`· ${model.intents.map(terminal_reporter_oneLine).join(', ')}`)}`);
407
+ this.log('');
408
+ if ('default' === this.mode) {
409
+ this.timer = setInterval(()=>this.tick(), this.interactive ? 1000 : 30000);
410
+ this.timer.unref();
411
+ if (this.interactive) this.removeResizeListener = this.io.onResize?.(()=>this.renderProgress());
412
+ this.renderProgress();
413
+ }
414
+ } else if ('project-end' === event.type) {
415
+ this.completeProject(event.project);
416
+ this.schedule();
417
+ } else if ('project-phase' === event.type) {
418
+ if ('running' === event.phase) this.projectPhases.delete(event.projectName);
419
+ else this.projectPhases.set(event.projectName, {
420
+ phase: event.phase,
421
+ since: Date.now()
422
+ });
423
+ this.schedule();
424
+ } else if ('document-start' === event.type) {
425
+ this.file(event.projectName, event.sourcePath, event.cases).started = true;
426
+ this.schedule();
427
+ } else if ('document-end' === event.type) {
428
+ const file = this.file(event.document.projectName, event.document.sourcePath, event.cases.length);
429
+ file.status = fileStatus(event.document.status, event.cases);
430
+ file.started = true;
431
+ file.completionOrder = ++this.completionOrder;
432
+ file.durationMs = event.durationMs;
433
+ file.retries = event.document.attemptIndex ?? 0;
434
+ file.outcomes = new Map(event.cases.map((outcome)=>[
435
+ outcome.caseId,
436
+ outcome
437
+ ]));
438
+ file.attemptOutcomes.clear();
439
+ for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
440
+ if (!this.interactive) this.printFile(file);
441
+ this.schedule();
442
+ } else if ('step-start' === event.type) this.startStep(event.projectName, event.info);
443
+ else if ('step-end' === event.type) {
444
+ if (event.result) this.usage.add(event.result);
445
+ const key = 'case' === event.info.scope ? event.info.case.caseId : event.info.document.documentId;
446
+ const task = this.active.get(key);
447
+ if (task) if ('document' === event.info.scope) this.active.delete(key);
448
+ else {
449
+ task.step = void 0;
450
+ task.warning = void 0;
451
+ }
452
+ this.schedule();
453
+ } else if ('case-attempt-end' === event.type) this.attempts.set(event.attempt.caseId, {
454
+ run: event.attempt,
455
+ max: event.maxAttempts
456
+ });
457
+ else if ('case-end' === event.type || 'case-progress' === event.type) {
458
+ const file = this.file(event.outcome.projectName, event.outcome.sourcePath);
459
+ if ('case-progress' === event.type) {
460
+ file.attemptOutcomes.set(event.outcome.caseId, event.outcome);
461
+ file.retries = Math.max(file.retries, event.documentAttemptIndex);
462
+ } else {
463
+ file.attemptOutcomes.delete(event.outcome.caseId);
464
+ file.outcomes.set(event.outcome.caseId, event.outcome);
465
+ file.retries = Math.max(file.retries, event.documentRetries ?? 0);
466
+ }
467
+ this.active.delete(event.outcome.caseId);
468
+ this.schedule();
469
+ } else if ('document-retry' === event.type) {
470
+ const file = this.file(event.document.projectName, event.document.sourcePath);
471
+ file.outcomes.clear();
472
+ file.attemptOutcomes.clear();
473
+ file.status = void 0;
474
+ file.completionOrder = 0;
475
+ for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
476
+ this.log(this.color.yellow(` ↻ ${identity(file.projectName, file.sourcePath)} · file retry ${(event.document.attemptIndex ?? 0) + 2}/${event.maxAttempts}`));
477
+ this.schedule();
478
+ }
479
+ }
480
+ startStep(projectName, info) {
481
+ const position = 'case' === info.scope ? info.case : info.document;
482
+ const file = this.file(projectName, position.sourcePath);
483
+ file.started = true;
484
+ const key = 'case' === info.scope ? info.case.caseId : info.document.documentId;
485
+ const attempt = 'case' === info.scope ? info.case.attemptIndex : 0;
486
+ const previous = this.attempts.get(key);
487
+ if (previous && previous.run.attemptIndex < attempt) {
488
+ this.attempts.delete(key);
489
+ if ('default' === this.mode && !this.interactive) this.log(this.color.yellow(` ↻ ${identity(projectName, position.sourcePath, 'case' === info.scope ? info.case.name : void 0)} · case retry · attempt ${attempt + 1}/${previous.max}`));
490
+ }
491
+ const existing = this.active.get(key);
492
+ const now = Date.now();
493
+ this.active.set(key, {
494
+ file,
495
+ name: 'case' === info.scope ? info.case.name : void 0,
496
+ step: `${'steps' === position.phase ? 'step' : position.phase} ${position.stepIndex + 1}/${info.stepCount} · ${terminal_reporter_oneLine(info.node)}`,
497
+ since: existing?.attempt === attempt ? existing.since : now,
498
+ attempt,
499
+ maxAttempts: previous?.max ?? existing?.maxAttempts,
500
+ runId: 'case' === info.scope ? info.case.runId : info.document.documentRunId,
501
+ phase: position.phase,
502
+ stepIndex: position.stepIndex,
503
+ caseIndex: 'case' === info.scope ? info.case.caseIndex : void 0
504
+ });
505
+ this.schedule();
506
+ }
507
+ schedule() {
508
+ if (!this.interactive || !this.timer || this.scheduled) return;
509
+ this.scheduled = setTimeout(()=>{
510
+ this.scheduled = void 0;
511
+ this.renderProgress();
512
+ }, 100);
513
+ this.scheduled.unref();
514
+ }
515
+ counts(items, total) {
516
+ const failed = items.filter((item)=>'failed' === item.status).length;
517
+ const passed = items.filter((item)=>'success' === item.status).length;
518
+ const notRun = items.filter((item)=>'not-run' === item.status).length;
519
+ return this.stateCounts(passed, failed, notRun, total);
520
+ }
521
+ stateCounts(passed, failed, notRun, total) {
522
+ const c = this.color;
523
+ return [
524
+ failed ? c.red.bold(`${failed} failed`) : '',
525
+ c.green.bold(`${passed} passed`),
526
+ notRun ? c.yellow(`${notRun} not run`) : ''
527
+ ].filter(Boolean).join(c.dim(' | ')) + c.dim(` (${total})`);
528
+ }
529
+ footer(durationMs = Date.now() - this.startedAt, result) {
530
+ const files = [
531
+ ...this.files.values()
532
+ ];
533
+ const cases = files.flatMap((file)=>[
534
+ ...file.outcomes.values()
535
+ ]);
536
+ const c = this.color;
537
+ const metrics = result ? this.finalMetrics : this.usage.snapshot();
538
+ return [
539
+ ` ${c.dim('Test Files')} ${this.counts(files, this.totalFiles)}`,
540
+ ` ${c.dim(' Cases')} ${result ? this.stateCounts(result.summary.passed, result.summary.failed, result.summary.notRun, result.summary.total) : this.counts(cases, this.total)}`,
541
+ ` ${c.dim(' Start at')} ${new Date(this.startedAt).toLocaleTimeString('en-GB', {
542
+ hour12: false
543
+ })}`,
544
+ ` ${c.dim(' Duration')} ${formatDuration(durationMs)}`,
545
+ ...formatModelUsage(metrics).map((model, index)=>` ${c.dim(index ? ' ' : ' Models:')} ${model}`),
546
+ ...!result || this.warnings.size ? [
547
+ ` ${c.dim(' Warnings')} ${this.warnings.size ? c.yellow(String(this.warnings.size)) : c.dim('0')}${!result && this.latestNotice ? c.dim(` · ${this.latestNotice}`) : ''}`
548
+ ] : []
549
+ ];
550
+ }
551
+ renderProgress(render = true) {
552
+ if (!this.interactive || !this.timer) return;
553
+ const projects = new Map();
554
+ const files = [
555
+ ...this.files.values()
556
+ ].filter((file)=>file.started && !file.printed).sort((a, b)=>a.projectName.localeCompare(b.projectName) || a.sourcePath.localeCompare(b.sourcePath));
557
+ for (const file of files){
558
+ const group = projects.get(file.projectName) ?? [];
559
+ group.push(file);
560
+ projects.set(file.projectName, group);
561
+ }
562
+ for (const projectName of this.projectPhases.keys())if (!projects.has(projectName)) projects.set(projectName, []);
563
+ const footer = this.footer();
564
+ const budget = Math.max(0, (this.io.rows ?? 24) - 2 - footer.length - 1);
565
+ let lines = this.liveTree(projects, 'full');
566
+ if (lines.length > budget) lines = this.liveTree(projects, 'recent');
567
+ if (lines.length > budget) lines = this.liveTree(projects, 'compact');
568
+ if (lines.length > budget) lines = this.liveTree(projects, 'active');
569
+ if (lines.length) lines.push('');
570
+ if (render) this.progressView.render(lines, footer);
571
+ else this.progressView.update(lines, footer);
572
+ }
573
+ liveTree(projects, mode) {
574
+ const c = this.color;
575
+ const lines = [];
576
+ const groups = [
577
+ ...projects
578
+ ];
579
+ const isActive = ([projectName, files])=>this.projectPhases.has(projectName) || files.some((file)=>!file.status);
580
+ if ('full' !== mode) groups.sort((a, b)=>Number(isActive(b)) - Number(isActive(a)));
581
+ for (const [projectName, files] of groups){
582
+ const phase = this.projectPhases.get(projectName);
583
+ const running = files.filter((file)=>!file.status);
584
+ const completed = files.filter((file)=>file.status);
585
+ const recent = [
586
+ ...completed
587
+ ].sort((a, b)=>b.completionOrder - a.completionOrder).slice(0, 'recent' === mode ? 1 : 0);
588
+ const visible = 'full' === mode ? files : [
589
+ ...running,
590
+ ...recent
591
+ ];
592
+ const hidden = completed.filter((file)=>!visible.includes(file));
593
+ const hiddenFailures = hidden.filter((file)=>'failed' === file.status).length;
594
+ const details = [
595
+ phase ? c.yellow(`${projectPhaseLabel(phase.phase)} ${formatDuration(Date.now() - phase.since)}`) : '',
596
+ c.yellow(`${running.length} ${1 === running.length ? 'file' : 'files'} running`),
597
+ completed.length ? c.dim(`${completed.length} completed`) : '',
598
+ 'active' === mode && hiddenFailures ? c.red(`${hiddenFailures} failed`) : ''
599
+ ].filter(Boolean);
600
+ lines.push(this.liveRow(` ${c.yellow('❯')} ${c.bold(terminal_reporter_oneLine(projectName))}`, c.dim(' · ') + details.join(c.dim(' · '))));
601
+ for (const [fileIndex, file] of visible.entries()){
602
+ const lastFile = fileIndex === visible.length - 1 && (!hidden.length || 'active' === mode);
603
+ const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
604
+ const outcomes = [
605
+ ...new Map([
606
+ ...file.attemptOutcomes,
607
+ ...file.outcomes
608
+ ]).values()
609
+ ].sort((a, b)=>a.caseIndex - b.caseIndex);
610
+ const failedCases = outcomes.filter((outcome)=>'failed' === outcome.status).length;
611
+ lines.push(this.liveRow(` ${c.dim(lastFile ? '└─' : '├─')} ${file.status ? `${this.statusSymbol(file.status)} ` : ''}${terminal_reporter_oneLine(file.sourcePath)}`, ` ${c.dim(`${outcomes.length}/${file.total} cases`)}${file.status ? c.dim(` ${formatDuration(file.durationMs)}`) : ''}${'active' === mode && failedCases ? c.red(` · ${failedCases} failed`) : ''}`));
612
+ const tasks = [
613
+ ...this.active.values()
614
+ ].filter((task)=>task.file === file);
615
+ const shown = 'full' === mode ? outcomes : 'recent' === mode ? outcomes.slice(-1) : [];
616
+ const children = [
617
+ ...tasks.map((task)=>({
618
+ task
619
+ })),
620
+ ...shown.map((outcome)=>({
621
+ outcome
622
+ }))
623
+ ];
624
+ if ('full' === mode) children.sort((a, b)=>this.liveChildIndex(a) - this.liveChildIndex(b));
625
+ const hiddenCases = outcomes.filter((outcome)=>!shown.includes(outcome));
626
+ if (hiddenCases.length && 'active' !== mode) {
627
+ const failed = hiddenCases.filter((outcome)=>'failed' === outcome.status).length;
628
+ children.push({
629
+ summary: `… ${hiddenCases.length} completed ${1 === hiddenCases.length ? 'case' : 'cases'}${failed ? ` · ${failed} failed` : ''}`,
630
+ failed: failed > 0
631
+ });
632
+ }
633
+ for (const [index, child] of children.entries()){
634
+ const last = index === children.length - 1;
635
+ const prefix = `${stem}${c.dim(last ? '└─' : '├─')} `;
636
+ if ('outcome' in child) {
637
+ lines.push(this.liveRow(`${prefix}${this.formatCaseResult(child.outcome, file.retries)}`, ''));
638
+ continue;
639
+ }
640
+ if ('summary' in child) {
641
+ lines.push(this.liveRow(`${prefix}${child.failed ? c.red(child.summary) : c.dim(child.summary)}`, ''));
642
+ continue;
643
+ }
644
+ const { task } = child;
645
+ const hint = task.name && task.step ? truncateTerminalLine(` · ${task.step}`, Math.floor(Math.max(0, (this.io.columns ?? 100) - 1) / 2)) : '';
646
+ const retry = task.attempt ? ` · attempt ${task.attempt + 1}${task.maxAttempts ? `/${task.maxAttempts}` : ''}` : '';
647
+ lines.push(this.liveRow(`${prefix}${terminal_reporter_oneLine(task.name ?? task.step ?? 'file lifecycle')}`, `${c.dim(hint)} ${c.yellow(formatDuration(Date.now() - task.since))}${c.yellow(retry)}`));
648
+ if (task.warning) lines.push(this.liveRow(`${stem}${last ? ' ' : `${c.dim('│')} `}${c.dim('└─')} ${c.yellow(`⚠ ${task.warning}`)}`, ''));
649
+ }
650
+ }
651
+ if (hidden.length && 'active' !== mode) {
652
+ const summary = `… ${hidden.length} completed ${1 === hidden.length ? 'file' : 'files'}${hiddenFailures ? ` · ${hiddenFailures} failed` : ''}`;
653
+ lines.push(this.liveRow(` ${c.dim('└─')} ${hiddenFailures ? c.red(summary) : c.dim(summary)}`, ''));
654
+ }
655
+ }
656
+ return lines;
657
+ }
658
+ liveChildIndex(child) {
659
+ if ('outcome' in child) return child.outcome.caseIndex;
660
+ if ('summary' in child) return Number.MAX_SAFE_INTEGER;
661
+ return child.task.caseIndex ?? ('beforeAll' === child.task.phase ? -1 : Number.MAX_SAFE_INTEGER);
662
+ }
663
+ liveRow(label, suffix) {
664
+ const columns = Math.max(0, (this.io.columns ?? 100) - 1);
665
+ const tail = truncateTerminalLine(suffix, columns);
666
+ return truncateTerminalLine(label, columns - string_width(tail)) + tail;
667
+ }
668
+ tick() {
669
+ if (this.interactive) return void this.renderProgress();
670
+ if ((this.active.size || this.projectPhases.size) && Date.now() - this.lastOutput >= 30000) this.log(`… Still running: ${[
671
+ ...[
672
+ ...this.projectPhases
673
+ ].map(([projectName, phase])=>`[${terminal_reporter_oneLine(projectName)}] · ${projectPhaseLabel(phase.phase)} (${formatDuration(Date.now() - phase.since)})`),
674
+ ...[
675
+ ...this.active.values()
676
+ ].map((task)=>`${identity(task.file.projectName, task.file.sourcePath, task.name)} · ${task.step ?? 'between steps'} (${formatDuration(Date.now() - task.since)})`)
677
+ ].join(' | ')}`);
678
+ }
679
+ formatCaseResult(outcome, documentRetries) {
680
+ const c = this.color;
681
+ const attempts = outcome.attempts ?? (outcome.run ? [
682
+ outcome.run
683
+ ] : []);
684
+ const retries = Math.max(attempts.length - 1, outcome.run?.attemptIndex ?? 0);
685
+ const duration = formatDuration(attempts.reduce((sum, attempt)=>sum + attempt.durationMs, 0));
686
+ const symbol = 'success' === outcome.status ? c.green('✓') : 'failed' === outcome.status ? c.red('×') : c.yellow('–');
687
+ const detail = 'not-run' === outcome.status ? c.dim(` · ${outcome.notRunReason ?? 'not run'}`) : ` ${c.dim(duration)}${retries ? c.yellow(` · ${'success' === outcome.status ? 'passed after retry' : 'failed after retries'} (${retries + 1} attempts)`) : documentRetries ? c.yellow(` · file retried ${documentRetries}x`) : ''}`;
688
+ return `${symbol} ${terminal_reporter_oneLine(outcome.name)}${detail}`;
689
+ }
690
+ statusSymbol(status) {
691
+ return 'failed' === status ? this.color.red('×') : 'not-run' === status ? this.color.yellow('–') : this.color.green('✓');
692
+ }
693
+ resultFileLines(file, lastFile) {
694
+ const c = this.color;
695
+ const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
696
+ const cases = [
697
+ ...file.outcomes.values()
698
+ ].sort((a, b)=>a.caseIndex - b.caseIndex);
699
+ const failed = cases.filter((item)=>'failed' === item.status).length;
700
+ const notRun = cases.filter((item)=>'not-run' === item.status).length;
701
+ const stats = `(${cases.length} cases${failed ? ` | ${failed} failed` : ''}${notRun ? ` | ${notRun} not run` : ''})`;
702
+ return [
703
+ ` ${c.dim(lastFile ? '└─' : '├─')} ${this.statusSymbol(file.status)} ${c.bold(terminal_reporter_oneLine(file.sourcePath))} ${c.dim(stats)} ${c.dim(formatDuration(file.durationMs))}`,
704
+ ...cases.map((item, caseIndex)=>`${stem}${c.dim(caseIndex === cases.length - 1 ? '└─' : '├─')} ${this.formatCaseResult(item, file.retries)}`)
705
+ ];
706
+ }
707
+ printFile(file) {
708
+ if (file.printed) return;
709
+ file.printed = true;
710
+ this.log([
711
+ '',
712
+ ` ${this.color.bold(terminal_reporter_oneLine(file.projectName))}`,
713
+ ...this.resultFileLines(file, true)
714
+ ].join('\n'));
715
+ }
716
+ printProject(projectName, files, failed) {
717
+ if (!files.length || files.every((file)=>file.printed)) return;
718
+ const c = this.color;
719
+ const status = failed || files.some((file)=>'failed' === file.status) ? 'failed' : files.every((file)=>'not-run' === file.status) ? 'not-run' : files.some((file)=>'not-run' === file.status) ? 'failed' : 'success';
720
+ const lines = [
721
+ '',
722
+ ` ${this.statusSymbol(status)} ${c.bold(terminal_reporter_oneLine(projectName))} ${c.dim(`(${files.length} ${1 === files.length ? 'file' : 'files'})`)}`
723
+ ];
724
+ for (const [index, file] of files.entries())if (!file.printed) {
725
+ file.printed = true;
726
+ lines.push(...this.resultFileLines(file, index === files.length - 1));
727
+ }
728
+ this.log(lines.join('\n'));
729
+ }
730
+ failure(source, error, context) {
731
+ this.failures.push({
732
+ source,
733
+ error,
734
+ context
735
+ });
736
+ }
737
+ completeProject(project) {
738
+ this.projectPhases.delete(project.name);
739
+ const projectFiles = new Set();
740
+ const documents = new Map(project.documents.map((document)=>[
741
+ document.documentId,
742
+ document
743
+ ]));
744
+ const resultSources = new Set([
745
+ ...project.cases.map((outcome)=>outcome.sourcePath),
746
+ ...project.documents.map((document)=>document.sourcePath),
747
+ ...project.collectionErrors.map((error)=>error.sourcePath)
748
+ ]);
749
+ for (const plan of project.plans ?? []){
750
+ const selected = plan.cases.filter((item)=>item.selected).length;
751
+ if (selected || resultSources.has(plan.sourcePath)) projectFiles.add(this.file(project.name, plan.sourcePath, selected));
752
+ }
753
+ for (const outcome of project.cases)this.file(project.name, outcome.sourcePath).outcomes.set(outcome.caseId, outcome);
754
+ for (const document of documents.values()){
755
+ const file = this.file(project.name, document.sourcePath);
756
+ file.status = document.status;
757
+ file.retries = document.attemptIndex ?? 0;
758
+ file.durationMs = project.documents.filter((item)=>item.documentId === document.documentId).reduce((sum, item)=>sum + item.durationMs, 0);
759
+ }
760
+ for (const error of project.collectionErrors)this.file(project.name, error.sourcePath).status = 'failed';
761
+ for (const file of this.files.values())if (file.projectName === project.name) {
762
+ file.attemptOutcomes.clear();
763
+ const cases = [
764
+ ...file.outcomes.values()
765
+ ];
766
+ file.status = fileStatus(file.status, cases);
767
+ projectFiles.add(file);
768
+ }
769
+ for (const [key, task] of this.active)if (task.file.projectName === project.name) this.active.delete(key);
770
+ if (this.interactive) this.printProject(project.name, [
771
+ ...projectFiles
772
+ ], 'failed' === project.status);
773
+ else for (const file of projectFiles)this.printFile(file);
774
+ }
775
+ finish(result) {
776
+ this.dispose();
777
+ this.finalMetrics = result.metrics ?? this.usage.snapshot();
778
+ if (result.startedAt) this.startedAt = Date.parse(result.startedAt);
779
+ for (const project of result.projects)this.completeProject(project);
780
+ this.total = result.summary.total;
781
+ this.totalFiles = this.files.size;
782
+ }
783
+ sectionHeading(label, kind) {
784
+ const c = this.color;
785
+ const width = Math.max(0, (this.io.columns ?? 80) - 2);
786
+ const title = truncateTerminalLine(` ${label} `, width);
787
+ const padding = Math.max(0, width - string_width(title));
788
+ const left = Math.floor(padding / 2);
789
+ const line = 'warning' === kind ? c.yellow : c.red;
790
+ const badge = 'warning' === kind ? c.bgYellow.black.bold : c.bgRed.white.bold;
791
+ this.log(`\n ${line('─'.repeat(left))}${badge(title)}${line('─'.repeat(padding - left))}\n`);
792
+ }
793
+ summary(result) {
794
+ const c = this.color;
795
+ if (this.interactive && this.warnings.size) {
796
+ this.sectionHeading(`Warnings ${this.warnings.size}`, 'warning');
797
+ for (const warning of [
798
+ ...this.warnings.values()
799
+ ].slice(0, 5))this.log(` ${c.yellow('WARN')} ${c.dim(warning.scope)}${warning.message}`);
800
+ if (this.warnings.size > 5) this.log(c.dim(` … ${this.warnings.size - 5} more warnings; see debug logs or --reporter verbose.`));
801
+ }
802
+ if (this.failures.length) {
803
+ this.sectionHeading(`Failures ${this.failures.length}`, 'failure');
804
+ for (const failure of this.failures){
805
+ if ('default' === this.mode) {
806
+ const detail = formatFailureDetails(failure.source, failure.error, failure.context);
807
+ const body = detail.body.map((line)=>{
808
+ if (line.startsWith(' Error:')) return c.red(line);
809
+ if (line.startsWith(' at ')) return c.dim(line);
810
+ return line.replace(/^ {2}(?:Step|Location|Attempt|File try|Phase|Input)\s+/, (label)=>c.dim(label));
811
+ }).join('\n');
812
+ const block = `${c.bgRed.white.bold(' FAIL ')} ${c.bold(detail.title)}\n${body}\n`;
813
+ if (this.interactive) this.log(block);
814
+ else this.io.error(`FAIL ${detail.title}\n${detail.body.join('\n')}\n`);
815
+ continue;
816
+ }
817
+ const message = formatCliError(failure.error);
818
+ this.io.error(`FAIL midscene-test: ${failure.source}:\n${message.split('\n').map((line)=>` ${line}`).join('\n')}\n`);
819
+ }
820
+ }
821
+ this.log(`\n${this.footer(result.durationMs, result).join('\n')}`);
822
+ this.log('');
823
+ this.failures = [];
824
+ }
825
+ dispose() {
826
+ if (this.timer) clearInterval(this.timer);
827
+ if (this.scheduled) clearTimeout(this.scheduled);
828
+ this.timer = void 0;
829
+ this.scheduled = void 0;
830
+ this.removeResizeListener?.();
831
+ this.removeResizeListener = void 0;
832
+ this.progressView.clear();
833
+ }
834
+ constructor(io, mode = 'default'){
835
+ terminal_reporter_define_property(this, "io", void 0);
836
+ terminal_reporter_define_property(this, "mode", void 0);
837
+ terminal_reporter_define_property(this, "total", void 0);
838
+ terminal_reporter_define_property(this, "completionOrder", void 0);
839
+ terminal_reporter_define_property(this, "totalFiles", void 0);
840
+ terminal_reporter_define_property(this, "files", void 0);
841
+ terminal_reporter_define_property(this, "warnings", void 0);
842
+ terminal_reporter_define_property(this, "latestNotice", void 0);
843
+ terminal_reporter_define_property(this, "usage", void 0);
844
+ terminal_reporter_define_property(this, "finalMetrics", void 0);
845
+ terminal_reporter_define_property(this, "attempts", void 0);
846
+ terminal_reporter_define_property(this, "active", void 0);
847
+ terminal_reporter_define_property(this, "projectPhases", void 0);
848
+ terminal_reporter_define_property(this, "failures", void 0);
849
+ terminal_reporter_define_property(this, "lastOutput", void 0);
850
+ terminal_reporter_define_property(this, "startedAt", void 0);
851
+ terminal_reporter_define_property(this, "timer", void 0);
852
+ terminal_reporter_define_property(this, "scheduled", void 0);
853
+ terminal_reporter_define_property(this, "progressView", void 0);
854
+ terminal_reporter_define_property(this, "removeResizeListener", void 0);
855
+ terminal_reporter_define_property(this, "color", void 0);
856
+ this.io = io;
857
+ this.mode = mode;
858
+ this.total = 0;
859
+ this.completionOrder = 0;
860
+ this.totalFiles = 0;
861
+ this.files = new Map();
862
+ this.warnings = new Map();
863
+ this.usage = new CliModelUsage();
864
+ this.attempts = new Map();
865
+ this.active = new Map();
866
+ this.projectPhases = new Map();
867
+ this.failures = [];
868
+ this.lastOutput = Date.now();
869
+ this.startedAt = Date.now();
870
+ this.progressView = new TerminalProgress(io);
871
+ this.color = new chalk.Instance({
872
+ level: io.isTTY && !process.env.NO_COLOR ? 1 : 0
873
+ });
874
+ }
875
+ }
876
+ const escapeData = (text)=>text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
877
+ const escapeProperty = (text)=>escapeData(text).replace(/:/g, '%3A').replace(/,/g, '%2C');
878
+ const markdown = (text)=>text.replace(/[&<>`\r\n]/g, (char)=>({
879
+ '&': '&amp;',
880
+ '<': '&lt;',
881
+ '>': '&gt;',
882
+ '`': '&#96;',
883
+ '\r': ' ',
884
+ '\n': ' '
885
+ })[char]);
886
+ const code = (text)=>{
887
+ const value = text.replace(/[\r\n]+/g, ' ');
888
+ const longest = Math.max(0, ...(value.match(/`+/g) ?? []).map((run)=>run.length));
889
+ const fence = '`'.repeat(longest + 1);
890
+ const pad = value.startsWith('`') || value.endsWith('`') ? ' ' : '';
891
+ return `${fence}${pad}${value}${pad}${fence}`;
892
+ };
893
+ function githubFailureAnnotation(source, message, file) {
894
+ if ('true' !== process.env.GITHUB_ACTIONS) return;
895
+ const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd();
896
+ const path = file ? relative(workspace, resolve(file)).split(sep).join('/') : void 0;
897
+ return `::error ${path ? `file=${escapeProperty(path)},` : ''}title=${escapeProperty(source)}::${escapeData(message)}`;
898
+ }
899
+ function writeGitHubSummary(result) {
900
+ const output = process.env.GITHUB_STEP_SUMMARY;
901
+ if ('true' !== process.env.GITHUB_ACTIONS || !output) return;
902
+ const failedCases = [
903
+ ...new Map(result.cases.map((item)=>[
904
+ item.caseId,
905
+ item
906
+ ])).values()
907
+ ].filter((item)=>'failed' === item.status);
908
+ const summary = [
909
+ '## Midscene Test',
910
+ '',
911
+ ...renderTestBadgeLines(result),
912
+ '| Passed | Failed | Not run | Passed after retry | Duration |',
913
+ '| ---: | ---: | ---: | ---: | ---: |',
914
+ `| ${result.summary.passed} | ${result.summary.failed} | ${result.summary.notRun} | ${result.summary.passedAfterRetry} | ${formatDuration(result.durationMs)} |`,
915
+ '',
916
+ 'Passed after retry is included in Passed.',
917
+ '',
918
+ ...failedCases.map((item)=>`- Failed: **${markdown(item.name)}** — ${markdown(item.projectName)}/${code(item.sourcePath)}`),
919
+ ...result.summary.collectionErrors ? [
920
+ `- Collection errors: ${result.summary.collectionErrors}`
921
+ ] : [],
922
+ ...result.summary.documentFailures ? [
923
+ `- Failed files: ${result.summary.documentFailures}`
924
+ ] : [],
925
+ ...result.summary.projectFailures ? [
926
+ `- Failed projects: ${result.summary.projectFailures}`
927
+ ] : [],
928
+ '',
929
+ ...result.reportPath ? [
930
+ `Report artifact entry: ${code(result.reportPath)}`
931
+ ] : [],
932
+ `Structured results: ${code(result.summaryPath)}`,
933
+ ''
934
+ ].join('\n');
935
+ appendFileSync(output, `${summary}\n`);
936
+ }
937
+ const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
938
+ const sortNodesForSpec = (nodes)=>{
939
+ const priority = (name)=>'aiAct' === name ? 0 : 'aiAssert' === name ? 1 : 2;
940
+ return [
941
+ ...nodes
942
+ ].sort((left, right)=>priority(left.name) - priority(right.name) || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
943
+ };
944
+ const escapeTableCell = (value)=>value.trim().replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r\n|\r|\n/g, '<br>');
945
+ const renderNodeOverview = (nodes)=>{
946
+ const heading = '## Available Nodes';
947
+ if (0 === nodes.length) return `${heading}\n\nNo nodes are registered by the current Test Project.`;
948
+ const rows = nodes.map((node)=>`| ${escapeTableCell(node.name)} | ${escapeTableCell(node.description || "Description not declared.")} |`);
949
+ return `${heading}\n\nThis table lists all available Nodes. See [Node Details](#node-details) below for each Node's usage and input schema.\n\n${[
950
+ "| Node | Description |",
951
+ '| --- | --- |',
952
+ ...rows
953
+ ].join('\n')}`;
954
+ };
955
+ const renderWorkflowLocations = (location)=>{
956
+ const sections = [
957
+ '## Where to use these Nodes',
958
+ 'Use these Nodes in Workflow Documents (`.yaml` or `.yml` files) selected by an Execution Project. Case file patterns and the config file path below are relative to this spec file. Case file patterns use glob syntax. A custom `--config` path does not change where case files are selected.',
959
+ 'Projects created by `midscene-test create` select `cases/**/*.{yaml,yml}` by default, so you can write test cases in `cases/example.yaml` or subdirectories of `cases/`. If an Execution Project has no `files` configuration, Midscene Test searches `**/*.{yaml,yml}` recursively instead. Check `projects[].files` in your configuration before choosing a file location.'
960
+ ];
961
+ if (location) {
962
+ sections.push(`**Config file:** ${location.configPath ? `\`${location.configPath}\`` : 'No config file loaded.'}`);
963
+ for (const project of location.projects){
964
+ const patterns = project.files.include.map((pattern)=>`\`${pattern}\``).join(', ');
965
+ const projectLabel = location.projects.length > 1 || 'default' !== project.name ? ` (Execution Project: ${project.name})` : '';
966
+ const excludes = project.files.exclude?.length ? `; excludes: ${project.files.exclude.map((pattern)=>`\`${pattern}\``).join(', ')}` : '';
967
+ sections.push(`**Case files:** ${patterns}${projectLabel}${excludes}`);
968
+ }
969
+ }
970
+ sections.push('Files matching `files.exclude` are not selected. Midscene Test always ignores `.git/`, `.midscene/`, `midscene_run/`, and `node_modules/` directories, including nested ones.', 'Call a Node in `cases[].steps`, or in the Workflow Document lifecycle steps `beforeAll`, `beforeEach`, `afterEach`, and `afterAll`. Root Nodes are shared defaults; `projects[].nodes` adds local Nodes and overrides same-named root Nodes only within that Execution Project. Each Execution Project supplies its own runtime environment.');
971
+ return sections.join('\n\n');
972
+ };
973
+ const WORKFLOW_STEP_CONTRACT = `## Workflow Step Contract
974
+
975
+ Each step uses a node name as its key. The \`$\` object contains engine metadata and is not part of the node input.
976
+
977
+ \`\`\`yaml
978
+ cases:
979
+ - name: Example case
980
+ steps:
981
+ - node.name:
982
+ prompt: Describe the task.
983
+ $:
984
+ timeout: 30000
985
+ continue-on-error: false
986
+ \`\`\`
987
+
988
+ A Node may map a string step value to one declared input field. Each Node section states whether it supports this shorthand and names the target field.
989
+
990
+ Each Node's returned \`data\` is saved on that Step's result. Results are not named or automatically passed to later Steps. A failed Case does not stop later Cases by default; \`test.bail\` controls early stopping across the run.`;
991
+ const sortJsonValue = (value)=>{
992
+ if (Array.isArray(value)) return value.map(sortJsonValue);
993
+ if ('object' != typeof value || null === value) return value;
994
+ return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0).map(([key, child])=>[
995
+ key,
996
+ sortJsonValue(child)
997
+ ]));
998
+ };
999
+ const assertSelfContainedRefs = (value, node)=>{
1000
+ if (Array.isArray(value)) {
1001
+ for (const child of value)assertSelfContainedRefs(child, node);
1002
+ return;
1003
+ }
1004
+ if ('object' != typeof value || null === value) return;
1005
+ for (const [key, child] of Object.entries(value)){
1006
+ if ('$ref' === key && 'string' == typeof child && !child.startsWith('#')) throw new Error(`Cannot describe node "${node}": inputSchema generated an external $ref.`);
1007
+ assertSelfContainedRefs(child, node);
1008
+ }
1009
+ };
1010
+ const toJsonSchema = (node)=>{
1011
+ try {
1012
+ const schema = z.toJSONSchema(node.inputSchema, {
1013
+ target: 'draft-2020-12',
1014
+ io: 'input',
1015
+ unrepresentable: 'throw',
1016
+ cycles: 'ref',
1017
+ reused: 'ref'
1018
+ });
1019
+ assertSelfContainedRefs(schema, node.name);
1020
+ return sortJsonValue(schema);
1021
+ } catch (error) {
1022
+ if (error instanceof Error && error.message.startsWith(`Cannot describe node "${node.name}":`)) throw error;
1023
+ throw new Error(`Cannot describe node "${node.name}": inputSchema contains a Zod type that cannot be represented as JSON Schema.`, {
1024
+ cause: error
1025
+ });
1026
+ }
1027
+ };
1028
+ const renderNode = (node, warnings)=>{
1029
+ const sections = [
1030
+ `### \`${node.name}\``
1031
+ ];
1032
+ if (node.title) sections.push(`**Title:** ${node.title.trim()}`);
1033
+ if (node.description) sections.push(node.description.trim());
1034
+ else {
1035
+ warnings.push(`node "${node.name}" has no description`);
1036
+ sections.push("> Description not declared. Do not infer this node's behavior.");
1037
+ }
1038
+ if (false === node.stringInputKey || void 0 === node.stringInputKey) sections.push('**String shorthand:** Not supported by this Node.');
1039
+ else sections.push(`**String shorthand:** Maps to \`{ ${node.stringInputKey}: value }\`.`);
1040
+ sections.push('#### Input Schema');
1041
+ if (node.inputSchema) sections.push(`\`\`\`json\n${JSON.stringify(toJsonSchema(node), null, 2)}\n\`\`\``);
1042
+ else {
1043
+ warnings.push(`node "${node.name}" has no inputSchema`);
1044
+ sections.push('> Input schema not declared. Do not invent input fields for this node.');
1045
+ }
1046
+ return sections.join('\n\n');
1047
+ };
1048
+ const renderNodeSpec = (nodes, location)=>{
1049
+ const warnings = [];
1050
+ const sortedNodes = sortNodesForSpec(nodes);
1051
+ const sections = [
1052
+ GENERATED_COMMENT,
1053
+ '# Midscene Test Node Spec',
1054
+ 'This document describes the Nodes available in YAML test files (`.yaml` or `.yml`) for the Midscene Test framework (`@midscene/test`). It lists the Nodes registered by the current Test Project, their inputs, and how to call them in test steps and lifecycle hooks.',
1055
+ "Learn about the framework and YAML test authoring:\n\n- [Midscene Test overview](https://midscenejs.com/midscene-test/overview): framework concepts and the role of Nodes.\n- [Create and use test projects](https://midscenejs.com/midscene-test/use): write YAML cases, run tests, and inspect results.\n- [Configure test projects](https://midscenejs.com/midscene-test/configuration): register Nodes, select case files, and manage runtime resources.",
1056
+ renderNodeOverview(sortedNodes),
1057
+ renderWorkflowLocations(location),
1058
+ WORKFLOW_STEP_CONTRACT
1059
+ ];
1060
+ if (sortedNodes.length > 0) {
1061
+ sections.push('## Node Details');
1062
+ for (const node of sortedNodes)sections.push(renderNode(node, warnings));
1063
+ }
1064
+ return {
1065
+ markdown: `${sections.join('\n\n')}\n`,
1066
+ warnings
1067
+ };
1068
+ };
1069
+ function captureTerminalOutput(output, streams = [
1070
+ process.stdout,
1071
+ process.stderr
1072
+ ]) {
1073
+ const cleanups = [];
1074
+ for (const stream of streams){
1075
+ const original = stream.write;
1076
+ const forward = original.bind(stream);
1077
+ const intercepted = (chunk, encoding, callback)=>{
1078
+ const done = 'function' == typeof encoding ? encoding : callback;
1079
+ const message = 'string' == typeof chunk ? chunk : Buffer.from(chunk).toString();
1080
+ let accepted = true;
1081
+ output(message, (text)=>{
1082
+ accepted = forward(text, done);
1083
+ });
1084
+ return accepted;
1085
+ };
1086
+ stream.write = intercepted;
1087
+ cleanups.push(()=>{
1088
+ if (stream.write === intercepted) stream.write = original;
1089
+ });
1090
+ }
1091
+ return ()=>{
1092
+ for (const cleanup of cleanups)cleanup();
1093
+ };
1094
+ }
1095
+ const parseTestCliArgs = (args, cwd = process.cwd())=>{
1096
+ const command = 'nodes' === args[0] ? args[0] : void 0;
1097
+ const commandOffset = command ? 1 : 0;
1098
+ let inputPath;
1099
+ let configPath;
1100
+ let resultDir;
1101
+ const projectNames = [];
1102
+ let reporter;
1103
+ for(let index = commandOffset; index < args.length; index += 1){
1104
+ const token = args[index];
1105
+ const equal = token.startsWith('-') ? token.indexOf('=') : -1;
1106
+ const arg = equal < 0 ? token : token.slice(0, equal);
1107
+ const inlineValue = equal < 0 ? void 0 : token.slice(equal + 1);
1108
+ if (!arg.startsWith('-')) {
1109
+ if (inputPath) throw new Error('Only one test file or directory is allowed.');
1110
+ inputPath = resolve(cwd, arg);
1111
+ continue;
1112
+ }
1113
+ if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--reporter' === arg) {
1114
+ const value = inlineValue ?? args[index + 1];
1115
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
1116
+ if ('--config' === arg) configPath = value;
1117
+ else if ('--result-dir' === arg) resultDir = resolve(cwd, value);
1118
+ else if ('--project' === arg) projectNames.push(value);
1119
+ else {
1120
+ if ('default' !== value && 'verbose' !== value) throw new Error(`Unknown reporter: ${value}. Expected default or verbose.`);
1121
+ reporter = value;
1122
+ }
1123
+ if (void 0 === inlineValue) index += 1;
1124
+ } else throw new Error(`Unknown option: ${token}`);
1125
+ }
1126
+ if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
1127
+ if ('nodes' === command && reporter) throw new Error('--reporter is not supported by nodes.');
1128
+ if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
1129
+ return {
1130
+ ...command ? {
1131
+ command
1132
+ } : {},
1133
+ cwd,
1134
+ ...command ? {
1135
+ projectRoot: inputPath
1136
+ } : {
1137
+ testPath: inputPath
1138
+ },
1139
+ configPath,
1140
+ resultDir,
1141
+ ...projectNames.length > 0 ? {
1142
+ projectNames
1143
+ } : {},
1144
+ ...reporter ? {
1145
+ reporter
1146
+ } : {}
1147
+ };
1148
+ };
1149
+ const defaultCliIO = {
1150
+ log: console.log,
1151
+ error: console.error,
1152
+ write: process.stdout.write.bind(process.stdout),
1153
+ isTTY: Boolean(process.stdout.isTTY && !process.env.CI),
1154
+ get columns () {
1155
+ return process.stdout.columns;
1156
+ },
1157
+ get rows () {
1158
+ return process.stdout.rows;
1159
+ },
1160
+ onResize (listener) {
1161
+ process.stdout.on('resize', listener);
1162
+ return ()=>process.stdout.off('resize', listener);
1163
+ }
1164
+ };
1165
+ const testCliHelp = `Midscene Test: run native cases/steps Test projects.
1166
+
1167
+ Usage:
1168
+ midscene-test [file.yaml | directory] [options]
1169
+ midscene-test --config <midscene.config.ts> [options]
1170
+ midscene-test nodes [directory] [--config midscene.config.ts]
1171
+ midscene-test create --help
1172
+ midscene-test model verify Check model connectivity and Midscene compatibility
1173
+
1174
+ Options:
1175
+ --config <path> Native TypeScript or JavaScript Test config
1176
+ --project <name> Select a native execution Project (repeatable)
1177
+ --result-dir <path> Store Test result files in this directory
1178
+ --reporter <default|verbose> Concise Case results or full step diagnostics
1179
+ --help, -h Show this help
1180
+ --version Show the package version
1181
+
1182
+ With a config, run paths filter project files. Without one, they select standalone inputs.
1183
+ Run paths never change config discovery or the artifact root.
1184
+ For test runs, config discovery checks cwd only; --config is relative to cwd.
1185
+ The selected config directory becomes the test root.
1186
+ File include/exclude and tag rules still apply to explicit paths.
1187
+
1188
+ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
1189
+ `;
1190
+ const assertDirectory = (path, label)=>{
1191
+ if (!existsSync(path) || !statSync(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
1192
+ };
1193
+ const runNodesCommand = async (options, io)=>{
1194
+ const cwd = resolve(options.cwd);
1195
+ assertDirectory(cwd, 'Test working directory');
1196
+ const projectRoot = options.projectRoot ? resolve(cwd, options.projectRoot) : void 0;
1197
+ if (projectRoot) assertDirectory(projectRoot, 'Test project directory');
1198
+ const configSearchRoot = projectRoot ?? cwd;
1199
+ const configPath = options.configPath ? resolve(configSearchRoot, options.configPath) : discoverTestConfig(configSearchRoot);
1200
+ if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test nodes --config only accepts a native TypeScript or JavaScript project config: ${configPath}.`);
1201
+ if (options.configPath && (!configPath || !existsSync(configPath))) throw new Error(`Midscene config does not exist: ${configPath}`);
1202
+ const project = await loadTestProject(configPath);
1203
+ const projectName = options.projectNames?.[0];
1204
+ const selectedProjects = projectName ? project.projects.filter((candidate)=>candidate.name === projectName) : project.projects;
1205
+ if (0 === selectedProjects.length) throw new Error(`Unknown Midscene project: ${projectName}`);
1206
+ const filenames = new Map();
1207
+ const owners = new Map();
1208
+ for (const executionProject of project.projects){
1209
+ const safeName = executionProject.name.normalize('NFC').replace(/[<>:"/\\|?*\u0000-\u001f]/g, '-').replace(/[. ]+$/g, '-');
1210
+ const filename = project.hasExplicitProjects ? `midscene-node-spec.${safeName}.md` : 'midscene-node-spec.md';
1211
+ const key = filename.toLowerCase();
1212
+ const owner = owners.get(key);
1213
+ if (void 0 !== owner) throw new Error(`Projects "${owner}" and "${executionProject.name}" generate the same Node Spec filename: ${filename}`);
1214
+ owners.set(key, executionProject.name);
1215
+ filenames.set(executionProject.name, filename);
1216
+ }
1217
+ const specs = selectedProjects.map((executionProject)=>{
1218
+ const nodes = sortNodesForSpec(executionProject.nodes.definitions());
1219
+ const document = renderNodeSpec(nodes, {
1220
+ configPath: configPath ? relative(configSearchRoot, configPath).split(sep).join('/') : void 0,
1221
+ projects: [
1222
+ {
1223
+ name: executionProject.name,
1224
+ files: executionProject.files ?? DEFAULT_TEST_FILE_SELECTION
1225
+ }
1226
+ ]
1227
+ });
1228
+ return {
1229
+ executionProject,
1230
+ nodes,
1231
+ document
1232
+ };
1233
+ });
1234
+ for (const { executionProject, nodes, document } of specs){
1235
+ for (const warning of document.warnings)io.error(`midscene-test nodes: ${warning}`);
1236
+ const specPath = resolve(configSearchRoot, filenames.get(executionProject.name));
1237
+ writeFileSync(specPath, document.markdown);
1238
+ if (project.hasExplicitProjects || projectName) io.log(`Execution Project: ${executionProject.name}`);
1239
+ io.log(`Registered Nodes (${nodes.length}):`);
1240
+ if (0 === nodes.length) io.log('No nodes are registered by the current Test Project.');
1241
+ for (const node of nodes)io.log(`- ${node.name}: ${node.description?.trim() || "Description not declared."}`);
1242
+ io.log(`\nNode Spec generated: ${specPath}`);
1243
+ }
1244
+ };
1245
+ const reportTestProjectResult = (result, io, reporter, projectRoot, infrastructureErrors = result.errors ?? [])=>{
1246
+ reporter.finish(result);
1247
+ const reportedErrors = new Set();
1248
+ const reportError = (source, error, sourcePath, context)=>{
1249
+ if (error instanceof Error && reportedErrors.has(error)) return;
1250
+ reporter.failure(source, error, {
1251
+ projectRoot,
1252
+ ...context
1253
+ });
1254
+ const annotation = githubFailureAnnotation(source, formatConciseError(error), sourcePath ? resolve(projectRoot, sourcePath) : void 0);
1255
+ if (annotation) io.log(annotation);
1256
+ const pending = [
1257
+ error
1258
+ ];
1259
+ while(pending.length > 0){
1260
+ const current = pending.pop();
1261
+ if (!(current instanceof Error) || reportedErrors.has(current)) continue;
1262
+ reportedErrors.add(current);
1263
+ pending.push(current.cause);
1264
+ if (current instanceof AggregateError) pending.push(...current.errors);
1265
+ }
1266
+ };
1267
+ for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error, failure.sourcePath, {
1268
+ projectName: failure.projectName,
1269
+ sourcePath: failure.sourcePath,
1270
+ phase: 'collection'
1271
+ });
1272
+ const finalDocuments = new Map(result.documents.map((document)=>[
1273
+ document.documentId,
1274
+ document
1275
+ ]));
1276
+ const documentContext = (projectName, documentId, sourcePath)=>{
1277
+ const project = result.projects.find((project)=>project.name === projectName);
1278
+ const document = finalDocuments.get(documentId);
1279
+ const plan = document?.plan ?? project?.plans?.find((plan)=>plan.documentId === documentId);
1280
+ const fileIndex = document?.attemptIndex ?? 0;
1281
+ return {
1282
+ plan,
1283
+ projectName,
1284
+ sourcePath,
1285
+ ...plan?.retry.scope === 'document' ? {
1286
+ fileAttempt: {
1287
+ index: fileIndex,
1288
+ total: Math.max(fileIndex + 1, plan.retry.count + 1)
1289
+ }
1290
+ } : {},
1291
+ caseAttempts: plan?.retry.scope === 'document' ? 1 : (plan?.retry.count ?? project?.retry ?? 0) + 1
1292
+ };
1293
+ };
1294
+ const finalCases = new Map(result.cases.map((outcome)=>[
1295
+ outcome.caseId,
1296
+ outcome
1297
+ ]));
1298
+ for (const outcome of finalCases.values()){
1299
+ if ('failed' !== outcome.status || !outcome.run) continue;
1300
+ const run = outcome.run;
1301
+ const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
1302
+ const { caseAttempts, plan, ...scope } = documentContext(run.projectName, outcome.documentId, run.sourcePath);
1303
+ const context = {
1304
+ ...scope,
1305
+ caseName: run.name,
1306
+ attempt: {
1307
+ index: run.attemptIndex,
1308
+ total: Math.max(caseAttempts, run.attemptIndex + 1)
1309
+ }
1310
+ };
1311
+ const casePlan = plan?.cases.find((item)=>item.caseId === outcome.caseId);
1312
+ for (const [phase, steps] of [
1313
+ [
1314
+ 'beforeEach',
1315
+ run.beforeEach
1316
+ ],
1317
+ [
1318
+ 'steps',
1319
+ run.steps
1320
+ ],
1321
+ [
1322
+ 'afterEach',
1323
+ run.afterEach
1324
+ ]
1325
+ ])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}${run.attemptIndex ? ` (failed after ${run.attemptIndex + 1} attempts)` : ''}`, step.error, run.sourcePath, {
1326
+ ...context,
1327
+ step,
1328
+ stepCount: casePlan?.[phase].length
1329
+ });
1330
+ for (const [phase, errors] of [
1331
+ [
1332
+ 'execution',
1333
+ run.executionErrors
1334
+ ],
1335
+ [
1336
+ 'teardown',
1337
+ run.teardownErrors
1338
+ ]
1339
+ ])for (const error of errors ?? [])reportError(source, error, run.sourcePath, {
1340
+ ...context,
1341
+ phase
1342
+ });
1343
+ }
1344
+ for (const document of finalDocuments.values()){
1345
+ if ('failed' !== document.status) continue;
1346
+ const source = `${document.projectName}/${document.sourcePath}`;
1347
+ const { caseAttempts: _, plan, ...context } = documentContext(document.projectName, document.documentId, document.sourcePath);
1348
+ for (const [phase, steps] of [
1349
+ [
1350
+ 'beforeAll',
1351
+ document.beforeAll
1352
+ ],
1353
+ [
1354
+ 'afterAll',
1355
+ document.afterAll
1356
+ ]
1357
+ ])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error, document.sourcePath, {
1358
+ ...context,
1359
+ step,
1360
+ stepCount: plan?.[phase].length
1361
+ });
1362
+ for (const [phase, errors] of [
1363
+ [
1364
+ 'execution',
1365
+ document.executionErrors
1366
+ ],
1367
+ [
1368
+ 'teardown',
1369
+ document.teardownErrors
1370
+ ]
1371
+ ])for (const error of errors ?? [])reportError(source, error, document.sourcePath, {
1372
+ ...context,
1373
+ phase
1374
+ });
1375
+ for (const failure of document.hostErrors ?? [])reportError(`${source} / host ${failure.phase}`, failure.error, document.sourcePath, {
1376
+ ...context,
1377
+ phase: `host ${failure.phase}`
1378
+ });
1379
+ }
1380
+ for (const project of result.projects){
1381
+ if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError, void 0, {
1382
+ projectName: project.name,
1383
+ phase: 'setup'
1384
+ });
1385
+ for (const error of project.lifecycle?.teardownErrors ?? [])reportError(`${project.name} / teardown`, error, void 0, {
1386
+ projectName: project.name,
1387
+ phase: 'teardown'
1388
+ });
1389
+ }
1390
+ for (const error of infrastructureErrors)reportError('infrastructure', error);
1391
+ reporter.summary(result);
1392
+ if ('verbose' === reporter.mode) io.log(`Results: ${result.resultDir}`);
1393
+ io.log(`Summary: ${result.summaryPath}`);
1394
+ if (result.markdownPath && 'verbose' === reporter.mode) io.log(`Markdown: ${result.markdownPath}`);
1395
+ if (result.reportPath) io.log(`Report: ${result.reportPath}`);
1396
+ return result.exitCode;
1397
+ };
1398
+ async function runTestCli(args, io = defaultCliIO, onResult) {
1399
+ try {
1400
+ if ('model' === args[0]) {
1401
+ const { runModelCommand } = await import("./183.mjs").then((mod)=>({
1402
+ runModelCommand: mod.runModelCommand
1403
+ }));
1404
+ return runModelCommand(args, void 0, {
1405
+ stdout: io.log,
1406
+ stderr: io.error
1407
+ });
1408
+ }
1409
+ if ('create' === args[0]) {
1410
+ const { runCreateCommand } = await import("./1~112.mjs").then((mod)=>({
1411
+ runCreateCommand: mod.runCreateCommand
1412
+ }));
1413
+ await runCreateCommand(args.slice(1), io);
1414
+ return 0;
1415
+ }
1416
+ if (args.includes('--help') || args.includes('-h')) {
1417
+ io.log(testCliHelp);
1418
+ return 0;
1419
+ }
1420
+ if (args.includes('--version')) {
1421
+ io.log(package_namespaceObject.rE);
1422
+ return 0;
1423
+ }
1424
+ const options = parseTestCliArgs(args);
1425
+ if ('nodes' === options.command) {
1426
+ await runNodesCommand(options, io);
1427
+ return 0;
1428
+ }
1429
+ const reporter = new TerminalReporter(io, options.reporter);
1430
+ const restoreOutput = io === defaultCliIO && io.isTTY && 'verbose' !== options.reporter ? captureTerminalOutput((message, write)=>reporter.output(message, write)) : void 0;
1431
+ try {
1432
+ return await runWithConsoleLogHandler((entry)=>reporter.console(entry), async ()=>{
1433
+ const plan = await prepareTestRun({
1434
+ ...options,
1435
+ onProgress: (message)=>reporter.progress(message),
1436
+ onEvent: (event)=>reporter.event(event)
1437
+ });
1438
+ const projectRoot = realpathSync(plan.projectRoot);
1439
+ let result;
1440
+ try {
1441
+ result = await runPreparedTestProject(plan);
1442
+ } catch (error) {
1443
+ if (!(error instanceof WorkflowExecutionFailure)) throw error;
1444
+ reportTestProjectResult(error.result, io, reporter, projectRoot, error.errors);
1445
+ onResult?.(error.result);
1446
+ return 1;
1447
+ }
1448
+ const exitCode = reportTestProjectResult(result, io, reporter, projectRoot);
1449
+ onResult?.(result);
1450
+ return exitCode;
1451
+ });
1452
+ } finally{
1453
+ try {
1454
+ reporter.dispose();
1455
+ } finally{
1456
+ restoreOutput?.();
1457
+ }
1458
+ }
1459
+ } catch (error) {
1460
+ io.error(formatCliError(error));
1461
+ return 1;
1462
+ }
1463
+ }
1464
+ runTestCli(process.argv.slice(2), void 0, writeGitHubSummary).then((exitCode)=>{
1465
+ process.exitCode = exitCode;
1466
+ });
1467
+ export { package_namespaceObject };
1468
+
1469
+ //# sourceMappingURL=164.mjs.map