@midscene/test 1.16.1 → 1.16.5
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/dist/es/{1~77.mjs → 1~731.mjs} +9 -9
- package/dist/es/1~731.mjs.map +1 -0
- package/dist/es/cli/index.mjs +342 -83
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +45 -1
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +1532 -134
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +340 -81
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +45 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/test-cli.js +1523 -131
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/templates/test/android/.env.example +1 -1
- package/dist/templates/test/android/README.md +1 -1
- package/dist/templates/test/android/README.zh.md +1 -1
- package/dist/templates/test/computer/.env.example +1 -1
- package/dist/templates/test/computer/README.md +1 -1
- package/dist/templates/test/computer/README.zh.md +1 -1
- package/dist/templates/test/harmony/.env.example +1 -1
- package/dist/templates/test/harmony/README.md +1 -1
- package/dist/templates/test/harmony/README.zh.md +1 -1
- package/dist/templates/test/ios/.env.example +1 -1
- package/dist/templates/test/ios/README.md +1 -1
- package/dist/templates/test/ios/README.zh.md +1 -1
- package/dist/templates/test/web/.env.example +1 -1
- package/dist/templates/test/web/README.md +1 -1
- package/dist/templates/test/web/README.zh.md +1 -1
- package/dist/types/cli/index.d.ts +75 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +14 -13
- package/dist/es/1~77.mjs.map +0 -1
package/dist/es/test-cli.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import * as __rspack_external__midscene_shared_env_0c37bb76 from "@midscene/shared/env";
|
|
2
|
+
import * as __rspack_external_chalk from "chalk";
|
|
1
3
|
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
2
4
|
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
3
|
-
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, getPendingScopeOperations, getResourceCleanupCompletion, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText, trackResourceOperation, waitForScopeOperations } from "@midscene/core/internal/test-runner";
|
|
4
|
-
import {
|
|
5
|
+
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, getPendingScopeOperations, getResourceCleanupCompletion, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText, sanitizeReportValue, sumTestRunReportMetrics, trackResourceOperation, waitForScopeOperations } from "@midscene/core/internal/test-runner";
|
|
6
|
+
import { getDebug, runWithConsoleLogHandler, runWithConsoleLogScope } from "@midscene/shared/logger";
|
|
7
|
+
import { inspect, stripVTControlCharacters } from "node:util";
|
|
8
|
+
import string_width from "string-width";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
10
|
import { z } from "zod/v4";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
11
|
import { require as api_require } from "tsx/cjs/api";
|
|
8
12
|
import { globSync } from "tinyglobby";
|
|
9
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -11,8 +15,13 @@ import { JSON_SCHEMA, load } from "js-yaml";
|
|
|
11
15
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
16
|
import { setMaxListeners } from "node:events";
|
|
13
17
|
import { TestRunReportAssembler, calculateTestRunHealth } from "@midscene/core/report";
|
|
14
|
-
import { getDebug } from "@midscene/shared/logger";
|
|
15
18
|
var __webpack_modules__ = {
|
|
19
|
+
"@midscene/shared/env" (module) {
|
|
20
|
+
module.exports = __rspack_external__midscene_shared_env_0c37bb76;
|
|
21
|
+
},
|
|
22
|
+
chalk (module) {
|
|
23
|
+
module.exports = __rspack_external_chalk;
|
|
24
|
+
},
|
|
16
25
|
"node:fs" (module) {
|
|
17
26
|
module.exports = __rspack_external_node_fs_5ea92f0c;
|
|
18
27
|
},
|
|
@@ -21,7 +30,7 @@ var __webpack_modules__ = {
|
|
|
21
30
|
},
|
|
22
31
|
"./package.json" (module) {
|
|
23
32
|
module.exports = {
|
|
24
|
-
rE: "1.16.
|
|
33
|
+
rE: "1.16.5"
|
|
25
34
|
};
|
|
26
35
|
}
|
|
27
36
|
};
|
|
@@ -102,6 +111,1017 @@ const formatCliError = (error)=>'string' == typeof error ? error : inspect(error
|
|
|
102
111
|
depth: null,
|
|
103
112
|
customInspect: false
|
|
104
113
|
});
|
|
114
|
+
function formatConciseError(error) {
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const messages = [];
|
|
117
|
+
let current = error;
|
|
118
|
+
while(current instanceof Error && !seen.has(current)){
|
|
119
|
+
seen.add(current);
|
|
120
|
+
if ('NODE_EXECUTION_ERROR' === current.code && void 0 !== current.cause) {
|
|
121
|
+
current = current.cause;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (current.message && !messages.includes(current.message)) messages.push(current.message);
|
|
125
|
+
if (current instanceof AggregateError) {
|
|
126
|
+
for (const nested of current.errors)if (!(nested === current || seen.has(nested))) {
|
|
127
|
+
seen.add(nested);
|
|
128
|
+
messages.push(nested instanceof Error ? nested.message : String(nested));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
current = current.cause;
|
|
132
|
+
}
|
|
133
|
+
if (!messages.length) return current instanceof Error ? current.message : formatCliError(current);
|
|
134
|
+
return messages.join('\nCaused by: ');
|
|
135
|
+
}
|
|
136
|
+
var external_chalk_ = __webpack_require__("chalk");
|
|
137
|
+
const oneLine = (value)=>stripVTControlCharacters(value).replace(/\s+/g, ' ').trim();
|
|
138
|
+
const shorten = (value, length)=>{
|
|
139
|
+
const text = oneLine(value);
|
|
140
|
+
return text.length <= length ? text : `${text.slice(0, length - 1)}…`;
|
|
141
|
+
};
|
|
142
|
+
function firstBusinessFrame(error, projectRoot) {
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
const frames = [];
|
|
145
|
+
const visit = (value, depth = 0)=>{
|
|
146
|
+
if (!value || 'object' != typeof value || seen.has(value) || depth > 12) return;
|
|
147
|
+
seen.add(value);
|
|
148
|
+
const detail = value;
|
|
149
|
+
visit(detail.cause, depth + 1);
|
|
150
|
+
if (Array.isArray(detail.errors)) for (const child of detail.errors.slice(0, 10))visit(child, depth + 1);
|
|
151
|
+
if ('string' != typeof detail.stack) return;
|
|
152
|
+
for (const line of detail.stack.split('\n')){
|
|
153
|
+
if (!line.trim().startsWith('at ')) continue;
|
|
154
|
+
let location = line.trim().slice(3).replace(/^async /, '');
|
|
155
|
+
if (location.endsWith(')')) location = location.slice(location.lastIndexOf('(') + 1, -1);
|
|
156
|
+
const match = location.match(/^(.*):(\d+):(\d+)$/);
|
|
157
|
+
if (!match) continue;
|
|
158
|
+
let file = match[1];
|
|
159
|
+
if (file.startsWith('file://')) try {
|
|
160
|
+
file = fileURLToPath(file);
|
|
161
|
+
} catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const normalized = file.replace(/\\/g, '/');
|
|
165
|
+
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]}`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
visit(error);
|
|
169
|
+
const chosen = frames[0];
|
|
170
|
+
if (!chosen || !projectRoot) return chosen;
|
|
171
|
+
const match = chosen.match(/^(.*):(\d+):(\d+)$/);
|
|
172
|
+
const file = match[1];
|
|
173
|
+
const paths = /^[A-Za-z]:[\\/]/.test(file) ? external_node_path_.win32 : {
|
|
174
|
+
isAbsolute: external_node_path_.isAbsolute,
|
|
175
|
+
relative: external_node_path_.relative
|
|
176
|
+
};
|
|
177
|
+
const display = paths.isAbsolute(file) ? paths.relative(projectRoot, file) : file;
|
|
178
|
+
return `${display.replace(/\\/g, '/')}:${match[2]}:${match[3]}`;
|
|
179
|
+
}
|
|
180
|
+
function formatFailureInput(input) {
|
|
181
|
+
if (void 0 === input) return [];
|
|
182
|
+
const value = sanitizeReportValue(input).value;
|
|
183
|
+
const render = (value)=>shorten(sanitizeReportError({
|
|
184
|
+
message: 'string' == typeof value ? value : inspect(value, {
|
|
185
|
+
colors: false,
|
|
186
|
+
depth: 2,
|
|
187
|
+
maxArrayLength: 4,
|
|
188
|
+
maxStringLength: 180,
|
|
189
|
+
breakLength: 1 / 0,
|
|
190
|
+
customInspect: false,
|
|
191
|
+
getters: false
|
|
192
|
+
})
|
|
193
|
+
}).message, 160);
|
|
194
|
+
if (value && 'object' == typeof value && !Array.isArray(value)) {
|
|
195
|
+
const entries = Object.entries(value);
|
|
196
|
+
const rows = entries.slice(0, 4).map(([key, value])=>`${shorten(key, 48)}: ${render(value)}`);
|
|
197
|
+
if (entries.length > 4) rows.push(`… ${entries.length - 4} more fields (see Summary)`);
|
|
198
|
+
return rows;
|
|
199
|
+
}
|
|
200
|
+
return [
|
|
201
|
+
render(value)
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
function formatFailureDetails(source, error, context = {}) {
|
|
205
|
+
const title = context.projectName ? `[${oneLine(context.projectName)}] ${oneLine(context.caseName ?? context.sourcePath ?? context.phase ?? source)}` : oneLine(source);
|
|
206
|
+
const body = [];
|
|
207
|
+
const field = (label, value)=>body.push(` ${label.padEnd(9)}${value}`);
|
|
208
|
+
const step = context.step;
|
|
209
|
+
if (step) {
|
|
210
|
+
const phase = 'steps' === step.phase ? '' : `${step.phase} `;
|
|
211
|
+
const index = step.stepIndex + 1;
|
|
212
|
+
const position = void 0 === context.stepCount ? String(index) : `${index}/${Math.max(index, context.stepCount)}`;
|
|
213
|
+
field('Step', `${phase}${position} · ${oneLine(step.node)}`);
|
|
214
|
+
} else if (context.phase) field('Phase', oneLine(context.phase));
|
|
215
|
+
if (context.sourcePath) {
|
|
216
|
+
const location = step?.meta.sourceLocation;
|
|
217
|
+
field('Location', `${oneLine(context.sourcePath)}${location ? `:${location.line}` : ''}`);
|
|
218
|
+
}
|
|
219
|
+
for (const [label, attempt] of [
|
|
220
|
+
[
|
|
221
|
+
'Attempt',
|
|
222
|
+
context.attempt
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
'File try',
|
|
226
|
+
context.fileAttempt
|
|
227
|
+
]
|
|
228
|
+
])if (attempt && (attempt.total > 1 || attempt.index > 0)) field(label, `${attempt.index + 1}/${attempt.total}`);
|
|
229
|
+
if (body.length) body.push('');
|
|
230
|
+
const safeMessage = sanitizeReportError({
|
|
231
|
+
message: formatConciseError(error)
|
|
232
|
+
}).message;
|
|
233
|
+
const lines = stripVTControlCharacters(safeMessage).split(/\r?\n/);
|
|
234
|
+
body.push(` Error: ${shorten(lines[0], 320)}`, ...lines.slice(1, 6).map((line)=>` ${shorten(line, 320)}`));
|
|
235
|
+
if (lines.length > 6) body.push(' … (use --reporter verbose for details)');
|
|
236
|
+
const input = step ? formatFailureInput(step.input) : [];
|
|
237
|
+
if (input.length) {
|
|
238
|
+
body.push('');
|
|
239
|
+
input.forEach((line, index)=>field(0 === index ? 'Input' : '', line));
|
|
240
|
+
}
|
|
241
|
+
const frame = firstBusinessFrame(error, context.projectRoot);
|
|
242
|
+
if (frame) body.push('', ` at ${oneLine(frame)}`);
|
|
243
|
+
return {
|
|
244
|
+
title,
|
|
245
|
+
body
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
var env_ = __webpack_require__("@midscene/shared/env");
|
|
249
|
+
function _define_property(obj, key, value) {
|
|
250
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
251
|
+
value: value,
|
|
252
|
+
enumerable: true,
|
|
253
|
+
configurable: true,
|
|
254
|
+
writable: true
|
|
255
|
+
});
|
|
256
|
+
else obj[key] = value;
|
|
257
|
+
return obj;
|
|
258
|
+
}
|
|
259
|
+
function configuredModelNames() {
|
|
260
|
+
const env = env_.globalConfigManager.getAllEnvConfig();
|
|
261
|
+
const defaultName = env.MIDSCENE_MODEL_NAME;
|
|
262
|
+
const models = new Map();
|
|
263
|
+
for (const [intent, name] of [
|
|
264
|
+
[
|
|
265
|
+
'default',
|
|
266
|
+
defaultName
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
'planning',
|
|
270
|
+
env.MIDSCENE_PLANNING_MODEL_NAME || defaultName
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
'insight',
|
|
274
|
+
env.MIDSCENE_INSIGHT_MODEL_NAME || defaultName
|
|
275
|
+
]
|
|
276
|
+
]){
|
|
277
|
+
if (!name) continue;
|
|
278
|
+
const intents = models.get(name) ?? [];
|
|
279
|
+
intents.push(intent);
|
|
280
|
+
models.set(name, intents);
|
|
281
|
+
}
|
|
282
|
+
return [
|
|
283
|
+
...models
|
|
284
|
+
].map(([name, intents])=>({
|
|
285
|
+
name,
|
|
286
|
+
intents
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
class CliModelUsage {
|
|
290
|
+
add(step) {
|
|
291
|
+
for (const trace of step.report?.traces ?? [])if (trace.metrics) {
|
|
292
|
+
this.executions.set(trace.executionId, trace.metrics);
|
|
293
|
+
this.missing.delete(trace.executionId);
|
|
294
|
+
} else if (!this.executions.has(trace.executionId)) this.missing.add(trace.executionId);
|
|
295
|
+
}
|
|
296
|
+
snapshot() {
|
|
297
|
+
if (!this.executions.size && !this.missing.size) return;
|
|
298
|
+
return sumTestRunReportMetrics([
|
|
299
|
+
...this.executions.values()
|
|
300
|
+
], this.missing.size ? [
|
|
301
|
+
'Some execution usage has not been recorded.'
|
|
302
|
+
] : []);
|
|
303
|
+
}
|
|
304
|
+
constructor(){
|
|
305
|
+
_define_property(this, "executions", new Map());
|
|
306
|
+
_define_property(this, "missing", new Set());
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function formatTokenCount(value) {
|
|
310
|
+
if (value < 1000) return String(Math.round(value));
|
|
311
|
+
if (value < 1000000) return `${(value / 1000).toFixed(1)}k`;
|
|
312
|
+
return `${(value / 1000000).toFixed(1)}m`;
|
|
313
|
+
}
|
|
314
|
+
function formatModelUsage(metrics) {
|
|
315
|
+
if (!metrics) return [
|
|
316
|
+
'—'
|
|
317
|
+
];
|
|
318
|
+
if (!metrics.models?.length) {
|
|
319
|
+
const name = metrics.modelCallCount || metrics.collection?.status === 'unknown' ? 'unknown' : '—';
|
|
320
|
+
return [
|
|
321
|
+
`${name} · ${formatTokenUsage(metrics)}`
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
return metrics.models.map((model)=>{
|
|
325
|
+
const name = stripVTControlCharacters(model.modelName).replace(/\s+/g, ' ').trim() || 'unknown';
|
|
326
|
+
const usage = formatTokenUsage({
|
|
327
|
+
...metrics,
|
|
328
|
+
modelCallCount: model.callCount,
|
|
329
|
+
promptTokens: model.promptTokens,
|
|
330
|
+
cachedInputTokens: model.cachedInputTokens,
|
|
331
|
+
completionTokens: model.completionTokens,
|
|
332
|
+
totalTokens: model.totalTokens
|
|
333
|
+
});
|
|
334
|
+
return `${name} · ${usage}`;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function formatTokenUsage(metrics, includeCached = true) {
|
|
338
|
+
if (!metrics) return '—';
|
|
339
|
+
const state = (field)=>metrics.collection?.fields?.[field] ?? metrics.collection?.status ?? 'complete';
|
|
340
|
+
if ('unknown' === state('totalTokens')) return '— (usage unavailable)';
|
|
341
|
+
if (!metrics.modelCallCount && !metrics.totalTokens && 'complete' === state('totalTokens')) return '0 (no model calls)';
|
|
342
|
+
const count = (field)=>'unknown' === state(field) ? '—' : `${'partial' === state(field) ? '≥' : ''}${formatTokenCount(metrics[field])}`;
|
|
343
|
+
return [
|
|
344
|
+
`${count('totalTokens')} total${'partial' === state('totalTokens') ? ' (partial)' : ''}`,
|
|
345
|
+
`${count('promptTokens')} input${includeCached && metrics.cachedInputTokens ? ` (${formatTokenCount(metrics.cachedInputTokens)} cached)` : ''}`,
|
|
346
|
+
`${count('completionTokens')} output`
|
|
347
|
+
].join(' · ');
|
|
348
|
+
}
|
|
349
|
+
function terminal_progress_define_property(obj, key, value) {
|
|
350
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
351
|
+
value: value,
|
|
352
|
+
enumerable: true,
|
|
353
|
+
configurable: true,
|
|
354
|
+
writable: true
|
|
355
|
+
});
|
|
356
|
+
else obj[key] = value;
|
|
357
|
+
return obj;
|
|
358
|
+
}
|
|
359
|
+
const graphemes = new Intl.Segmenter(void 0, {
|
|
360
|
+
granularity: 'grapheme'
|
|
361
|
+
});
|
|
362
|
+
const SYNC_START = '\u001b[?2026h';
|
|
363
|
+
const SYNC_END = '\u001b[?2026l';
|
|
364
|
+
const truncateTerminalLine = (text, columns)=>{
|
|
365
|
+
if (columns <= 0) return '';
|
|
366
|
+
if (string_width(text) <= columns) return text;
|
|
367
|
+
let output = '';
|
|
368
|
+
let width = 0;
|
|
369
|
+
for (const { segment } of graphemes.segment(stripVTControlCharacters(text))){
|
|
370
|
+
width += string_width(segment);
|
|
371
|
+
if (width > columns - 1) break;
|
|
372
|
+
output += segment;
|
|
373
|
+
}
|
|
374
|
+
return `${output}…`;
|
|
375
|
+
};
|
|
376
|
+
class TerminalProgress {
|
|
377
|
+
erase() {
|
|
378
|
+
if (!this.lines.length) return '';
|
|
379
|
+
const columns = Math.max(1, this.io.columns ?? 100);
|
|
380
|
+
const occupiedRows = this.lines.reduce((sum, line)=>sum + Math.max(1, Math.ceil(string_width(line) / columns)), 0);
|
|
381
|
+
const rows = Math.min(occupiedRows, Math.max(0, (this.io.rows ?? 24) - 1));
|
|
382
|
+
this.lines = [];
|
|
383
|
+
return `\r${rows ? `\u001b[${rows}A` : ''}\u001b[0J`;
|
|
384
|
+
}
|
|
385
|
+
frame(previousHeight = 0) {
|
|
386
|
+
const availableRows = Math.max(0, (this.io.rows ?? 24) - 2);
|
|
387
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
388
|
+
if (!availableRows || !columns) return '';
|
|
389
|
+
const footer = this.footer.slice(0, availableRows);
|
|
390
|
+
const budget = availableRows - footer.length;
|
|
391
|
+
const overflow = this.content.length > budget;
|
|
392
|
+
const visible = Math.max(0, budget - (overflow ? 1 : 0));
|
|
393
|
+
const body = this.content.slice(0, visible);
|
|
394
|
+
if (overflow && budget > 0) body.push(` … ${this.content.length - visible} more rows`);
|
|
395
|
+
const lines = [
|
|
396
|
+
...body,
|
|
397
|
+
...footer
|
|
398
|
+
].map((line)=>truncateTerminalLine(line, columns));
|
|
399
|
+
const blanks = Math.max(0, Math.min(previousHeight, availableRows) - lines.length);
|
|
400
|
+
this.lines = [
|
|
401
|
+
...Array.from({
|
|
402
|
+
length: blanks
|
|
403
|
+
}, ()=>''),
|
|
404
|
+
...lines
|
|
405
|
+
];
|
|
406
|
+
return this.lines.length ? `${this.lines.join('\n')}\n` : '';
|
|
407
|
+
}
|
|
408
|
+
render(content, footer) {
|
|
409
|
+
this.update(content, footer);
|
|
410
|
+
const previousHeight = this.lines.length;
|
|
411
|
+
const erase = this.erase();
|
|
412
|
+
this.io.write?.(`${SYNC_START}${erase}${this.frame(previousHeight)}${SYNC_END}`);
|
|
413
|
+
}
|
|
414
|
+
update(content, footer) {
|
|
415
|
+
this.content = content;
|
|
416
|
+
this.footer = footer;
|
|
417
|
+
}
|
|
418
|
+
output(message, write) {
|
|
419
|
+
this.io.write?.(SYNC_START);
|
|
420
|
+
try {
|
|
421
|
+
const erase = this.erase();
|
|
422
|
+
if (erase) this.io.write?.(erase);
|
|
423
|
+
write(message.endsWith('\n') ? message : `${message}\n`);
|
|
424
|
+
const frame = this.frame();
|
|
425
|
+
if (frame) this.io.write?.(frame);
|
|
426
|
+
} finally{
|
|
427
|
+
this.io.write?.(SYNC_END);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
clear() {
|
|
431
|
+
const erase = this.erase();
|
|
432
|
+
this.content = [];
|
|
433
|
+
this.footer = [];
|
|
434
|
+
if (erase) this.io.write?.(`${SYNC_START}${erase}${SYNC_END}`);
|
|
435
|
+
}
|
|
436
|
+
constructor(io){
|
|
437
|
+
terminal_progress_define_property(this, "io", void 0);
|
|
438
|
+
terminal_progress_define_property(this, "lines", void 0);
|
|
439
|
+
terminal_progress_define_property(this, "content", void 0);
|
|
440
|
+
terminal_progress_define_property(this, "footer", void 0);
|
|
441
|
+
this.io = io;
|
|
442
|
+
this.lines = [];
|
|
443
|
+
this.content = [];
|
|
444
|
+
this.footer = [];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const warning_format_oneLine = (text)=>stripVTControlCharacters(text).replace(/\s+/g, ' ').trim();
|
|
448
|
+
function formatWarningScope(scope) {
|
|
449
|
+
if (!scope) return '';
|
|
450
|
+
const parts = [
|
|
451
|
+
`[${warning_format_oneLine(scope.projectName)}]`
|
|
452
|
+
];
|
|
453
|
+
if (scope.sourcePath) parts.push(warning_format_oneLine(scope.sourcePath));
|
|
454
|
+
if (scope.caseName) parts.push(warning_format_oneLine(scope.caseName));
|
|
455
|
+
if (scope.phase) {
|
|
456
|
+
const phase = 'steps' === scope.phase ? 'step' : scope.phase;
|
|
457
|
+
parts.push(`${phase}${void 0 === scope.stepIndex ? '' : `[${scope.stepIndex + 1}]`}${scope.node ? ` ${warning_format_oneLine(scope.node)}` : ''}`);
|
|
458
|
+
}
|
|
459
|
+
return `${parts.join(' › ')} · `;
|
|
460
|
+
}
|
|
461
|
+
function errorMessage(error) {
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
let current = error;
|
|
464
|
+
let message = '';
|
|
465
|
+
let status;
|
|
466
|
+
while(current && 'object' == typeof current && !seen.has(current)){
|
|
467
|
+
seen.add(current);
|
|
468
|
+
const detail = current;
|
|
469
|
+
if ('string' == typeof detail.message) message = detail.message;
|
|
470
|
+
if ('number' == typeof detail.status) status = detail.status;
|
|
471
|
+
current = detail.cause;
|
|
472
|
+
}
|
|
473
|
+
const firstLine = warning_format_oneLine((message || String(error)).split(/\r?\n/)[0]);
|
|
474
|
+
return status && !firstLine.startsWith(String(status)) ? `${status}: ${firstLine}` : firstLine;
|
|
475
|
+
}
|
|
476
|
+
function formatWarning(entry) {
|
|
477
|
+
const details = entry.warning;
|
|
478
|
+
let message;
|
|
479
|
+
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)}`;
|
|
480
|
+
else {
|
|
481
|
+
const error = entry.args?.find((arg)=>arg instanceof Error);
|
|
482
|
+
const prefix = 'string' == typeof entry.args?.[0] ? warning_format_oneLine(entry.args[0].split(/\r?\n/)[0]).replace(/:\s*$/, '') : '';
|
|
483
|
+
message = error ? `${prefix ? `${prefix}: ` : ''}${errorMessage(error)}` : warning_format_oneLine(entry.message.split(/\r?\n/)[0]);
|
|
484
|
+
message = message.replace(/^\[midscene:warning\]\s*/, '');
|
|
485
|
+
}
|
|
486
|
+
if (message.startsWith('waitForNetworkIdle is skipped for Playwright.')) message = 'Playwright: post-action network-idle wait skipped.';
|
|
487
|
+
return message.length > 240 ? `${message.slice(0, 239)}…` : message;
|
|
488
|
+
}
|
|
489
|
+
function warningKey(entry, message) {
|
|
490
|
+
const details = entry.warning;
|
|
491
|
+
if (details?.kind === 'model-request-retry') return `${entry.topic}\n${JSON.stringify(entry.scope)}\n${details.callId}\n${details.attempt}`;
|
|
492
|
+
const scope = entry.args?.some((arg)=>arg instanceof Error) ? JSON.stringify(entry.scope) : '';
|
|
493
|
+
return `${entry.topic}\n${scope}\n${message}`;
|
|
494
|
+
}
|
|
495
|
+
function terminal_reporter_define_property(obj, key, value) {
|
|
496
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
497
|
+
value: value,
|
|
498
|
+
enumerable: true,
|
|
499
|
+
configurable: true,
|
|
500
|
+
writable: true
|
|
501
|
+
});
|
|
502
|
+
else obj[key] = value;
|
|
503
|
+
return obj;
|
|
504
|
+
}
|
|
505
|
+
const terminal_reporter_oneLine = (value)=>stripVTControlCharacters(value).replace(/\s+/g, ' ').trim();
|
|
506
|
+
const formatDuration = (ms)=>{
|
|
507
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
508
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
509
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
|
|
510
|
+
};
|
|
511
|
+
const identity = (project, path, name)=>`[${terminal_reporter_oneLine(project)}] ${terminal_reporter_oneLine(path)}${name ? ` › ${terminal_reporter_oneLine(name)}` : ''}`;
|
|
512
|
+
const projectPhaseLabel = (phase)=>'setup' === phase ? 'setting up' : 'cleaning up';
|
|
513
|
+
const fileStatus = (status, cases)=>{
|
|
514
|
+
if ('failed' === status || cases.some((item)=>'failed' === item.status)) return 'failed';
|
|
515
|
+
if (cases.length && cases.every((item)=>'not-run' === item.status)) return 'not-run';
|
|
516
|
+
if (cases.some((item)=>'not-run' === item.status)) return 'failed';
|
|
517
|
+
return status ?? (cases.length ? 'success' : 'not-run');
|
|
518
|
+
};
|
|
519
|
+
class TerminalReporter {
|
|
520
|
+
get interactive() {
|
|
521
|
+
return !!this.io.isTTY && !!this.io.write && 'default' === this.mode;
|
|
522
|
+
}
|
|
523
|
+
log(message) {
|
|
524
|
+
if (this.interactive) this.output(`${message}\n`, (text)=>this.io.write(text));
|
|
525
|
+
else this.io.log(message);
|
|
526
|
+
this.lastOutput = Date.now();
|
|
527
|
+
}
|
|
528
|
+
output(message, write) {
|
|
529
|
+
this.renderProgress(false);
|
|
530
|
+
this.progressView.output(message, write);
|
|
531
|
+
this.lastOutput = Date.now();
|
|
532
|
+
}
|
|
533
|
+
progress(message) {
|
|
534
|
+
if ('verbose' === this.mode) this.log(message);
|
|
535
|
+
}
|
|
536
|
+
console(entry) {
|
|
537
|
+
if ('report' === entry.type) {
|
|
538
|
+
if ('verbose' === this.mode) this.log(entry.message);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const scope = formatWarningScope(entry.scope);
|
|
542
|
+
if ('verbose' === this.mode) {
|
|
543
|
+
const kind = entry.warning?.kind === 'model-request-retry' ? 'model request retry · ' : entry.warning?.kind === 'model-request-detail' ? 'model request error · ' : '';
|
|
544
|
+
this.log(`WARN ${scope}${kind}${entry.message}`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (entry.warning?.kind === 'model-request-detail') return;
|
|
548
|
+
const message = formatWarning(entry);
|
|
549
|
+
const key = warningKey(entry, message);
|
|
550
|
+
if (this.warnings.has(key)) return;
|
|
551
|
+
this.warnings.set(key, {
|
|
552
|
+
scope,
|
|
553
|
+
message
|
|
554
|
+
});
|
|
555
|
+
if (this.interactive) {
|
|
556
|
+
let attached = false;
|
|
557
|
+
if (entry.warning?.kind === 'model-request-retry' && entry.scope) {
|
|
558
|
+
const owner = entry.scope;
|
|
559
|
+
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)) {
|
|
560
|
+
task.warning = message;
|
|
561
|
+
attached = true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!attached) this.latestNotice = message.startsWith('Playwright:') ? 'Playwright: network-idle wait skipped.' : `${scope}${message}`;
|
|
565
|
+
this.schedule();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.log(`${this.color.yellow('WARN')} ${this.color.dim(scope)}${message}`);
|
|
569
|
+
}
|
|
570
|
+
file(projectName, sourcePath, total = 0) {
|
|
571
|
+
const key = `${projectName}\0${sourcePath}`;
|
|
572
|
+
let file = this.files.get(key);
|
|
573
|
+
if (!file) {
|
|
574
|
+
file = {
|
|
575
|
+
projectName,
|
|
576
|
+
sourcePath,
|
|
577
|
+
total,
|
|
578
|
+
outcomes: new Map(),
|
|
579
|
+
attemptOutcomes: new Map(),
|
|
580
|
+
started: false,
|
|
581
|
+
printed: false,
|
|
582
|
+
durationMs: 0,
|
|
583
|
+
retries: 0,
|
|
584
|
+
completionOrder: 0
|
|
585
|
+
};
|
|
586
|
+
this.files.set(key, file);
|
|
587
|
+
}
|
|
588
|
+
file.total = Math.max(file.total, total);
|
|
589
|
+
return file;
|
|
590
|
+
}
|
|
591
|
+
event(event) {
|
|
592
|
+
if ('run-start' === event.type) {
|
|
593
|
+
this.total = event.cases;
|
|
594
|
+
this.totalFiles = event.documents;
|
|
595
|
+
this.startedAt = event.startedAt ? Date.parse(event.startedAt) : Date.now();
|
|
596
|
+
for (const file of event.files ?? [])this.file(file.projectName, file.sourcePath, file.cases);
|
|
597
|
+
const c = this.color;
|
|
598
|
+
this.log(`\n ${c.bgGreen.black.bold(' RUN ')} ${c.green(`midscene-test v${package_0.rE}`)} ${c.dim(terminal_reporter_oneLine(event.projectRoot ?? process.cwd()))}`);
|
|
599
|
+
this.log(` ${c.dim(`${event.projects} projects · ${event.documents} files · ${event.cases} cases`)}`);
|
|
600
|
+
if (void 0 !== event.maxConcurrentProjects && void 0 !== event.maxConcurrentFiles) this.log(` ${c.dim(`Concurrency ${event.maxConcurrentProjects ?? 'unlimited'} projects · ${event.maxConcurrentFiles ?? 'unlimited'} files (global limits)`)}`);
|
|
601
|
+
if (event.configPath) this.log(` ${c.dim(`Config ${terminal_reporter_oneLine(event.configPath)}`)}`);
|
|
602
|
+
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(', ')}`)}`);
|
|
603
|
+
this.log('');
|
|
604
|
+
if ('default' === this.mode) {
|
|
605
|
+
this.timer = setInterval(()=>this.tick(), this.interactive ? 1000 : 30000);
|
|
606
|
+
this.timer.unref();
|
|
607
|
+
if (this.interactive) this.removeResizeListener = this.io.onResize?.(()=>this.renderProgress());
|
|
608
|
+
this.renderProgress();
|
|
609
|
+
}
|
|
610
|
+
} else if ('project-end' === event.type) {
|
|
611
|
+
this.completeProject(event.project);
|
|
612
|
+
this.schedule();
|
|
613
|
+
} else if ('project-phase' === event.type) {
|
|
614
|
+
if ('running' === event.phase) this.projectPhases.delete(event.projectName);
|
|
615
|
+
else this.projectPhases.set(event.projectName, {
|
|
616
|
+
phase: event.phase,
|
|
617
|
+
since: Date.now()
|
|
618
|
+
});
|
|
619
|
+
this.schedule();
|
|
620
|
+
} else if ('document-start' === event.type) {
|
|
621
|
+
this.file(event.projectName, event.sourcePath, event.cases).started = true;
|
|
622
|
+
this.schedule();
|
|
623
|
+
} else if ('document-end' === event.type) {
|
|
624
|
+
const file = this.file(event.document.projectName, event.document.sourcePath, event.cases.length);
|
|
625
|
+
file.status = fileStatus(event.document.status, event.cases);
|
|
626
|
+
file.started = true;
|
|
627
|
+
file.completionOrder = ++this.completionOrder;
|
|
628
|
+
file.durationMs = event.durationMs;
|
|
629
|
+
file.retries = event.document.attemptIndex ?? 0;
|
|
630
|
+
file.outcomes = new Map(event.cases.map((outcome)=>[
|
|
631
|
+
outcome.caseId,
|
|
632
|
+
outcome
|
|
633
|
+
]));
|
|
634
|
+
file.attemptOutcomes.clear();
|
|
635
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
636
|
+
if (!this.interactive) this.printFile(file);
|
|
637
|
+
this.schedule();
|
|
638
|
+
} else if ('step-start' === event.type) this.startStep(event.projectName, event.info);
|
|
639
|
+
else if ('step-end' === event.type) {
|
|
640
|
+
if (event.result) this.usage.add(event.result);
|
|
641
|
+
const key = 'case' === event.info.scope ? event.info.case.caseId : event.info.document.documentId;
|
|
642
|
+
const task = this.active.get(key);
|
|
643
|
+
if (task) if ('document' === event.info.scope) this.active.delete(key);
|
|
644
|
+
else {
|
|
645
|
+
task.step = void 0;
|
|
646
|
+
task.warning = void 0;
|
|
647
|
+
}
|
|
648
|
+
this.schedule();
|
|
649
|
+
} else if ('case-attempt-end' === event.type) this.attempts.set(event.attempt.caseId, {
|
|
650
|
+
run: event.attempt,
|
|
651
|
+
max: event.maxAttempts
|
|
652
|
+
});
|
|
653
|
+
else if ('case-end' === event.type || 'case-progress' === event.type) {
|
|
654
|
+
const file = this.file(event.outcome.projectName, event.outcome.sourcePath);
|
|
655
|
+
if ('case-progress' === event.type) {
|
|
656
|
+
file.attemptOutcomes.set(event.outcome.caseId, event.outcome);
|
|
657
|
+
file.retries = Math.max(file.retries, event.documentAttemptIndex);
|
|
658
|
+
} else {
|
|
659
|
+
file.attemptOutcomes.delete(event.outcome.caseId);
|
|
660
|
+
file.outcomes.set(event.outcome.caseId, event.outcome);
|
|
661
|
+
file.retries = Math.max(file.retries, event.documentRetries ?? 0);
|
|
662
|
+
}
|
|
663
|
+
this.active.delete(event.outcome.caseId);
|
|
664
|
+
this.schedule();
|
|
665
|
+
} else if ('document-retry' === event.type) {
|
|
666
|
+
const file = this.file(event.document.projectName, event.document.sourcePath);
|
|
667
|
+
file.outcomes.clear();
|
|
668
|
+
file.attemptOutcomes.clear();
|
|
669
|
+
file.status = void 0;
|
|
670
|
+
file.completionOrder = 0;
|
|
671
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
672
|
+
this.log(this.color.yellow(` ↻ ${identity(file.projectName, file.sourcePath)} · file retry ${(event.document.attemptIndex ?? 0) + 2}/${event.maxAttempts}`));
|
|
673
|
+
this.schedule();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
startStep(projectName, info) {
|
|
677
|
+
const position = 'case' === info.scope ? info.case : info.document;
|
|
678
|
+
const file = this.file(projectName, position.sourcePath);
|
|
679
|
+
file.started = true;
|
|
680
|
+
const key = 'case' === info.scope ? info.case.caseId : info.document.documentId;
|
|
681
|
+
const attempt = 'case' === info.scope ? info.case.attemptIndex : 0;
|
|
682
|
+
const previous = this.attempts.get(key);
|
|
683
|
+
if (previous && previous.run.attemptIndex < attempt) {
|
|
684
|
+
this.attempts.delete(key);
|
|
685
|
+
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}`));
|
|
686
|
+
}
|
|
687
|
+
const existing = this.active.get(key);
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
this.active.set(key, {
|
|
690
|
+
file,
|
|
691
|
+
name: 'case' === info.scope ? info.case.name : void 0,
|
|
692
|
+
step: `${'steps' === position.phase ? 'step' : position.phase} ${position.stepIndex + 1}/${info.stepCount} · ${terminal_reporter_oneLine(info.node)}`,
|
|
693
|
+
since: existing?.attempt === attempt ? existing.since : now,
|
|
694
|
+
attempt,
|
|
695
|
+
maxAttempts: previous?.max ?? existing?.maxAttempts,
|
|
696
|
+
runId: 'case' === info.scope ? info.case.runId : info.document.documentRunId,
|
|
697
|
+
phase: position.phase,
|
|
698
|
+
stepIndex: position.stepIndex,
|
|
699
|
+
caseIndex: 'case' === info.scope ? info.case.caseIndex : void 0
|
|
700
|
+
});
|
|
701
|
+
this.schedule();
|
|
702
|
+
}
|
|
703
|
+
schedule() {
|
|
704
|
+
if (!this.interactive || !this.timer || this.scheduled) return;
|
|
705
|
+
this.scheduled = setTimeout(()=>{
|
|
706
|
+
this.scheduled = void 0;
|
|
707
|
+
this.renderProgress();
|
|
708
|
+
}, 100);
|
|
709
|
+
this.scheduled.unref();
|
|
710
|
+
}
|
|
711
|
+
counts(items, total) {
|
|
712
|
+
const failed = items.filter((item)=>'failed' === item.status).length;
|
|
713
|
+
const passed = items.filter((item)=>'success' === item.status).length;
|
|
714
|
+
const notRun = items.filter((item)=>'not-run' === item.status).length;
|
|
715
|
+
return this.stateCounts(passed, failed, notRun, total);
|
|
716
|
+
}
|
|
717
|
+
stateCounts(passed, failed, notRun, total) {
|
|
718
|
+
const c = this.color;
|
|
719
|
+
return [
|
|
720
|
+
failed ? c.red.bold(`${failed} failed`) : '',
|
|
721
|
+
c.green.bold(`${passed} passed`),
|
|
722
|
+
notRun ? c.yellow(`${notRun} not run`) : ''
|
|
723
|
+
].filter(Boolean).join(c.dim(' | ')) + c.dim(` (${total})`);
|
|
724
|
+
}
|
|
725
|
+
footer(durationMs = Date.now() - this.startedAt, result) {
|
|
726
|
+
const files = [
|
|
727
|
+
...this.files.values()
|
|
728
|
+
];
|
|
729
|
+
const cases = files.flatMap((file)=>[
|
|
730
|
+
...file.outcomes.values()
|
|
731
|
+
]);
|
|
732
|
+
const c = this.color;
|
|
733
|
+
const metrics = result ? this.finalMetrics : this.usage.snapshot();
|
|
734
|
+
return [
|
|
735
|
+
` ${c.dim('Test Files')} ${this.counts(files, this.totalFiles)}`,
|
|
736
|
+
` ${c.dim(' Cases')} ${result ? this.stateCounts(result.summary.passed, result.summary.failed, result.summary.notRun, result.summary.total) : this.counts(cases, this.total)}`,
|
|
737
|
+
` ${c.dim(' Start at')} ${new Date(this.startedAt).toLocaleTimeString('en-GB', {
|
|
738
|
+
hour12: false
|
|
739
|
+
})}`,
|
|
740
|
+
` ${c.dim(' Duration')} ${formatDuration(durationMs)}`,
|
|
741
|
+
...formatModelUsage(metrics).map((model, index)=>` ${c.dim(index ? ' ' : ' Models:')} ${model}`),
|
|
742
|
+
...!result || this.warnings.size ? [
|
|
743
|
+
` ${c.dim(' Warnings')} ${this.warnings.size ? c.yellow(String(this.warnings.size)) : c.dim('0')}${!result && this.latestNotice ? c.dim(` · ${this.latestNotice}`) : ''}`
|
|
744
|
+
] : []
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
renderProgress(render = true) {
|
|
748
|
+
if (!this.interactive || !this.timer) return;
|
|
749
|
+
const projects = new Map();
|
|
750
|
+
const files = [
|
|
751
|
+
...this.files.values()
|
|
752
|
+
].filter((file)=>file.started && !file.printed).sort((a, b)=>a.projectName.localeCompare(b.projectName) || a.sourcePath.localeCompare(b.sourcePath));
|
|
753
|
+
for (const file of files){
|
|
754
|
+
const group = projects.get(file.projectName) ?? [];
|
|
755
|
+
group.push(file);
|
|
756
|
+
projects.set(file.projectName, group);
|
|
757
|
+
}
|
|
758
|
+
for (const projectName of this.projectPhases.keys())if (!projects.has(projectName)) projects.set(projectName, []);
|
|
759
|
+
const footer = this.footer();
|
|
760
|
+
const budget = Math.max(0, (this.io.rows ?? 24) - 2 - footer.length - 1);
|
|
761
|
+
let lines = this.liveTree(projects, 'full');
|
|
762
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'recent');
|
|
763
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'compact');
|
|
764
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'active');
|
|
765
|
+
if (lines.length) lines.push('');
|
|
766
|
+
if (render) this.progressView.render(lines, footer);
|
|
767
|
+
else this.progressView.update(lines, footer);
|
|
768
|
+
}
|
|
769
|
+
liveTree(projects, mode) {
|
|
770
|
+
const c = this.color;
|
|
771
|
+
const lines = [];
|
|
772
|
+
const groups = [
|
|
773
|
+
...projects
|
|
774
|
+
];
|
|
775
|
+
const isActive = ([projectName, files])=>this.projectPhases.has(projectName) || files.some((file)=>!file.status);
|
|
776
|
+
if ('full' !== mode) groups.sort((a, b)=>Number(isActive(b)) - Number(isActive(a)));
|
|
777
|
+
for (const [projectName, files] of groups){
|
|
778
|
+
const phase = this.projectPhases.get(projectName);
|
|
779
|
+
const running = files.filter((file)=>!file.status);
|
|
780
|
+
const completed = files.filter((file)=>file.status);
|
|
781
|
+
const recent = [
|
|
782
|
+
...completed
|
|
783
|
+
].sort((a, b)=>b.completionOrder - a.completionOrder).slice(0, 'recent' === mode ? 1 : 0);
|
|
784
|
+
const visible = 'full' === mode ? files : [
|
|
785
|
+
...running,
|
|
786
|
+
...recent
|
|
787
|
+
];
|
|
788
|
+
const hidden = completed.filter((file)=>!visible.includes(file));
|
|
789
|
+
const hiddenFailures = hidden.filter((file)=>'failed' === file.status).length;
|
|
790
|
+
const details = [
|
|
791
|
+
phase ? c.yellow(`${projectPhaseLabel(phase.phase)} ${formatDuration(Date.now() - phase.since)}`) : '',
|
|
792
|
+
c.yellow(`${running.length} ${1 === running.length ? 'file' : 'files'} running`),
|
|
793
|
+
completed.length ? c.dim(`${completed.length} completed`) : '',
|
|
794
|
+
'active' === mode && hiddenFailures ? c.red(`${hiddenFailures} failed`) : ''
|
|
795
|
+
].filter(Boolean);
|
|
796
|
+
lines.push(this.liveRow(` ${c.yellow('❯')} ${c.bold(terminal_reporter_oneLine(projectName))}`, c.dim(' · ') + details.join(c.dim(' · '))));
|
|
797
|
+
for (const [fileIndex, file] of visible.entries()){
|
|
798
|
+
const lastFile = fileIndex === visible.length - 1 && (!hidden.length || 'active' === mode);
|
|
799
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
800
|
+
const outcomes = [
|
|
801
|
+
...new Map([
|
|
802
|
+
...file.attemptOutcomes,
|
|
803
|
+
...file.outcomes
|
|
804
|
+
]).values()
|
|
805
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
806
|
+
const failedCases = outcomes.filter((outcome)=>'failed' === outcome.status).length;
|
|
807
|
+
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`) : ''}`));
|
|
808
|
+
const tasks = [
|
|
809
|
+
...this.active.values()
|
|
810
|
+
].filter((task)=>task.file === file);
|
|
811
|
+
const shown = 'full' === mode ? outcomes : 'recent' === mode ? outcomes.slice(-1) : [];
|
|
812
|
+
const children = [
|
|
813
|
+
...tasks.map((task)=>({
|
|
814
|
+
task
|
|
815
|
+
})),
|
|
816
|
+
...shown.map((outcome)=>({
|
|
817
|
+
outcome
|
|
818
|
+
}))
|
|
819
|
+
];
|
|
820
|
+
if ('full' === mode) children.sort((a, b)=>this.liveChildIndex(a) - this.liveChildIndex(b));
|
|
821
|
+
const hiddenCases = outcomes.filter((outcome)=>!shown.includes(outcome));
|
|
822
|
+
if (hiddenCases.length && 'active' !== mode) {
|
|
823
|
+
const failed = hiddenCases.filter((outcome)=>'failed' === outcome.status).length;
|
|
824
|
+
children.push({
|
|
825
|
+
summary: `… ${hiddenCases.length} completed ${1 === hiddenCases.length ? 'case' : 'cases'}${failed ? ` · ${failed} failed` : ''}`,
|
|
826
|
+
failed: failed > 0
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
for (const [index, child] of children.entries()){
|
|
830
|
+
const last = index === children.length - 1;
|
|
831
|
+
const prefix = `${stem}${c.dim(last ? '└─' : '├─')} `;
|
|
832
|
+
if ('outcome' in child) {
|
|
833
|
+
lines.push(this.liveRow(`${prefix}${this.formatCaseResult(child.outcome, file.retries)}`, ''));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if ('summary' in child) {
|
|
837
|
+
lines.push(this.liveRow(`${prefix}${child.failed ? c.red(child.summary) : c.dim(child.summary)}`, ''));
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const { task } = child;
|
|
841
|
+
const hint = task.name && task.step ? truncateTerminalLine(` · ${task.step}`, Math.floor(Math.max(0, (this.io.columns ?? 100) - 1) / 2)) : '';
|
|
842
|
+
const retry = task.attempt ? ` · attempt ${task.attempt + 1}${task.maxAttempts ? `/${task.maxAttempts}` : ''}` : '';
|
|
843
|
+
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)}`));
|
|
844
|
+
if (task.warning) lines.push(this.liveRow(`${stem}${last ? ' ' : `${c.dim('│')} `}${c.dim('└─')} ${c.yellow(`⚠ ${task.warning}`)}`, ''));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (hidden.length && 'active' !== mode) {
|
|
848
|
+
const summary = `… ${hidden.length} completed ${1 === hidden.length ? 'file' : 'files'}${hiddenFailures ? ` · ${hiddenFailures} failed` : ''}`;
|
|
849
|
+
lines.push(this.liveRow(` ${c.dim('└─')} ${hiddenFailures ? c.red(summary) : c.dim(summary)}`, ''));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return lines;
|
|
853
|
+
}
|
|
854
|
+
liveChildIndex(child) {
|
|
855
|
+
if ('outcome' in child) return child.outcome.caseIndex;
|
|
856
|
+
if ('summary' in child) return Number.MAX_SAFE_INTEGER;
|
|
857
|
+
return child.task.caseIndex ?? ('beforeAll' === child.task.phase ? -1 : Number.MAX_SAFE_INTEGER);
|
|
858
|
+
}
|
|
859
|
+
liveRow(label, suffix) {
|
|
860
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
861
|
+
const tail = truncateTerminalLine(suffix, columns);
|
|
862
|
+
return truncateTerminalLine(label, columns - string_width(tail)) + tail;
|
|
863
|
+
}
|
|
864
|
+
tick() {
|
|
865
|
+
if (this.interactive) return void this.renderProgress();
|
|
866
|
+
if ((this.active.size || this.projectPhases.size) && Date.now() - this.lastOutput >= 30000) this.log(`… Still running: ${[
|
|
867
|
+
...[
|
|
868
|
+
...this.projectPhases
|
|
869
|
+
].map(([projectName, phase])=>`[${terminal_reporter_oneLine(projectName)}] · ${projectPhaseLabel(phase.phase)} (${formatDuration(Date.now() - phase.since)})`),
|
|
870
|
+
...[
|
|
871
|
+
...this.active.values()
|
|
872
|
+
].map((task)=>`${identity(task.file.projectName, task.file.sourcePath, task.name)} · ${task.step ?? 'between steps'} (${formatDuration(Date.now() - task.since)})`)
|
|
873
|
+
].join(' | ')}`);
|
|
874
|
+
}
|
|
875
|
+
formatCaseResult(outcome, documentRetries) {
|
|
876
|
+
const c = this.color;
|
|
877
|
+
const attempts = outcome.attempts ?? (outcome.run ? [
|
|
878
|
+
outcome.run
|
|
879
|
+
] : []);
|
|
880
|
+
const retries = Math.max(attempts.length - 1, outcome.run?.attemptIndex ?? 0);
|
|
881
|
+
const duration = formatDuration(attempts.reduce((sum, attempt)=>sum + attempt.durationMs, 0));
|
|
882
|
+
const symbol = 'success' === outcome.status ? c.green('✓') : 'failed' === outcome.status ? c.red('×') : c.yellow('–');
|
|
883
|
+
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`) : ''}`;
|
|
884
|
+
return `${symbol} ${terminal_reporter_oneLine(outcome.name)}${detail}`;
|
|
885
|
+
}
|
|
886
|
+
statusSymbol(status) {
|
|
887
|
+
return 'failed' === status ? this.color.red('×') : 'not-run' === status ? this.color.yellow('–') : this.color.green('✓');
|
|
888
|
+
}
|
|
889
|
+
resultFileLines(file, lastFile) {
|
|
890
|
+
const c = this.color;
|
|
891
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
892
|
+
const cases = [
|
|
893
|
+
...file.outcomes.values()
|
|
894
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
895
|
+
const failed = cases.filter((item)=>'failed' === item.status).length;
|
|
896
|
+
const notRun = cases.filter((item)=>'not-run' === item.status).length;
|
|
897
|
+
const stats = `(${cases.length} cases${failed ? ` | ${failed} failed` : ''}${notRun ? ` | ${notRun} not run` : ''})`;
|
|
898
|
+
return [
|
|
899
|
+
` ${c.dim(lastFile ? '└─' : '├─')} ${this.statusSymbol(file.status)} ${c.bold(terminal_reporter_oneLine(file.sourcePath))} ${c.dim(stats)} ${c.dim(formatDuration(file.durationMs))}`,
|
|
900
|
+
...cases.map((item, caseIndex)=>`${stem}${c.dim(caseIndex === cases.length - 1 ? '└─' : '├─')} ${this.formatCaseResult(item, file.retries)}`)
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
printFile(file) {
|
|
904
|
+
if (file.printed) return;
|
|
905
|
+
file.printed = true;
|
|
906
|
+
this.log([
|
|
907
|
+
'',
|
|
908
|
+
` ${this.color.bold(terminal_reporter_oneLine(file.projectName))}`,
|
|
909
|
+
...this.resultFileLines(file, true)
|
|
910
|
+
].join('\n'));
|
|
911
|
+
}
|
|
912
|
+
printProject(projectName, files, failed) {
|
|
913
|
+
if (!files.length || files.every((file)=>file.printed)) return;
|
|
914
|
+
const c = this.color;
|
|
915
|
+
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';
|
|
916
|
+
const lines = [
|
|
917
|
+
'',
|
|
918
|
+
` ${this.statusSymbol(status)} ${c.bold(terminal_reporter_oneLine(projectName))} ${c.dim(`(${files.length} ${1 === files.length ? 'file' : 'files'})`)}`
|
|
919
|
+
];
|
|
920
|
+
for (const [index, file] of files.entries())if (!file.printed) {
|
|
921
|
+
file.printed = true;
|
|
922
|
+
lines.push(...this.resultFileLines(file, index === files.length - 1));
|
|
923
|
+
}
|
|
924
|
+
this.log(lines.join('\n'));
|
|
925
|
+
}
|
|
926
|
+
failure(source, error, context) {
|
|
927
|
+
this.failures.push({
|
|
928
|
+
source,
|
|
929
|
+
error,
|
|
930
|
+
context
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
completeProject(project) {
|
|
934
|
+
this.projectPhases.delete(project.name);
|
|
935
|
+
const projectFiles = new Set();
|
|
936
|
+
const documents = new Map(project.documents.map((document)=>[
|
|
937
|
+
document.documentId,
|
|
938
|
+
document
|
|
939
|
+
]));
|
|
940
|
+
const resultSources = new Set([
|
|
941
|
+
...project.cases.map((outcome)=>outcome.sourcePath),
|
|
942
|
+
...project.documents.map((document)=>document.sourcePath),
|
|
943
|
+
...project.collectionErrors.map((error)=>error.sourcePath)
|
|
944
|
+
]);
|
|
945
|
+
for (const plan of project.plans ?? []){
|
|
946
|
+
const selected = plan.cases.filter((item)=>item.selected).length;
|
|
947
|
+
if (selected || resultSources.has(plan.sourcePath)) projectFiles.add(this.file(project.name, plan.sourcePath, selected));
|
|
948
|
+
}
|
|
949
|
+
for (const outcome of project.cases)this.file(project.name, outcome.sourcePath).outcomes.set(outcome.caseId, outcome);
|
|
950
|
+
for (const document of documents.values()){
|
|
951
|
+
const file = this.file(project.name, document.sourcePath);
|
|
952
|
+
file.status = document.status;
|
|
953
|
+
file.retries = document.attemptIndex ?? 0;
|
|
954
|
+
file.durationMs = project.documents.filter((item)=>item.documentId === document.documentId).reduce((sum, item)=>sum + item.durationMs, 0);
|
|
955
|
+
}
|
|
956
|
+
for (const error of project.collectionErrors)this.file(project.name, error.sourcePath).status = 'failed';
|
|
957
|
+
for (const file of this.files.values())if (file.projectName === project.name) {
|
|
958
|
+
file.attemptOutcomes.clear();
|
|
959
|
+
const cases = [
|
|
960
|
+
...file.outcomes.values()
|
|
961
|
+
];
|
|
962
|
+
file.status = fileStatus(file.status, cases);
|
|
963
|
+
projectFiles.add(file);
|
|
964
|
+
}
|
|
965
|
+
for (const [key, task] of this.active)if (task.file.projectName === project.name) this.active.delete(key);
|
|
966
|
+
if (this.interactive) this.printProject(project.name, [
|
|
967
|
+
...projectFiles
|
|
968
|
+
], 'failed' === project.status);
|
|
969
|
+
else for (const file of projectFiles)this.printFile(file);
|
|
970
|
+
}
|
|
971
|
+
finish(result) {
|
|
972
|
+
this.dispose();
|
|
973
|
+
this.finalMetrics = result.metrics ?? this.usage.snapshot();
|
|
974
|
+
if (result.startedAt) this.startedAt = Date.parse(result.startedAt);
|
|
975
|
+
for (const project of result.projects)this.completeProject(project);
|
|
976
|
+
this.total = result.summary.total;
|
|
977
|
+
this.totalFiles = this.files.size;
|
|
978
|
+
}
|
|
979
|
+
sectionHeading(label, kind) {
|
|
980
|
+
const c = this.color;
|
|
981
|
+
const width = Math.max(0, (this.io.columns ?? 80) - 2);
|
|
982
|
+
const title = truncateTerminalLine(` ${label} `, width);
|
|
983
|
+
const padding = Math.max(0, width - string_width(title));
|
|
984
|
+
const left = Math.floor(padding / 2);
|
|
985
|
+
const line = 'warning' === kind ? c.yellow : c.red;
|
|
986
|
+
const badge = 'warning' === kind ? c.bgYellow.black.bold : c.bgRed.white.bold;
|
|
987
|
+
this.log(`\n ${line('─'.repeat(left))}${badge(title)}${line('─'.repeat(padding - left))}\n`);
|
|
988
|
+
}
|
|
989
|
+
summary(result) {
|
|
990
|
+
const c = this.color;
|
|
991
|
+
if (this.interactive && this.warnings.size) {
|
|
992
|
+
this.sectionHeading(`Warnings ${this.warnings.size}`, 'warning');
|
|
993
|
+
for (const warning of [
|
|
994
|
+
...this.warnings.values()
|
|
995
|
+
].slice(0, 5))this.log(` ${c.yellow('WARN')} ${c.dim(warning.scope)}${warning.message}`);
|
|
996
|
+
if (this.warnings.size > 5) this.log(c.dim(` … ${this.warnings.size - 5} more warnings; see debug logs or --reporter verbose.`));
|
|
997
|
+
}
|
|
998
|
+
if (this.failures.length) {
|
|
999
|
+
this.sectionHeading(`Failures ${this.failures.length}`, 'failure');
|
|
1000
|
+
for (const failure of this.failures){
|
|
1001
|
+
if ('default' === this.mode) {
|
|
1002
|
+
const detail = formatFailureDetails(failure.source, failure.error, failure.context);
|
|
1003
|
+
const body = detail.body.map((line)=>{
|
|
1004
|
+
if (line.startsWith(' Error:')) return c.red(line);
|
|
1005
|
+
if (line.startsWith(' at ')) return c.dim(line);
|
|
1006
|
+
return line.replace(/^ {2}(?:Step|Location|Attempt|File try|Phase|Input)\s+/, (label)=>c.dim(label));
|
|
1007
|
+
}).join('\n');
|
|
1008
|
+
const block = `${c.bgRed.white.bold(' FAIL ')} ${c.bold(detail.title)}\n${body}\n`;
|
|
1009
|
+
if (this.interactive) this.log(block);
|
|
1010
|
+
else this.io.error(`FAIL ${detail.title}\n${detail.body.join('\n')}\n`);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const message = formatCliError(failure.error);
|
|
1014
|
+
this.io.error(`FAIL midscene-test: ${failure.source}:\n${message.split('\n').map((line)=>` ${line}`).join('\n')}\n`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
this.log(`\n${this.footer(result.durationMs, result).join('\n')}`);
|
|
1018
|
+
this.log('');
|
|
1019
|
+
this.failures = [];
|
|
1020
|
+
}
|
|
1021
|
+
dispose() {
|
|
1022
|
+
if (this.timer) clearInterval(this.timer);
|
|
1023
|
+
if (this.scheduled) clearTimeout(this.scheduled);
|
|
1024
|
+
this.timer = void 0;
|
|
1025
|
+
this.scheduled = void 0;
|
|
1026
|
+
this.removeResizeListener?.();
|
|
1027
|
+
this.removeResizeListener = void 0;
|
|
1028
|
+
this.progressView.clear();
|
|
1029
|
+
}
|
|
1030
|
+
constructor(io, mode = 'default'){
|
|
1031
|
+
terminal_reporter_define_property(this, "io", void 0);
|
|
1032
|
+
terminal_reporter_define_property(this, "mode", void 0);
|
|
1033
|
+
terminal_reporter_define_property(this, "total", void 0);
|
|
1034
|
+
terminal_reporter_define_property(this, "completionOrder", void 0);
|
|
1035
|
+
terminal_reporter_define_property(this, "totalFiles", void 0);
|
|
1036
|
+
terminal_reporter_define_property(this, "files", void 0);
|
|
1037
|
+
terminal_reporter_define_property(this, "warnings", void 0);
|
|
1038
|
+
terminal_reporter_define_property(this, "latestNotice", void 0);
|
|
1039
|
+
terminal_reporter_define_property(this, "usage", void 0);
|
|
1040
|
+
terminal_reporter_define_property(this, "finalMetrics", void 0);
|
|
1041
|
+
terminal_reporter_define_property(this, "attempts", void 0);
|
|
1042
|
+
terminal_reporter_define_property(this, "active", void 0);
|
|
1043
|
+
terminal_reporter_define_property(this, "projectPhases", void 0);
|
|
1044
|
+
terminal_reporter_define_property(this, "failures", void 0);
|
|
1045
|
+
terminal_reporter_define_property(this, "lastOutput", void 0);
|
|
1046
|
+
terminal_reporter_define_property(this, "startedAt", void 0);
|
|
1047
|
+
terminal_reporter_define_property(this, "timer", void 0);
|
|
1048
|
+
terminal_reporter_define_property(this, "scheduled", void 0);
|
|
1049
|
+
terminal_reporter_define_property(this, "progressView", void 0);
|
|
1050
|
+
terminal_reporter_define_property(this, "removeResizeListener", void 0);
|
|
1051
|
+
terminal_reporter_define_property(this, "color", void 0);
|
|
1052
|
+
this.io = io;
|
|
1053
|
+
this.mode = mode;
|
|
1054
|
+
this.total = 0;
|
|
1055
|
+
this.completionOrder = 0;
|
|
1056
|
+
this.totalFiles = 0;
|
|
1057
|
+
this.files = new Map();
|
|
1058
|
+
this.warnings = new Map();
|
|
1059
|
+
this.usage = new CliModelUsage();
|
|
1060
|
+
this.attempts = new Map();
|
|
1061
|
+
this.active = new Map();
|
|
1062
|
+
this.projectPhases = new Map();
|
|
1063
|
+
this.failures = [];
|
|
1064
|
+
this.lastOutput = Date.now();
|
|
1065
|
+
this.startedAt = Date.now();
|
|
1066
|
+
this.progressView = new TerminalProgress(io);
|
|
1067
|
+
this.color = new external_chalk_["default"].Instance({
|
|
1068
|
+
level: io.isTTY && !process.env.NO_COLOR ? 1 : 0
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const escapeData = (text)=>text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
1073
|
+
const escapeProperty = (text)=>escapeData(text).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
1074
|
+
const markdown = (text)=>text.replace(/[&<>`\r\n]/g, (char)=>({
|
|
1075
|
+
'&': '&',
|
|
1076
|
+
'<': '<',
|
|
1077
|
+
'>': '>',
|
|
1078
|
+
'`': '`',
|
|
1079
|
+
'\r': ' ',
|
|
1080
|
+
'\n': ' '
|
|
1081
|
+
})[char]);
|
|
1082
|
+
function githubFailureAnnotation(source, message, file) {
|
|
1083
|
+
if ('true' !== process.env.GITHUB_ACTIONS) return;
|
|
1084
|
+
const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd();
|
|
1085
|
+
const path = file ? (0, external_node_path_.relative)(workspace, (0, external_node_path_.resolve)(file)).split(external_node_path_.sep).join('/') : void 0;
|
|
1086
|
+
return `::error ${path ? `file=${escapeProperty(path)},` : ''}title=${escapeProperty(source)}::${escapeData(message)}`;
|
|
1087
|
+
}
|
|
1088
|
+
function writeGitHubSummary(result) {
|
|
1089
|
+
const output = process.env.GITHUB_STEP_SUMMARY;
|
|
1090
|
+
if ('true' !== process.env.GITHUB_ACTIONS || !output) return;
|
|
1091
|
+
const failedCases = [
|
|
1092
|
+
...new Map(result.cases.map((item)=>[
|
|
1093
|
+
item.caseId,
|
|
1094
|
+
item
|
|
1095
|
+
])).values()
|
|
1096
|
+
].filter((item)=>'failed' === item.status);
|
|
1097
|
+
const summary = [
|
|
1098
|
+
'## Midscene Test',
|
|
1099
|
+
'',
|
|
1100
|
+
'| Passed | Failed | Not run | Passed after retry | Duration |',
|
|
1101
|
+
'| ---: | ---: | ---: | ---: | ---: |',
|
|
1102
|
+
`| ${result.summary.passed} | ${result.summary.failed} | ${result.summary.notRun} | ${result.summary.passedAfterRetry} | ${formatDuration(result.durationMs)} |`,
|
|
1103
|
+
'',
|
|
1104
|
+
'Passed after retry is included in Passed.',
|
|
1105
|
+
'',
|
|
1106
|
+
...failedCases.map((item)=>`- Failed: **${markdown(item.name)}** — ${markdown(item.projectName)}/\`${markdown(item.sourcePath)}\``),
|
|
1107
|
+
...result.summary.collectionErrors ? [
|
|
1108
|
+
`- Collection errors: ${result.summary.collectionErrors}`
|
|
1109
|
+
] : [],
|
|
1110
|
+
...result.summary.documentFailures ? [
|
|
1111
|
+
`- Failed files: ${result.summary.documentFailures}`
|
|
1112
|
+
] : [],
|
|
1113
|
+
...result.summary.projectFailures ? [
|
|
1114
|
+
`- Failed projects: ${result.summary.projectFailures}`
|
|
1115
|
+
] : [],
|
|
1116
|
+
'',
|
|
1117
|
+
...result.reportPath ? [
|
|
1118
|
+
`Report artifact entry: \`${markdown(result.reportPath)}\``
|
|
1119
|
+
] : [],
|
|
1120
|
+
`Structured results: \`${markdown(result.summaryPath)}\``,
|
|
1121
|
+
''
|
|
1122
|
+
].join('\n');
|
|
1123
|
+
(0, external_node_fs_.appendFileSync)(output, `${summary}\n`);
|
|
1124
|
+
}
|
|
105
1125
|
const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
|
|
106
1126
|
const sortNodesForSpec = (nodes)=>{
|
|
107
1127
|
const priority = (name)=>'aiAct' === name ? 0 : 'aiAssert' === name ? 1 : 2;
|
|
@@ -234,6 +1254,32 @@ const renderNodeSpec = (nodes, location)=>{
|
|
|
234
1254
|
warnings
|
|
235
1255
|
};
|
|
236
1256
|
};
|
|
1257
|
+
function captureTerminalOutput(output, streams = [
|
|
1258
|
+
process.stdout,
|
|
1259
|
+
process.stderr
|
|
1260
|
+
]) {
|
|
1261
|
+
const cleanups = [];
|
|
1262
|
+
for (const stream of streams){
|
|
1263
|
+
const original = stream.write;
|
|
1264
|
+
const forward = original.bind(stream);
|
|
1265
|
+
const intercepted = (chunk, encoding, callback)=>{
|
|
1266
|
+
const done = 'function' == typeof encoding ? encoding : callback;
|
|
1267
|
+
const message = 'string' == typeof chunk ? chunk : Buffer.from(chunk).toString();
|
|
1268
|
+
let accepted = true;
|
|
1269
|
+
output(message, (text)=>{
|
|
1270
|
+
accepted = forward(text, done);
|
|
1271
|
+
});
|
|
1272
|
+
return accepted;
|
|
1273
|
+
};
|
|
1274
|
+
stream.write = intercepted;
|
|
1275
|
+
cleanups.push(()=>{
|
|
1276
|
+
if (stream.write === intercepted) stream.write = original;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return ()=>{
|
|
1280
|
+
for (const cleanup of cleanups)cleanup();
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
237
1283
|
const registry_NodeRegistry = NodeRegistry;
|
|
238
1284
|
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
239
1285
|
const getDefaultExport = (loaded, absolutePath)=>{
|
|
@@ -581,11 +1627,13 @@ function parseWorkflowSource(source) {
|
|
|
581
1627
|
start: state.position + sourceOffset,
|
|
582
1628
|
end: 0,
|
|
583
1629
|
value: null,
|
|
584
|
-
children: []
|
|
1630
|
+
children: [],
|
|
1631
|
+
kind: null
|
|
585
1632
|
});
|
|
586
1633
|
const node = stack.pop();
|
|
587
1634
|
node.end = state.position + sourceOffset;
|
|
588
1635
|
node.value = state.result;
|
|
1636
|
+
node.kind = state.kind;
|
|
589
1637
|
stack.at(-1)?.children.push(node);
|
|
590
1638
|
nodes.push(node);
|
|
591
1639
|
if ('mapping' !== state.kind) return;
|
|
@@ -615,6 +1663,36 @@ function parseWorkflowSource(source) {
|
|
|
615
1663
|
const value = sanitizeReportText(redacted, 65536);
|
|
616
1664
|
return {
|
|
617
1665
|
parsed,
|
|
1666
|
+
locate (path) {
|
|
1667
|
+
let node = nodes.at(-1);
|
|
1668
|
+
for (const part of path){
|
|
1669
|
+
if (!node) return;
|
|
1670
|
+
while(1 === node.children.length && node.children[0].value === node.value)node = node.children[0];
|
|
1671
|
+
if ('number' == typeof part && 'sequence' === node.kind) node = node.children[part];
|
|
1672
|
+
else {
|
|
1673
|
+
if ('string' != typeof part || 'mapping' !== node.kind) return;
|
|
1674
|
+
const children = node.children;
|
|
1675
|
+
node = void 0;
|
|
1676
|
+
for(let index = 0; index < children.length; index += 2)if (children[index].value === part) {
|
|
1677
|
+
node = children[index + 1];
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (!node) return;
|
|
1683
|
+
let start = node.start;
|
|
1684
|
+
while(start < node.end)if (/\s/.test(source[start])) start++;
|
|
1685
|
+
else if ('#' === source[start]) {
|
|
1686
|
+
const end = source.indexOf('\n', start);
|
|
1687
|
+
if (end < 0) return;
|
|
1688
|
+
start = end + 1;
|
|
1689
|
+
} else break;
|
|
1690
|
+
const prefix = source.slice(0, start).split(/\r\n|\r|\n/);
|
|
1691
|
+
return {
|
|
1692
|
+
line: prefix.length,
|
|
1693
|
+
column: prefix.at(-1).length + 1
|
|
1694
|
+
};
|
|
1695
|
+
},
|
|
618
1696
|
sourceYaml: {
|
|
619
1697
|
value,
|
|
620
1698
|
...redactedPaths.size ? {
|
|
@@ -673,7 +1751,7 @@ const resolveWorkflowVariables = (value, options)=>{
|
|
|
673
1751
|
]));
|
|
674
1752
|
return value;
|
|
675
1753
|
};
|
|
676
|
-
function
|
|
1754
|
+
function collect_define_property(obj, key, value) {
|
|
677
1755
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
678
1756
|
value: value,
|
|
679
1757
|
enumerable: true,
|
|
@@ -690,7 +1768,7 @@ class NativeWorkflowFormatError extends WorkflowParseError {
|
|
|
690
1768
|
sourcePath,
|
|
691
1769
|
actualFormat,
|
|
692
1770
|
expectedFormat: 'native'
|
|
693
|
-
}),
|
|
1771
|
+
}), collect_define_property(this, "actualFormat", void 0);
|
|
694
1772
|
this.actualFormat = actualFormat;
|
|
695
1773
|
}
|
|
696
1774
|
}
|
|
@@ -727,6 +1805,18 @@ function collectWorkflowDocument(source, options) {
|
|
|
727
1805
|
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
728
1806
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
729
1807
|
...step,
|
|
1808
|
+
meta: {
|
|
1809
|
+
...step.meta,
|
|
1810
|
+
sourceLocation: snapshot.locate('steps' === phase ? [
|
|
1811
|
+
'cases',
|
|
1812
|
+
caseIndex,
|
|
1813
|
+
'steps',
|
|
1814
|
+
stepIndex
|
|
1815
|
+
] : [
|
|
1816
|
+
phase,
|
|
1817
|
+
stepIndex
|
|
1818
|
+
])
|
|
1819
|
+
},
|
|
730
1820
|
templateInput: step.input,
|
|
731
1821
|
input: resolveWorkflowVariables(step.input, {
|
|
732
1822
|
variables: options.variables,
|
|
@@ -901,6 +1991,9 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
901
1991
|
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
902
1992
|
} : {},
|
|
903
1993
|
summary: result.summary,
|
|
1994
|
+
...result.metrics ? {
|
|
1995
|
+
metrics: result.metrics
|
|
1996
|
+
} : {},
|
|
904
1997
|
projects: result.projects.map((project)=>({
|
|
905
1998
|
projectId: project.projectId,
|
|
906
1999
|
name: project.name,
|
|
@@ -1297,11 +2390,12 @@ async function prepareTestRun(options) {
|
|
|
1297
2390
|
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1298
2391
|
reportEnabled: true,
|
|
1299
2392
|
publications: [],
|
|
1300
|
-
onProgress: options.onProgress ?? (()=>{})
|
|
2393
|
+
onProgress: options.onProgress ?? (()=>{}),
|
|
2394
|
+
onEvent: options.onEvent
|
|
1301
2395
|
};
|
|
1302
2396
|
}
|
|
1303
2397
|
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
1304
|
-
const
|
|
2398
|
+
const result_markdown_duration = (ms)=>{
|
|
1305
2399
|
const seconds = Math.round(ms / 1000);
|
|
1306
2400
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1307
2401
|
};
|
|
@@ -1312,7 +2406,7 @@ function renderTestResultMarkdown(dump, options = {}) {
|
|
|
1312
2406
|
const lines = [
|
|
1313
2407
|
'## Midscene Test',
|
|
1314
2408
|
'',
|
|
1315
|
-
`${outcome} · ${
|
|
2409
|
+
`${outcome} · ${result_markdown_duration(dump.durationMs)}`,
|
|
1316
2410
|
''
|
|
1317
2411
|
];
|
|
1318
2412
|
if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
|
|
@@ -1375,6 +2469,38 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1375
2469
|
}));
|
|
1376
2470
|
return output;
|
|
1377
2471
|
}
|
|
2472
|
+
function scopeNodeConsole(node) {
|
|
2473
|
+
return {
|
|
2474
|
+
...node,
|
|
2475
|
+
execute (ctx) {
|
|
2476
|
+
const position = 'case' === ctx.scope ? ctx.case : ctx.document;
|
|
2477
|
+
const scope = {
|
|
2478
|
+
projectName: position.projectName,
|
|
2479
|
+
sourcePath: position.sourcePath,
|
|
2480
|
+
...'case' === ctx.scope ? {
|
|
2481
|
+
caseId: ctx.case.caseId,
|
|
2482
|
+
caseName: ctx.case.name,
|
|
2483
|
+
runId: ctx.case.runId
|
|
2484
|
+
} : {
|
|
2485
|
+
runId: ctx.document.documentRunId
|
|
2486
|
+
},
|
|
2487
|
+
phase: position.phase,
|
|
2488
|
+
stepIndex: position.stepIndex,
|
|
2489
|
+
node: node.name
|
|
2490
|
+
};
|
|
2491
|
+
return runWithConsoleLogScope(scope, ()=>node.execute({
|
|
2492
|
+
...ctx,
|
|
2493
|
+
onTeardown (teardown) {
|
|
2494
|
+
ctx.onTeardown(()=>runWithConsoleLogScope({
|
|
2495
|
+
...scope,
|
|
2496
|
+
phase: 'teardown',
|
|
2497
|
+
stepIndex: void 0
|
|
2498
|
+
}, teardown));
|
|
2499
|
+
}
|
|
2500
|
+
}));
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
1378
2504
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1379
2505
|
const formatStep = (info)=>{
|
|
1380
2506
|
const position = stepPosition(info);
|
|
@@ -1421,69 +2547,121 @@ async function executeDocumentInvocation(options) {
|
|
|
1421
2547
|
const documentRetry = 'document' === invocation.retry.scope;
|
|
1422
2548
|
const attempts = [];
|
|
1423
2549
|
const fileSetup = invocation.bindings.documentSetup;
|
|
1424
|
-
const documentSetup =
|
|
2550
|
+
const documentSetup = fileSetup ? {
|
|
1425
2551
|
...fileSetup,
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
2552
|
+
setup (ctx) {
|
|
2553
|
+
const scope = {
|
|
2554
|
+
projectName: project.name,
|
|
2555
|
+
sourcePath: document.sourcePath,
|
|
2556
|
+
runId: ctx.document.documentRunId,
|
|
2557
|
+
phase: 'fileSetup'
|
|
2558
|
+
};
|
|
2559
|
+
return runWithConsoleLogScope(scope, async ()=>{
|
|
2560
|
+
try {
|
|
2561
|
+
return await fileSetup.setup({
|
|
2562
|
+
...ctx,
|
|
2563
|
+
onTeardown (teardown) {
|
|
2564
|
+
ctx.onTeardown(()=>runWithConsoleLogScope({
|
|
2565
|
+
...scope,
|
|
2566
|
+
phase: 'teardown'
|
|
2567
|
+
}, teardown));
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
if (isProjectResourceFailure(error)) options.markProjectFatal();
|
|
2572
|
+
throw error;
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
1433
2575
|
}
|
|
1434
|
-
} :
|
|
2576
|
+
} : void 0;
|
|
2577
|
+
options.onEvent?.({
|
|
2578
|
+
type: 'document-start',
|
|
2579
|
+
documentId: document.documentId,
|
|
2580
|
+
projectName: project.name,
|
|
2581
|
+
sourcePath: document.sourcePath,
|
|
2582
|
+
cases: document.cases.length
|
|
2583
|
+
});
|
|
1435
2584
|
await runDocumentAttempts({
|
|
1436
2585
|
retry: documentRetry ? invocation.retry.count : 0,
|
|
1437
2586
|
signal: options.signal,
|
|
1438
2587
|
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1439
2588
|
}, async (documentAttemptIndex)=>{
|
|
1440
2589
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1441
|
-
const execution = await
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2590
|
+
const execution = await runWithConsoleLogScope({
|
|
2591
|
+
projectName: project.name,
|
|
2592
|
+
sourcePath: document.sourcePath
|
|
2593
|
+
}, ()=>runWorkflowDocument(document, {
|
|
2594
|
+
plan: invocation.plan,
|
|
2595
|
+
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
2596
|
+
...invocation.bindings,
|
|
2597
|
+
resolveNode: (name)=>scopeNodeConsole(invocation.bindings.resolveNode(name)),
|
|
2598
|
+
documentSetup,
|
|
2599
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
2600
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
2601
|
+
project,
|
|
2602
|
+
projectContext: options.projectContext,
|
|
2603
|
+
retry: documentRetry ? 0 : invocation.retry.count,
|
|
2604
|
+
signal: options.signal,
|
|
2605
|
+
defaultTimeoutMs: invocation.defaultTimeoutMs,
|
|
2606
|
+
shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
|
|
2607
|
+
stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
|
|
2608
|
+
isFatalError: (run)=>[
|
|
2609
|
+
...run.beforeEach,
|
|
2610
|
+
...run.steps,
|
|
2611
|
+
...run.afterEach
|
|
2612
|
+
].some((step)=>step.error && (isFatalDeviceError(step.error) || isProjectResourceFailure(step.error))) || (run.teardownErrors ?? []).some((error)=>isFatalDeviceError(error) || isProjectResourceFailure(error)),
|
|
2613
|
+
onCaseStart: (collectedCase)=>{
|
|
2614
|
+
options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
2615
|
+
},
|
|
2616
|
+
onStepStart: (info)=>{
|
|
2617
|
+
options.onEvent?.({
|
|
2618
|
+
type: 'step-start',
|
|
2619
|
+
projectName: project.name,
|
|
2620
|
+
info
|
|
2621
|
+
});
|
|
2622
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
2623
|
+
options.onProgress(`${indent}→ ${formatStep(info)}`);
|
|
2624
|
+
},
|
|
2625
|
+
onStepResult: async (info, result)=>{
|
|
2626
|
+
if (isProjectResourceFailure(result.error)) options.markProjectFatal();
|
|
2627
|
+
await invocation.bindings.onStepResult?.(info, result);
|
|
2628
|
+
options.onProgress(formatStepResult(info, result));
|
|
2629
|
+
options.onEvent?.({
|
|
2630
|
+
type: 'step-end',
|
|
2631
|
+
projectName: project.name,
|
|
2632
|
+
info,
|
|
2633
|
+
result
|
|
2634
|
+
});
|
|
2635
|
+
},
|
|
2636
|
+
onCaseResult: async (attempt)=>{
|
|
2637
|
+
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
2638
|
+
options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} case attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
2639
|
+
if (!documentRetry) options.onEvent?.({
|
|
2640
|
+
type: 'case-attempt-end',
|
|
2641
|
+
attempt,
|
|
2642
|
+
maxAttempts: invocation.retry.count + 1
|
|
2643
|
+
});
|
|
2644
|
+
},
|
|
2645
|
+
onCaseOutcome: (outcome)=>{
|
|
2646
|
+
if (documentRetry) options.onEvent?.({
|
|
2647
|
+
type: 'case-progress',
|
|
2648
|
+
outcome,
|
|
2649
|
+
documentAttemptIndex
|
|
2650
|
+
});
|
|
2651
|
+
else options.onEvent?.({
|
|
2652
|
+
type: 'case-end',
|
|
2653
|
+
outcome
|
|
2654
|
+
});
|
|
2655
|
+
if (outcome.attempts?.some((attempt)=>[
|
|
2656
|
+
...attempt.beforeEach,
|
|
2657
|
+
...attempt.steps,
|
|
2658
|
+
...attempt.afterEach
|
|
2659
|
+
].some((step)=>isProjectResourceFailure(step.error)) || attempt.teardownErrors?.some(isProjectResourceFailure))) options.markProjectFatal();
|
|
2660
|
+
if (!options.isolatedContext && caseHasFatalError(outcome)) options.markProjectFatal();
|
|
2661
|
+
if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
|
|
2662
|
+
},
|
|
2663
|
+
onDocumentResult: options.onDocumentResult
|
|
2664
|
+
})).catch(async (error)=>{
|
|
1487
2665
|
if (error instanceof WorkflowExecutionFailure) {
|
|
1488
2666
|
const partial = error.result;
|
|
1489
2667
|
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>isResourceRecoveryError(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
@@ -1553,6 +2731,11 @@ async function executeDocumentInvocation(options) {
|
|
|
1553
2731
|
shouldStop: options.shouldBail()
|
|
1554
2732
|
});
|
|
1555
2733
|
if (documentRetry) execution.document.retryDecision = decision;
|
|
2734
|
+
if (documentRetry && 'retry' === decision.decision) options.onEvent?.({
|
|
2735
|
+
type: 'document-retry',
|
|
2736
|
+
document: execution.document,
|
|
2737
|
+
maxAttempts: invocation.retry.count + 1
|
|
2738
|
+
});
|
|
1556
2739
|
attempts.push(execution);
|
|
1557
2740
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1558
2741
|
...outcome,
|
|
@@ -1587,7 +2770,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1587
2770
|
});
|
|
1588
2771
|
const final = attempts.at(-1);
|
|
1589
2772
|
if (!final) return;
|
|
2773
|
+
if (documentRetry) for (const outcome of final.cases)options.onEvent?.({
|
|
2774
|
+
type: 'case-end',
|
|
2775
|
+
outcome,
|
|
2776
|
+
documentRetries: attempts.length - 1
|
|
2777
|
+
});
|
|
1590
2778
|
const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
|
|
2779
|
+
options.onEvent?.({
|
|
2780
|
+
type: 'document-end',
|
|
2781
|
+
document: final.document,
|
|
2782
|
+
cases: final.cases,
|
|
2783
|
+
durationMs: attempts.reduce((total, attempt)=>total + attempt.document.durationMs, 0)
|
|
2784
|
+
});
|
|
1591
2785
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1592
2786
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1593
2787
|
}
|
|
@@ -1627,11 +2821,45 @@ async function runPreparedFiles(options) {
|
|
|
1627
2821
|
code: 'UNRESOLVED_FILE_RESOURCES'
|
|
1628
2822
|
}));
|
|
1629
2823
|
};
|
|
2824
|
+
const finalizeProject = (state)=>{
|
|
2825
|
+
const { prepared } = state;
|
|
2826
|
+
const completed = new Set(state.cases.map((item)=>item.caseId));
|
|
2827
|
+
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
2828
|
+
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
2829
|
+
documentId: document.documentId,
|
|
2830
|
+
caseId: item.caseId,
|
|
2831
|
+
projectName: prepared.project.name,
|
|
2832
|
+
name: item.definition.name,
|
|
2833
|
+
sourcePath: item.sourcePath,
|
|
2834
|
+
caseIndex: item.caseIndex,
|
|
2835
|
+
status: 'not-run',
|
|
2836
|
+
notRunReason: reason
|
|
2837
|
+
});
|
|
2838
|
+
const order = new Map(prepared.documents.map((document, i)=>[
|
|
2839
|
+
document.documentId,
|
|
2840
|
+
i
|
|
2841
|
+
]));
|
|
2842
|
+
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
2843
|
+
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
2844
|
+
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
2845
|
+
};
|
|
1630
2846
|
const finishProject = async (state)=>{
|
|
1631
2847
|
if (!state.runtime || state.finished) return;
|
|
1632
2848
|
state.finished = true;
|
|
2849
|
+
try {
|
|
2850
|
+
options.onEvent?.({
|
|
2851
|
+
type: 'project-phase',
|
|
2852
|
+
projectName: state.prepared.project.name,
|
|
2853
|
+
phase: 'teardown'
|
|
2854
|
+
});
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
options.onInfrastructureError(error);
|
|
2857
|
+
}
|
|
1633
2858
|
const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
|
|
1634
|
-
state.lifecycle = await
|
|
2859
|
+
state.lifecycle = await runWithConsoleLogScope({
|
|
2860
|
+
projectName: state.prepared.project.name,
|
|
2861
|
+
phase: 'teardown'
|
|
2862
|
+
}, ()=>state.runtime.finish(failed ? 'failed' : 'success'));
|
|
1635
2863
|
const releaseProject = ()=>{
|
|
1636
2864
|
occupiedProjects--;
|
|
1637
2865
|
wake?.();
|
|
@@ -1645,7 +2873,11 @@ async function runPreparedFiles(options) {
|
|
|
1645
2873
|
options.onInfrastructureError(error);
|
|
1646
2874
|
releaseProject();
|
|
1647
2875
|
});
|
|
1648
|
-
} else
|
|
2876
|
+
} else {
|
|
2877
|
+
releaseProject();
|
|
2878
|
+
finalizeProject(state);
|
|
2879
|
+
options.onProjectEnd?.(state);
|
|
2880
|
+
}
|
|
1649
2881
|
};
|
|
1650
2882
|
const admit = ()=>{
|
|
1651
2883
|
for(let offset = 0; offset < states.length; offset++){
|
|
@@ -1686,7 +2918,20 @@ async function runPreparedFiles(options) {
|
|
|
1686
2918
|
});
|
|
1687
2919
|
setMaxListeners(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
|
|
1688
2920
|
options.announceProject(prepared, index);
|
|
1689
|
-
|
|
2921
|
+
options.onEvent?.({
|
|
2922
|
+
type: 'project-phase',
|
|
2923
|
+
projectName: project.name,
|
|
2924
|
+
phase: 'setup'
|
|
2925
|
+
});
|
|
2926
|
+
await runWithConsoleLogScope({
|
|
2927
|
+
projectName: project.name,
|
|
2928
|
+
phase: 'setup'
|
|
2929
|
+
}, ()=>state.runtime.start());
|
|
2930
|
+
options.onEvent?.({
|
|
2931
|
+
type: 'project-phase',
|
|
2932
|
+
projectName: project.name,
|
|
2933
|
+
phase: 'running'
|
|
2934
|
+
});
|
|
1690
2935
|
state.initializing = false;
|
|
1691
2936
|
if (!state.runtime.canRun) {
|
|
1692
2937
|
state.fatal = true;
|
|
@@ -1714,8 +2959,9 @@ async function runPreparedFiles(options) {
|
|
|
1714
2959
|
}));
|
|
1715
2960
|
},
|
|
1716
2961
|
addFailedCases: options.addFailedCases,
|
|
1717
|
-
onProgress: (message)=>options.onProgress(prepared, message),
|
|
2962
|
+
onProgress: (message)=>options.onProgress(prepared, options.maxConcurrentFiles > 1 ? ` [${invocation.document.sourcePath}]${message}` : message),
|
|
1718
2963
|
sinks: state,
|
|
2964
|
+
onEvent: options.onEvent,
|
|
1719
2965
|
isolatedContext: void 0 !== project.fileSetup,
|
|
1720
2966
|
isProjectResourceFailure: isSharedResourceFailure,
|
|
1721
2967
|
setupTimeoutMs: options.setupTimeoutMs,
|
|
@@ -1794,25 +3040,7 @@ async function runPreparedFiles(options) {
|
|
|
1794
3040
|
for (const state of states){
|
|
1795
3041
|
const { prepared } = state;
|
|
1796
3042
|
if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
|
|
1797
|
-
|
|
1798
|
-
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
1799
|
-
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
1800
|
-
documentId: document.documentId,
|
|
1801
|
-
caseId: item.caseId,
|
|
1802
|
-
projectName: prepared.project.name,
|
|
1803
|
-
name: item.definition.name,
|
|
1804
|
-
sourcePath: item.sourcePath,
|
|
1805
|
-
caseIndex: item.caseIndex,
|
|
1806
|
-
status: 'not-run',
|
|
1807
|
-
notRunReason: reason
|
|
1808
|
-
});
|
|
1809
|
-
const order = new Map(prepared.documents.map((document, i)=>[
|
|
1810
|
-
document.documentId,
|
|
1811
|
-
i
|
|
1812
|
-
]));
|
|
1813
|
-
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1814
|
-
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1815
|
-
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
3043
|
+
finalizeProject(state);
|
|
1816
3044
|
}
|
|
1817
3045
|
return states;
|
|
1818
3046
|
}
|
|
@@ -1884,6 +3112,32 @@ async function runPreparedTestProject(plan) {
|
|
|
1884
3112
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
1885
3113
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
1886
3114
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
3115
|
+
plan.onEvent?.({
|
|
3116
|
+
type: 'run-start',
|
|
3117
|
+
configuredModels: configuredModelNames(),
|
|
3118
|
+
projects: preparedProjects.length,
|
|
3119
|
+
documents: totalDocuments + totalErrors,
|
|
3120
|
+
cases: totalCases,
|
|
3121
|
+
projectRoot,
|
|
3122
|
+
configPath,
|
|
3123
|
+
startedAt: startedAt.toISOString(),
|
|
3124
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects,
|
|
3125
|
+
maxConcurrentFiles: definition.test.maxConcurrentFiles,
|
|
3126
|
+
files: preparedProjects.flatMap((prepared)=>[
|
|
3127
|
+
...prepared.documents.map((document)=>({
|
|
3128
|
+
documentId: document.documentId,
|
|
3129
|
+
projectName: prepared.project.name,
|
|
3130
|
+
sourcePath: document.sourcePath,
|
|
3131
|
+
cases: document.cases.length
|
|
3132
|
+
})),
|
|
3133
|
+
...prepared.collectionErrors.map((error)=>({
|
|
3134
|
+
documentId: error.sourcePath,
|
|
3135
|
+
projectName: prepared.project.name,
|
|
3136
|
+
sourcePath: error.sourcePath,
|
|
3137
|
+
cases: 0
|
|
3138
|
+
}))
|
|
3139
|
+
])
|
|
3140
|
+
});
|
|
1887
3141
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1888
3142
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? 1 / 0, Math.max(1, totalDocuments));
|
|
1889
3143
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
@@ -1986,7 +3240,12 @@ async function runPreparedTestProject(plan) {
|
|
|
1986
3240
|
shouldBail: bailReached,
|
|
1987
3241
|
addFailedCases,
|
|
1988
3242
|
announceProject,
|
|
1989
|
-
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
3243
|
+
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(preparedProjects.length > 1 || effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
3244
|
+
onEvent: (event)=>runInfrastructureCallback(()=>plan.onEvent?.(event)),
|
|
3245
|
+
onProjectEnd: (state)=>runInfrastructureCallback(()=>plan.onEvent?.({
|
|
3246
|
+
type: 'project-end',
|
|
3247
|
+
project: buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle)
|
|
3248
|
+
})),
|
|
1990
3249
|
onInfrastructureError: recordInfrastructureError
|
|
1991
3250
|
});
|
|
1992
3251
|
states.forEach((state, index)=>{
|
|
@@ -2067,6 +3326,10 @@ async function runPreparedTestProject(plan) {
|
|
|
2067
3326
|
buildRunnerDump: (index)=>{
|
|
2068
3327
|
reportIndex = index;
|
|
2069
3328
|
const dump = buildTestRunReportDump(completedResult, index);
|
|
3329
|
+
completedResult = {
|
|
3330
|
+
...completedResult,
|
|
3331
|
+
metrics: dump.metrics
|
|
3332
|
+
};
|
|
2070
3333
|
const warn = getDebug('test-runner:report-assembler', {
|
|
2071
3334
|
console: true
|
|
2072
3335
|
});
|
|
@@ -2084,6 +3347,16 @@ async function runPreparedTestProject(plan) {
|
|
|
2084
3347
|
await publication.publish(completedResult);
|
|
2085
3348
|
});
|
|
2086
3349
|
await publish('write-result', (0, external_node_path_.join)((0, external_node_path_.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
3350
|
+
if (!completedResult.metrics) {
|
|
3351
|
+
const dump = buildTestRunReportDump(completedResult, reportIndex ?? {
|
|
3352
|
+
sources: [],
|
|
3353
|
+
metrics: sumTestRunReportMetrics([])
|
|
3354
|
+
});
|
|
3355
|
+
completedResult = {
|
|
3356
|
+
...completedResult,
|
|
3357
|
+
metrics: dump.metrics
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
2087
3360
|
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
2088
3361
|
completedResult = {
|
|
2089
3362
|
...completedResult,
|
|
@@ -2104,6 +3377,7 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
2104
3377
|
let configPath;
|
|
2105
3378
|
let resultDir;
|
|
2106
3379
|
const projectNames = [];
|
|
3380
|
+
let reporter;
|
|
2107
3381
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
2108
3382
|
const token = args[index];
|
|
2109
3383
|
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
@@ -2114,16 +3388,21 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
2114
3388
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
2115
3389
|
continue;
|
|
2116
3390
|
}
|
|
2117
|
-
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
3391
|
+
if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--reporter' === arg) {
|
|
2118
3392
|
const value = inlineValue ?? args[index + 1];
|
|
2119
3393
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
2120
3394
|
if ('--config' === arg) configPath = value;
|
|
2121
3395
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
2122
|
-
else projectNames.push(value);
|
|
3396
|
+
else if ('--project' === arg) projectNames.push(value);
|
|
3397
|
+
else {
|
|
3398
|
+
if ('default' !== value && 'verbose' !== value) throw new Error(`Unknown reporter: ${value}. Expected default or verbose.`);
|
|
3399
|
+
reporter = value;
|
|
3400
|
+
}
|
|
2123
3401
|
if (void 0 === inlineValue) index += 1;
|
|
2124
3402
|
} else throw new Error(`Unknown option: ${token}`);
|
|
2125
3403
|
}
|
|
2126
3404
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
3405
|
+
if ('nodes' === command && reporter) throw new Error('--reporter is not supported by nodes.');
|
|
2127
3406
|
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
2128
3407
|
return {
|
|
2129
3408
|
...command ? {
|
|
@@ -2135,13 +3414,27 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
2135
3414
|
resultDir,
|
|
2136
3415
|
...projectNames.length > 0 ? {
|
|
2137
3416
|
projectNames
|
|
3417
|
+
} : {},
|
|
3418
|
+
...reporter ? {
|
|
3419
|
+
reporter
|
|
2138
3420
|
} : {}
|
|
2139
3421
|
};
|
|
2140
3422
|
};
|
|
2141
3423
|
const defaultCliIO = {
|
|
2142
3424
|
log: console.log,
|
|
2143
3425
|
error: console.error,
|
|
2144
|
-
write:
|
|
3426
|
+
write: process.stdout.write.bind(process.stdout),
|
|
3427
|
+
isTTY: Boolean(process.stdout.isTTY && !process.env.CI),
|
|
3428
|
+
get columns () {
|
|
3429
|
+
return process.stdout.columns;
|
|
3430
|
+
},
|
|
3431
|
+
get rows () {
|
|
3432
|
+
return process.stdout.rows;
|
|
3433
|
+
},
|
|
3434
|
+
onResize (listener) {
|
|
3435
|
+
process.stdout.on('resize', listener);
|
|
3436
|
+
return ()=>process.stdout.off('resize', listener);
|
|
3437
|
+
}
|
|
2145
3438
|
};
|
|
2146
3439
|
const testCliHelp = `Midscene Test: run native cases/steps Test projects.
|
|
2147
3440
|
|
|
@@ -2156,6 +3449,7 @@ Options:
|
|
|
2156
3449
|
--config <path> Native TypeScript or JavaScript Test config
|
|
2157
3450
|
--project <name> Select a native execution Project (repeatable)
|
|
2158
3451
|
--result-dir <path> Store Test result files in this directory
|
|
3452
|
+
--reporter <default|verbose> Concise Case results or full step diagnostics
|
|
2159
3453
|
--help, -h Show this help
|
|
2160
3454
|
--version Show the package version
|
|
2161
3455
|
|
|
@@ -2216,11 +3510,17 @@ const runNodesCommand = async (options, io)=>{
|
|
|
2216
3510
|
io.log(`\nNode Spec generated: ${specPath}`);
|
|
2217
3511
|
}
|
|
2218
3512
|
};
|
|
2219
|
-
const reportTestProjectResult = (result, io, infrastructureErrors = result.errors ?? [])=>{
|
|
3513
|
+
const reportTestProjectResult = (result, io, reporter, projectRoot, infrastructureErrors = result.errors ?? [])=>{
|
|
3514
|
+
reporter.finish(result);
|
|
2220
3515
|
const reportedErrors = new Set();
|
|
2221
|
-
const reportError = (source, error)=>{
|
|
3516
|
+
const reportError = (source, error, sourcePath, context)=>{
|
|
2222
3517
|
if (error instanceof Error && reportedErrors.has(error)) return;
|
|
2223
|
-
|
|
3518
|
+
reporter.failure(source, error, {
|
|
3519
|
+
projectRoot,
|
|
3520
|
+
...context
|
|
3521
|
+
});
|
|
3522
|
+
const annotation = githubFailureAnnotation(source, formatConciseError(error), sourcePath ? (0, external_node_path_.resolve)(projectRoot, sourcePath) : void 0);
|
|
3523
|
+
if (annotation) io.log(annotation);
|
|
2224
3524
|
const pending = [
|
|
2225
3525
|
error
|
|
2226
3526
|
];
|
|
@@ -2232,7 +3532,33 @@ const reportTestProjectResult = (result, io, infrastructureErrors = result.error
|
|
|
2232
3532
|
if (current instanceof AggregateError) pending.push(...current.errors);
|
|
2233
3533
|
}
|
|
2234
3534
|
};
|
|
2235
|
-
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error
|
|
3535
|
+
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error, failure.sourcePath, {
|
|
3536
|
+
projectName: failure.projectName,
|
|
3537
|
+
sourcePath: failure.sourcePath,
|
|
3538
|
+
phase: 'collection'
|
|
3539
|
+
});
|
|
3540
|
+
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
3541
|
+
document.documentId,
|
|
3542
|
+
document
|
|
3543
|
+
]));
|
|
3544
|
+
const documentContext = (projectName, documentId, sourcePath)=>{
|
|
3545
|
+
const project = result.projects.find((project)=>project.name === projectName);
|
|
3546
|
+
const document = finalDocuments.get(documentId);
|
|
3547
|
+
const plan = document?.plan ?? project?.plans?.find((plan)=>plan.documentId === documentId);
|
|
3548
|
+
const fileIndex = document?.attemptIndex ?? 0;
|
|
3549
|
+
return {
|
|
3550
|
+
plan,
|
|
3551
|
+
projectName,
|
|
3552
|
+
sourcePath,
|
|
3553
|
+
...plan?.retry.scope === 'document' ? {
|
|
3554
|
+
fileAttempt: {
|
|
3555
|
+
index: fileIndex,
|
|
3556
|
+
total: Math.max(fileIndex + 1, plan.retry.count + 1)
|
|
3557
|
+
}
|
|
3558
|
+
} : {},
|
|
3559
|
+
caseAttempts: plan?.retry.scope === 'document' ? 1 : (plan?.retry.count ?? project?.retry ?? 0) + 1
|
|
3560
|
+
};
|
|
3561
|
+
};
|
|
2236
3562
|
const finalCases = new Map(result.cases.map((outcome)=>[
|
|
2237
3563
|
outcome.caseId,
|
|
2238
3564
|
outcome
|
|
@@ -2241,49 +3567,107 @@ const reportTestProjectResult = (result, io, infrastructureErrors = result.error
|
|
|
2241
3567
|
if ('failed' !== outcome.status || !outcome.run) continue;
|
|
2242
3568
|
const run = outcome.run;
|
|
2243
3569
|
const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
...
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
3570
|
+
const { caseAttempts, plan, ...scope } = documentContext(run.projectName, outcome.documentId, run.sourcePath);
|
|
3571
|
+
const context = {
|
|
3572
|
+
...scope,
|
|
3573
|
+
caseName: run.name,
|
|
3574
|
+
attempt: {
|
|
3575
|
+
index: run.attemptIndex,
|
|
3576
|
+
total: Math.max(caseAttempts, run.attemptIndex + 1)
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
const casePlan = plan?.cases.find((item)=>item.caseId === outcome.caseId);
|
|
3580
|
+
for (const [phase, steps] of [
|
|
3581
|
+
[
|
|
3582
|
+
'beforeEach',
|
|
3583
|
+
run.beforeEach
|
|
3584
|
+
],
|
|
3585
|
+
[
|
|
3586
|
+
'steps',
|
|
3587
|
+
run.steps
|
|
3588
|
+
],
|
|
3589
|
+
[
|
|
3590
|
+
'afterEach',
|
|
3591
|
+
run.afterEach
|
|
3592
|
+
]
|
|
3593
|
+
])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, {
|
|
3594
|
+
...context,
|
|
3595
|
+
step,
|
|
3596
|
+
stepCount: casePlan?.[phase].length
|
|
3597
|
+
});
|
|
3598
|
+
for (const [phase, errors] of [
|
|
3599
|
+
[
|
|
3600
|
+
'execution',
|
|
3601
|
+
run.executionErrors
|
|
3602
|
+
],
|
|
3603
|
+
[
|
|
3604
|
+
'teardown',
|
|
3605
|
+
run.teardownErrors
|
|
3606
|
+
]
|
|
3607
|
+
])for (const error of errors ?? [])reportError(source, error, run.sourcePath, {
|
|
3608
|
+
...context,
|
|
3609
|
+
phase
|
|
3610
|
+
});
|
|
2253
3611
|
}
|
|
2254
|
-
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
2255
|
-
document.documentId,
|
|
2256
|
-
document
|
|
2257
|
-
]));
|
|
2258
3612
|
for (const document of finalDocuments.values()){
|
|
2259
3613
|
if ('failed' !== document.status) continue;
|
|
2260
3614
|
const source = `${document.projectName}/${document.sourcePath}`;
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
3615
|
+
const { caseAttempts: _, plan, ...context } = documentContext(document.projectName, document.documentId, document.sourcePath);
|
|
3616
|
+
for (const [phase, steps] of [
|
|
3617
|
+
[
|
|
3618
|
+
'beforeAll',
|
|
3619
|
+
document.beforeAll
|
|
3620
|
+
],
|
|
3621
|
+
[
|
|
3622
|
+
'afterAll',
|
|
3623
|
+
document.afterAll
|
|
3624
|
+
]
|
|
3625
|
+
])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error, document.sourcePath, {
|
|
3626
|
+
...context,
|
|
3627
|
+
step,
|
|
3628
|
+
stepCount: plan?.[phase].length
|
|
3629
|
+
});
|
|
3630
|
+
for (const [phase, errors] of [
|
|
3631
|
+
[
|
|
3632
|
+
'execution',
|
|
3633
|
+
document.executionErrors
|
|
3634
|
+
],
|
|
3635
|
+
[
|
|
3636
|
+
'teardown',
|
|
3637
|
+
document.teardownErrors
|
|
3638
|
+
]
|
|
3639
|
+
])for (const error of errors ?? [])reportError(source, error, document.sourcePath, {
|
|
3640
|
+
...context,
|
|
3641
|
+
phase
|
|
3642
|
+
});
|
|
3643
|
+
for (const failure of document.hostErrors ?? [])reportError(`${source} / host ${failure.phase}`, failure.error, document.sourcePath, {
|
|
3644
|
+
...context,
|
|
3645
|
+
phase: `host ${failure.phase}`
|
|
3646
|
+
});
|
|
2270
3647
|
}
|
|
2271
3648
|
for (const project of result.projects){
|
|
2272
|
-
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError
|
|
2273
|
-
|
|
3649
|
+
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError, void 0, {
|
|
3650
|
+
projectName: project.name,
|
|
3651
|
+
phase: 'setup'
|
|
3652
|
+
});
|
|
3653
|
+
for (const error of project.lifecycle?.teardownErrors ?? [])reportError(`${project.name} / teardown`, error, void 0, {
|
|
3654
|
+
projectName: project.name,
|
|
3655
|
+
phase: 'teardown'
|
|
3656
|
+
});
|
|
2274
3657
|
}
|
|
2275
3658
|
for (const error of infrastructureErrors)reportError('infrastructure', error);
|
|
2276
|
-
|
|
2277
|
-
io.log(`Results: ${result.resultDir}`);
|
|
3659
|
+
reporter.summary(result);
|
|
3660
|
+
if ('verbose' === reporter.mode) io.log(`Results: ${result.resultDir}`);
|
|
2278
3661
|
io.log(`Summary: ${result.summaryPath}`);
|
|
2279
|
-
if (result.markdownPath) io.log(`Markdown: ${result.markdownPath}`);
|
|
3662
|
+
if (result.markdownPath && 'verbose' === reporter.mode) io.log(`Markdown: ${result.markdownPath}`);
|
|
2280
3663
|
if (result.reportPath) io.log(`Report: ${result.reportPath}`);
|
|
3664
|
+
writeGitHubSummary(result);
|
|
2281
3665
|
return result.exitCode;
|
|
2282
3666
|
};
|
|
2283
3667
|
async function runTestCli(args, io = defaultCliIO) {
|
|
2284
3668
|
try {
|
|
2285
3669
|
if ('model' === args[0]) {
|
|
2286
|
-
const { runModelCommand } = await __webpack_require__.e("
|
|
3670
|
+
const { runModelCommand } = await __webpack_require__.e("731").then(__webpack_require__.bind(__webpack_require__, "./src/cli/model-command.ts?3cff"));
|
|
2287
3671
|
return runModelCommand(args, void 0, {
|
|
2288
3672
|
stdout: io.log,
|
|
2289
3673
|
stderr: io.error
|
|
@@ -2307,18 +3691,32 @@ async function runTestCli(args, io = defaultCliIO) {
|
|
|
2307
3691
|
await runNodesCommand(options, io);
|
|
2308
3692
|
return 0;
|
|
2309
3693
|
}
|
|
2310
|
-
|
|
3694
|
+
const reporter = new TerminalReporter(io, options.reporter);
|
|
3695
|
+
const restoreOutput = io === defaultCliIO && io.isTTY && 'verbose' !== options.reporter ? captureTerminalOutput((message, write)=>reporter.output(message, write)) : void 0;
|
|
3696
|
+
const projectRoot = (0, external_node_fs_.realpathSync)(options.projectRoot && (0, external_node_fs_.existsSync)(options.projectRoot) && (0, external_node_fs_.statSync)(options.projectRoot).isFile() ? (0, external_node_path_.dirname)(options.projectRoot) : options.projectRoot ?? options.cwd);
|
|
2311
3697
|
try {
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
3698
|
+
return await runWithConsoleLogHandler((entry)=>reporter.console(entry), async ()=>{
|
|
3699
|
+
let result;
|
|
3700
|
+
try {
|
|
3701
|
+
result = await runTestProject({
|
|
3702
|
+
...options,
|
|
3703
|
+
onProgress: (message)=>reporter.progress(message),
|
|
3704
|
+
onEvent: (event)=>reporter.event(event)
|
|
3705
|
+
});
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
if (!(error instanceof WorkflowExecutionFailure)) throw error;
|
|
3708
|
+
reportTestProjectResult(error.result, io, reporter, projectRoot, error.errors);
|
|
3709
|
+
return 1;
|
|
3710
|
+
}
|
|
3711
|
+
return reportTestProjectResult(result, io, reporter, projectRoot);
|
|
2315
3712
|
});
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
3713
|
+
} finally{
|
|
3714
|
+
try {
|
|
3715
|
+
reporter.dispose();
|
|
3716
|
+
} finally{
|
|
3717
|
+
restoreOutput?.();
|
|
3718
|
+
}
|
|
2320
3719
|
}
|
|
2321
|
-
return reportTestProjectResult(result, io);
|
|
2322
3720
|
} catch (error) {
|
|
2323
3721
|
io.error(formatCliError(error));
|
|
2324
3722
|
return 1;
|