@midscene/test 1.12.10-beta-20260920031616.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/{1~891.mjs → 1~214.mjs} +6 -6
- package/dist/es/{1~891.mjs.map → 1~214.mjs.map} +1 -1
- package/dist/es/cli/index.mjs +534 -1700
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +57 -1525
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +93 -168
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/runtime/index.mjs +317 -0
- package/dist/es/runtime/index.mjs.map +1 -0
- package/dist/es/test-cli.mjs +666 -1780
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/{0~458.js → 0~160.js} +5 -5
- package/dist/lib/{0~458.js.map → 0~160.js.map} +1 -1
- package/dist/lib/cli/index.js +545 -1711
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +92 -1557
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +130 -172
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/runtime/index.js +372 -0
- package/dist/lib/runtime/index.js.map +1 -0
- package/dist/lib/test-cli.js +670 -1784
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +62 -233
- package/dist/types/index.d.ts +186 -492
- package/dist/types/midscene/index.d.ts +82 -96
- package/dist/types/runtime/index.d.ts +52 -0
- package/package.json +51 -8
package/dist/lib/test-cli.js
CHANGED
|
@@ -14,6 +14,11 @@ var __webpack_modules__ = {
|
|
|
14
14
|
},
|
|
15
15
|
"yargs/yargs" (module) {
|
|
16
16
|
module.exports = require("yargs/yargs");
|
|
17
|
+
},
|
|
18
|
+
"./package.json" (module) {
|
|
19
|
+
module.exports = {
|
|
20
|
+
rE: "1.13.0"
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
24
|
var __webpack_module_cache__ = {};
|
|
@@ -75,6 +80,7 @@ var __webpack_exports__ = {};
|
|
|
75
80
|
(()=>{
|
|
76
81
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
77
82
|
var external_node_path_ = __webpack_require__("node:path");
|
|
83
|
+
var package_0 = __webpack_require__("./package.json");
|
|
78
84
|
const v4_namespaceObject = require("zod/v4");
|
|
79
85
|
const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
|
|
80
86
|
const sortNodesForReference = (nodes)=>{
|
|
@@ -127,7 +133,9 @@ cases:
|
|
|
127
133
|
continue-on-error: false
|
|
128
134
|
\`\`\`
|
|
129
135
|
|
|
130
|
-
A Node may map a string step value to one declared input field. Each Node section states whether it supports this shorthand and names the target field
|
|
136
|
+
A Node may map a string step value to one declared input field. Each Node section states whether it supports this shorthand and names the target field.
|
|
137
|
+
|
|
138
|
+
Each Node's returned \`data\` is saved on that Step's result. Results are not named or automatically passed to later Steps. A failed Case does not stop later Cases by default; \`test.bail\` controls early stopping across the run.`;
|
|
131
139
|
const sortJsonValue = (value)=>{
|
|
132
140
|
if (Array.isArray(value)) return value.map(sortJsonValue);
|
|
133
141
|
if ('object' != typeof value || null === value) return value;
|
|
@@ -208,275 +216,8 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
208
216
|
const external_node_url_namespaceObject = require("node:url");
|
|
209
217
|
const api_namespaceObject = require("tsx/cjs/api");
|
|
210
218
|
const esm_api_namespaceObject = require("tsx/esm/api");
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
value: value,
|
|
214
|
-
enumerable: true,
|
|
215
|
-
configurable: true,
|
|
216
|
-
writable: true
|
|
217
|
-
});
|
|
218
|
-
else obj[key] = value;
|
|
219
|
-
return obj;
|
|
220
|
-
}
|
|
221
|
-
class WorkflowError extends Error {
|
|
222
|
-
toJSON() {
|
|
223
|
-
return {
|
|
224
|
-
name: this.name,
|
|
225
|
-
message: this.message,
|
|
226
|
-
code: this.code,
|
|
227
|
-
...void 0 === this.details ? {} : {
|
|
228
|
-
details: this.details
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
constructor(message, options = {}){
|
|
233
|
-
super(message, {
|
|
234
|
-
cause: options.cause
|
|
235
|
-
}), _define_property(this, "code", void 0), _define_property(this, "details", void 0);
|
|
236
|
-
this.name = new.target.name;
|
|
237
|
-
this.code = options.code ?? 'WORKFLOW_ERROR';
|
|
238
|
-
this.details = options.details;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
class WorkflowParseError extends WorkflowError {
|
|
242
|
-
constructor(message, details, cause){
|
|
243
|
-
super(message, {
|
|
244
|
-
code: 'WORKFLOW_PARSE_ERROR',
|
|
245
|
-
details,
|
|
246
|
-
cause
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
class NodeDefinitionError extends WorkflowError {
|
|
251
|
-
constructor(message, details){
|
|
252
|
-
super(message, {
|
|
253
|
-
code: 'NODE_DEFINITION_ERROR',
|
|
254
|
-
details
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
class DuplicateNodeError extends WorkflowError {
|
|
259
|
-
constructor(node){
|
|
260
|
-
super(`Node "${node}" is already registered.`, {
|
|
261
|
-
code: 'DUPLICATE_NODE',
|
|
262
|
-
details: {
|
|
263
|
-
node
|
|
264
|
-
}
|
|
265
|
-
}), _define_property(this, "node", void 0);
|
|
266
|
-
this.node = node;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
class NodeNotFoundError extends WorkflowError {
|
|
270
|
-
constructor(node){
|
|
271
|
-
super(`Node "${node}" is not registered.`, {
|
|
272
|
-
code: 'NODE_NOT_FOUND',
|
|
273
|
-
details: {
|
|
274
|
-
node
|
|
275
|
-
}
|
|
276
|
-
}), _define_property(this, "node", void 0);
|
|
277
|
-
this.node = node;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
class NodeInputValidationError extends WorkflowError {
|
|
281
|
-
static fromZod(node, error) {
|
|
282
|
-
const issues = error.issues.map((issue)=>({
|
|
283
|
-
code: issue.code,
|
|
284
|
-
path: issue.path.map(String).join('.'),
|
|
285
|
-
message: issue.message
|
|
286
|
-
}));
|
|
287
|
-
const firstIssue = issues[0];
|
|
288
|
-
const path = firstIssue?.path || '<root>';
|
|
289
|
-
const message = firstIssue?.message ?? 'invalid input';
|
|
290
|
-
return new NodeInputValidationError(`Node "${node}" input validation failed at "${path}": ${message}`, {
|
|
291
|
-
node,
|
|
292
|
-
issues
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
constructor(message, details){
|
|
296
|
-
super(message, {
|
|
297
|
-
code: 'NODE_INPUT_VALIDATION_ERROR',
|
|
298
|
-
details
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
class StepTimeoutError extends WorkflowError {
|
|
303
|
-
constructor(timeoutMs, node){
|
|
304
|
-
super(node ? `Node "${node}" timed out after ${timeoutMs}ms.` : `Step timed out after ${timeoutMs}ms.`, {
|
|
305
|
-
code: 'STEP_TIMEOUT',
|
|
306
|
-
details: {
|
|
307
|
-
timeoutMs,
|
|
308
|
-
...void 0 === node ? {} : {
|
|
309
|
-
node
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}), _define_property(this, "timeoutMs", void 0), _define_property(this, "node", void 0);
|
|
313
|
-
this.timeoutMs = timeoutMs;
|
|
314
|
-
this.node = node;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
class NodeExecutionError extends WorkflowError {
|
|
318
|
-
constructor(node, cause){
|
|
319
|
-
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
|
|
320
|
-
super(`Node "${node}" failed: ${causeMessage}`, {
|
|
321
|
-
code: 'NODE_EXECUTION_ERROR',
|
|
322
|
-
details: {
|
|
323
|
-
node
|
|
324
|
-
},
|
|
325
|
-
cause
|
|
326
|
-
}), _define_property(this, "node", void 0);
|
|
327
|
-
this.node = node;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
class WorkflowLifecycleError extends WorkflowError {
|
|
331
|
-
constructor(message, details){
|
|
332
|
-
super(message, {
|
|
333
|
-
code: 'WORKFLOW_LIFECYCLE_ERROR',
|
|
334
|
-
details
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
class ProjectSetupError extends WorkflowError {
|
|
339
|
-
constructor(cause, details){
|
|
340
|
-
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
|
|
341
|
-
super(`Project "${details.projectName}" setup failed: ${causeMessage}`, {
|
|
342
|
-
code: 'PROJECT_SETUP_ERROR',
|
|
343
|
-
details,
|
|
344
|
-
cause
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
class ProjectTeardownError extends WorkflowError {
|
|
349
|
-
constructor(cause, details){
|
|
350
|
-
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
|
|
351
|
-
super(`Project "${details.projectName}" teardown failed: ${causeMessage}`, {
|
|
352
|
-
code: 'PROJECT_TEARDOWN_ERROR',
|
|
353
|
-
details,
|
|
354
|
-
cause
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
class NodeScopeTeardownError extends WorkflowError {
|
|
359
|
-
constructor(cause, details){
|
|
360
|
-
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
|
|
361
|
-
super(`${'case' === details.scope ? 'Case attempt' : 'Workflow document'} node teardown failed for "${details.node}": ${causeMessage}`, {
|
|
362
|
-
code: 'NODE_SCOPE_TEARDOWN_ERROR',
|
|
363
|
-
details,
|
|
364
|
-
cause
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
class FatalDeviceError extends WorkflowError {
|
|
369
|
-
constructor(message, cause){
|
|
370
|
-
super(message, {
|
|
371
|
-
code: 'FATAL_DEVICE_ERROR',
|
|
372
|
-
cause
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const isFatalDeviceError = (error)=>{
|
|
377
|
-
if (error instanceof FatalDeviceError) return true;
|
|
378
|
-
if (error instanceof WorkflowError && 'FATAL_DEVICE_ERROR' === error.code) return true;
|
|
379
|
-
if (error instanceof Error && /device offline|device not found|(?:adb|bdc|device) connection (?:was )?closed/i.test(error.message)) return true;
|
|
380
|
-
return error instanceof Error && void 0 !== error.cause ? isFatalDeviceError(error.cause) : false;
|
|
381
|
-
};
|
|
382
|
-
function normalizeNodeExecutionError(error, node) {
|
|
383
|
-
return error instanceof WorkflowError ? error : new NodeExecutionError(node, error);
|
|
384
|
-
}
|
|
385
|
-
const validateOptionalText = (value, field, node)=>{
|
|
386
|
-
if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
|
|
387
|
-
node,
|
|
388
|
-
field
|
|
389
|
-
});
|
|
390
|
-
};
|
|
391
|
-
const validateInputSchema = (schema, node)=>{
|
|
392
|
-
if (void 0 === schema) return;
|
|
393
|
-
if (!(schema instanceof v4_namespaceObject.z.ZodObject)) throw new NodeDefinitionError(`Node "${node}" inputSchema must be a Zod object schema.`, {
|
|
394
|
-
node,
|
|
395
|
-
field: 'inputSchema'
|
|
396
|
-
});
|
|
397
|
-
if ('$' in schema.shape) throw new NodeDefinitionError(`Node "${node}" inputSchema must not declare "$" as an input property.`, {
|
|
398
|
-
node,
|
|
399
|
-
field: 'inputSchema.$'
|
|
400
|
-
});
|
|
401
|
-
};
|
|
402
|
-
const validateDefinition = (options)=>{
|
|
403
|
-
if (!options || 'object' != typeof options) throw new NodeDefinitionError('Node definition must be an object.');
|
|
404
|
-
if ('string' != typeof options.name || 0 === options.name.trim().length) throw new NodeDefinitionError('Node name must be a non-empty string.');
|
|
405
|
-
validateOptionalText(options.title, 'title', options.name);
|
|
406
|
-
validateOptionalText(options.description, "description", options.name);
|
|
407
|
-
if (void 0 !== options.stringInputKey && false !== options.stringInputKey && ('string' != typeof options.stringInputKey || 0 === options.stringInputKey.trim().length)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must be a non-empty string or false.`, {
|
|
408
|
-
node: options.name,
|
|
409
|
-
field: 'stringInputKey'
|
|
410
|
-
});
|
|
411
|
-
validateInputSchema(options.inputSchema, options.name);
|
|
412
|
-
if ('string' == typeof options.stringInputKey && options.inputSchema instanceof v4_namespaceObject.z.ZodObject && !(options.stringInputKey in options.inputSchema.shape)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must reference a field in inputSchema.`, {
|
|
413
|
-
node: options.name,
|
|
414
|
-
field: 'stringInputKey'
|
|
415
|
-
});
|
|
416
|
-
if ('function' != typeof options.execute) throw new NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
|
|
417
|
-
node: options.name
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
function defineNode(options) {
|
|
421
|
-
validateDefinition(options);
|
|
422
|
-
return options;
|
|
423
|
-
}
|
|
424
|
-
function _check_private_redeclaration(obj, privateCollection) {
|
|
425
|
-
if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
|
|
426
|
-
}
|
|
427
|
-
function _class_apply_descriptor_get(receiver, descriptor) {
|
|
428
|
-
if (descriptor.get) return descriptor.get.call(receiver);
|
|
429
|
-
return descriptor.value;
|
|
430
|
-
}
|
|
431
|
-
function _class_extract_field_descriptor(receiver, privateMap, action) {
|
|
432
|
-
if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
|
|
433
|
-
return privateMap.get(receiver);
|
|
434
|
-
}
|
|
435
|
-
function _class_private_field_get(receiver, privateMap) {
|
|
436
|
-
var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
|
|
437
|
-
return _class_apply_descriptor_get(receiver, descriptor);
|
|
438
|
-
}
|
|
439
|
-
function _class_private_field_init(obj, privateMap, value) {
|
|
440
|
-
_check_private_redeclaration(obj, privateMap);
|
|
441
|
-
privateMap.set(obj, value);
|
|
442
|
-
}
|
|
443
|
-
var _nodes = /*#__PURE__*/ new WeakMap();
|
|
444
|
-
class NodeRegistry {
|
|
445
|
-
register(node) {
|
|
446
|
-
defineNode(node);
|
|
447
|
-
if (_class_private_field_get(this, _nodes).has(node.name)) throw new DuplicateNodeError(node.name);
|
|
448
|
-
_class_private_field_get(this, _nodes).set(node.name, node);
|
|
449
|
-
return this;
|
|
450
|
-
}
|
|
451
|
-
get(name) {
|
|
452
|
-
return _class_private_field_get(this, _nodes).get(name);
|
|
453
|
-
}
|
|
454
|
-
require(name) {
|
|
455
|
-
const node = this.get(name);
|
|
456
|
-
if (!node) throw new NodeNotFoundError(name);
|
|
457
|
-
return node;
|
|
458
|
-
}
|
|
459
|
-
has(name) {
|
|
460
|
-
return _class_private_field_get(this, _nodes).has(name);
|
|
461
|
-
}
|
|
462
|
-
names() {
|
|
463
|
-
return [
|
|
464
|
-
..._class_private_field_get(this, _nodes).keys()
|
|
465
|
-
];
|
|
466
|
-
}
|
|
467
|
-
definitions() {
|
|
468
|
-
return [
|
|
469
|
-
..._class_private_field_get(this, _nodes).values()
|
|
470
|
-
];
|
|
471
|
-
}
|
|
472
|
-
constructor(nodes = []){
|
|
473
|
-
_class_private_field_init(this, _nodes, {
|
|
474
|
-
writable: true,
|
|
475
|
-
value: new Map()
|
|
476
|
-
});
|
|
477
|
-
for (const node of nodes)this.register(node);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
219
|
+
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
220
|
+
const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
|
|
480
221
|
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
481
222
|
const getDefaultExport = (loaded, absolutePath)=>{
|
|
482
223
|
if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
|
|
@@ -511,9 +252,13 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
511
252
|
return pattern;
|
|
512
253
|
});
|
|
513
254
|
};
|
|
514
|
-
const
|
|
255
|
+
const test_project_validateTestFileSelection = (value, label = 'files')=>{
|
|
515
256
|
if (void 0 === value) return;
|
|
516
257
|
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
258
|
+
rejectUnknownKeys(value, [
|
|
259
|
+
'include',
|
|
260
|
+
'exclude'
|
|
261
|
+
], label);
|
|
517
262
|
const include = validatePatterns(value.include, 'include', label);
|
|
518
263
|
const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
|
|
519
264
|
return Object.freeze({
|
|
@@ -569,7 +314,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
569
314
|
assertJsonValue(value, label, new Set());
|
|
570
315
|
return deepFreezeJson(value);
|
|
571
316
|
};
|
|
572
|
-
const
|
|
317
|
+
const validateSetup = (value, label)=>{
|
|
573
318
|
if (void 0 === value) return;
|
|
574
319
|
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
575
320
|
rejectUnknownKeys(value, [
|
|
@@ -592,7 +337,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
592
337
|
};
|
|
593
338
|
const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
594
339
|
if (void 0 === value) {
|
|
595
|
-
const setup =
|
|
340
|
+
const setup = validateSetup(defaultSetup, 'setup');
|
|
596
341
|
return {
|
|
597
342
|
hasExplicitProjects: false,
|
|
598
343
|
projects: Object.freeze([
|
|
@@ -631,7 +376,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
631
376
|
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
632
377
|
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
633
378
|
names.add(candidate.name);
|
|
634
|
-
const files =
|
|
379
|
+
const files = test_project_validateTestFileSelection(candidate.files, `${label}.files`);
|
|
635
380
|
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
636
381
|
const localNodes = new NodeRegistry(candidate.nodes);
|
|
637
382
|
const nodes = new NodeRegistry([
|
|
@@ -649,7 +394,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
649
394
|
retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
|
|
650
395
|
variables: validateVariables(candidate.variables, `${label}.variables`),
|
|
651
396
|
...void 0 === candidate.setup ? {} : {
|
|
652
|
-
setup:
|
|
397
|
+
setup: validateSetup(candidate.setup, `${label}.setup`)
|
|
653
398
|
}
|
|
654
399
|
});
|
|
655
400
|
});
|
|
@@ -744,877 +489,151 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
744
489
|
}
|
|
745
490
|
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
746
491
|
}
|
|
747
|
-
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
748
|
-
const external_node_events_namespaceObject = require("node:events");
|
|
749
|
-
const report_namespaceObject = require("@midscene/core/report");
|
|
750
492
|
const external_tinyglobby_namespaceObject = require("tinyglobby");
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
493
|
+
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
494
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
495
|
+
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
496
|
+
projectId,
|
|
497
|
+
sourcePath,
|
|
498
|
+
caseIndex,
|
|
499
|
+
...invocationIndex ? [
|
|
500
|
+
invocationIndex
|
|
501
|
+
] : []
|
|
502
|
+
])).digest('hex');
|
|
503
|
+
const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
504
|
+
projectId,
|
|
505
|
+
sourcePath,
|
|
506
|
+
...invocationIndex ? [
|
|
507
|
+
invocationIndex
|
|
508
|
+
] : []
|
|
509
|
+
])).digest('hex');
|
|
510
|
+
const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
|
|
511
|
+
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
512
|
+
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
513
|
+
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
514
|
+
const missingVariable = (kind, name, options)=>{
|
|
515
|
+
throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
|
|
516
|
+
...options.location,
|
|
517
|
+
variable: name,
|
|
518
|
+
variableKind: kind
|
|
759
519
|
});
|
|
760
|
-
const teardownStack = [];
|
|
761
|
-
let context;
|
|
762
|
-
let setupError;
|
|
763
|
-
let started = false;
|
|
764
|
-
let acceptingTeardowns = void 0 !== options.setup;
|
|
765
|
-
let finishedResult;
|
|
766
|
-
const createResult = (requestedStatus, teardownErrors = [])=>{
|
|
767
|
-
const endedAt = new Date();
|
|
768
|
-
return {
|
|
769
|
-
projectName: options.project.name,
|
|
770
|
-
status: 'failed' === requestedStatus || setupError || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
771
|
-
...setupError ? {
|
|
772
|
-
setupError
|
|
773
|
-
} : {},
|
|
774
|
-
...teardownErrors.length > 0 ? {
|
|
775
|
-
teardownErrors
|
|
776
|
-
} : {},
|
|
777
|
-
startedAt: startedAt.toISOString(),
|
|
778
|
-
endedAt: endedAt.toISOString(),
|
|
779
|
-
durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
|
|
780
|
-
};
|
|
781
|
-
};
|
|
782
|
-
const onTeardown = (teardown)=>{
|
|
783
|
-
if (!acceptingTeardowns) throw new WorkflowLifecycleError('onTeardown() can only be called while Project setup is running.', {
|
|
784
|
-
projectName: options.project.name
|
|
785
|
-
});
|
|
786
|
-
if ('function' != typeof teardown) throw new WorkflowLifecycleError('Project onTeardown() requires a teardown function.', {
|
|
787
|
-
projectName: options.project.name
|
|
788
|
-
});
|
|
789
|
-
teardownStack.push({
|
|
790
|
-
registrationIndex: teardownStack.length,
|
|
791
|
-
teardown
|
|
792
|
-
});
|
|
793
|
-
};
|
|
794
|
-
return {
|
|
795
|
-
get context () {
|
|
796
|
-
return context;
|
|
797
|
-
},
|
|
798
|
-
get signal () {
|
|
799
|
-
return controller.signal;
|
|
800
|
-
},
|
|
801
|
-
get canRun () {
|
|
802
|
-
return started && !setupError && !controller.signal.aborted;
|
|
803
|
-
},
|
|
804
|
-
async start () {
|
|
805
|
-
if (started) throw new WorkflowLifecycleError('Project runtime has already started.', {
|
|
806
|
-
projectName: options.project.name
|
|
807
|
-
});
|
|
808
|
-
started = true;
|
|
809
|
-
if (controller.signal.aborted) {
|
|
810
|
-
acceptingTeardowns = false;
|
|
811
|
-
return createResult('failed');
|
|
812
|
-
}
|
|
813
|
-
if (options.setup) try {
|
|
814
|
-
context = await options.setup.setup({
|
|
815
|
-
project: options.project,
|
|
816
|
-
env: Object.freeze({
|
|
817
|
-
...process.env
|
|
818
|
-
}),
|
|
819
|
-
signal: controller.signal,
|
|
820
|
-
onTeardown
|
|
821
|
-
});
|
|
822
|
-
} catch (error) {
|
|
823
|
-
setupError = new ProjectSetupError(error, {
|
|
824
|
-
projectName: options.project.name
|
|
825
|
-
});
|
|
826
|
-
} finally{
|
|
827
|
-
acceptingTeardowns = false;
|
|
828
|
-
}
|
|
829
|
-
return createResult(setupError ? 'failed' : 'success');
|
|
830
|
-
},
|
|
831
|
-
async finish (requestedStatus = 'success') {
|
|
832
|
-
if (finishedResult) return finishedResult;
|
|
833
|
-
if (!started) throw new WorkflowLifecycleError('Project runtime must start before it can finish.', {
|
|
834
|
-
projectName: options.project.name
|
|
835
|
-
});
|
|
836
|
-
const statusBeforeTeardown = createResult(requestedStatus).status;
|
|
837
|
-
const teardownErrors = [];
|
|
838
|
-
for (const { registrationIndex, teardown } of teardownStack.reverse())try {
|
|
839
|
-
await teardown({
|
|
840
|
-
project: options.project,
|
|
841
|
-
context,
|
|
842
|
-
status: statusBeforeTeardown,
|
|
843
|
-
...setupError ? {
|
|
844
|
-
setupError
|
|
845
|
-
} : {}
|
|
846
|
-
});
|
|
847
|
-
} catch (error) {
|
|
848
|
-
teardownErrors.push(new ProjectTeardownError(error, {
|
|
849
|
-
projectName: options.project.name,
|
|
850
|
-
registrationIndex
|
|
851
|
-
}));
|
|
852
|
-
}
|
|
853
|
-
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
854
|
-
finishedResult = createResult(requestedStatus, teardownErrors);
|
|
855
|
-
return finishedResult;
|
|
856
|
-
},
|
|
857
|
-
abort (reason) {
|
|
858
|
-
controller.abort(reason);
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
520
|
};
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if ('number' == typeof candidate) {
|
|
866
|
-
if (Number.isFinite(candidate)) return;
|
|
867
|
-
throw new TypeError(`${path} contains a non-finite number.`);
|
|
868
|
-
}
|
|
869
|
-
if (void 0 === candidate) throw new TypeError(`${path} contains undefined.`);
|
|
870
|
-
if ('object' != typeof candidate) throw new TypeError(`${path} is not JSON-serializable.`);
|
|
871
|
-
if (!Array.isArray(candidate) && Object.getPrototypeOf(candidate) !== Object.prototype && null !== Object.getPrototypeOf(candidate)) throw new TypeError(`${path} must contain only plain objects.`);
|
|
872
|
-
if (seen.has(candidate)) throw new TypeError(`${path} contains a cycle.`);
|
|
873
|
-
seen.add(candidate);
|
|
874
|
-
if (Array.isArray(candidate)) candidate.forEach((child, index)=>visit(child, `${path}[${index}]`, seen));
|
|
875
|
-
else for (const [key, child] of Object.entries(candidate))visit(child, `${path}.${key}`, seen);
|
|
876
|
-
seen.delete(candidate);
|
|
877
|
-
};
|
|
878
|
-
try {
|
|
879
|
-
visit(value, label, new Set());
|
|
880
|
-
} catch (error) {
|
|
881
|
-
throw new NodeExecutionError(node, error);
|
|
882
|
-
}
|
|
521
|
+
const projectVariable = (name, options)=>{
|
|
522
|
+
if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
|
|
523
|
+
return options.variables[name];
|
|
883
524
|
};
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
node: step.node,
|
|
895
|
-
phase,
|
|
896
|
-
stepIndex,
|
|
897
|
-
input: step.input,
|
|
898
|
-
meta: step.meta,
|
|
899
|
-
startedAt: startedAt.toISOString(),
|
|
900
|
-
endedAt: endedAt.toISOString(),
|
|
901
|
-
durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
async function executeNode(step, execute, phase, stepIndex, options) {
|
|
905
|
-
const startedAt = new Date();
|
|
906
|
-
const abortController = new AbortController();
|
|
907
|
-
const parentSignal = options.parentSignal;
|
|
908
|
-
const abortFromParent = ()=>abortController.abort(parentSignal?.reason);
|
|
909
|
-
if (parentSignal?.aborted) abortFromParent();
|
|
910
|
-
else parentSignal?.addEventListener('abort', abortFromParent, {
|
|
911
|
-
once: true
|
|
912
|
-
});
|
|
913
|
-
const timeoutMs = step.meta.timeoutMs ?? options.defaultTimeoutMs;
|
|
914
|
-
let timeout;
|
|
915
|
-
let abortGraceTimeout;
|
|
916
|
-
const execution = Promise.resolve().then(()=>{
|
|
917
|
-
abortController.signal.throwIfAborted();
|
|
918
|
-
return execute(abortController.signal);
|
|
525
|
+
const environmentVariable = (name, options)=>{
|
|
526
|
+
const value = options.env?.[name];
|
|
527
|
+
if (void 0 === value) return missingVariable('environment', name, options);
|
|
528
|
+
return value;
|
|
529
|
+
};
|
|
530
|
+
const interpolationValue = (name, value, options)=>{
|
|
531
|
+
if ('object' == typeof value && null !== value) throw new test_runner_namespaceObject.WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
|
|
532
|
+
...options.location,
|
|
533
|
+
variable: name,
|
|
534
|
+
variableKind: 'project'
|
|
919
535
|
});
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
536
|
+
return String(value);
|
|
537
|
+
};
|
|
538
|
+
const resolveString = (value, options)=>{
|
|
539
|
+
const exactEnv = exactEnvExpression.exec(value);
|
|
540
|
+
if (exactEnv) return environmentVariable(exactEnv[1], options);
|
|
541
|
+
const exactProject = exactProjectExpression.exec(value);
|
|
542
|
+
if (exactProject) return projectVariable(exactProject[1], options);
|
|
543
|
+
return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
|
|
544
|
+
};
|
|
545
|
+
const resolveWorkflowVariables = (value, options)=>{
|
|
546
|
+
if ('string' == typeof value) return resolveString(value, options);
|
|
547
|
+
if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
|
|
548
|
+
if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
|
|
549
|
+
key,
|
|
550
|
+
resolveWorkflowVariables(child, options)
|
|
551
|
+
]));
|
|
552
|
+
return value;
|
|
553
|
+
};
|
|
554
|
+
function _define_property(obj, key, value) {
|
|
555
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
556
|
+
value: value,
|
|
557
|
+
enumerable: true,
|
|
558
|
+
configurable: true,
|
|
559
|
+
writable: true
|
|
931
560
|
});
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
561
|
+
else obj[key] = value;
|
|
562
|
+
return obj;
|
|
563
|
+
}
|
|
564
|
+
const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
565
|
+
class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
|
|
566
|
+
constructor(actualFormat, sourcePath){
|
|
567
|
+
super('legacy' === actualFormat ? `Legacy tasks/flow YAML is not supported by midscene-test: ${sourcePath}. Run it with \`midscene ${sourcePath}\`, or migrate the file to cases/steps.` : 'legacy-batch' === actualFormat ? `Legacy YAML batch configuration is not a midscene-test workflow document: ${sourcePath}. Run it with \`midscene --config ${sourcePath}\`, or migrate it to a native project config.` : `A workflow file cannot mix legacy tasks/flow with native cases/steps or lifecycle hooks: ${sourcePath}. Keep the formats in separate files and run each file with its matching command.`, {
|
|
568
|
+
sourcePath,
|
|
569
|
+
actualFormat,
|
|
570
|
+
expectedFormat: 'native'
|
|
571
|
+
}), _define_property(this, "actualFormat", void 0);
|
|
572
|
+
this.actualFormat = actualFormat;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
576
|
+
const allowedSet = new Set(allowed);
|
|
577
|
+
const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
|
|
578
|
+
if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
|
|
579
|
+
field: unknown[0]
|
|
939
580
|
});
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
]
|
|
946
|
-
]);
|
|
581
|
+
};
|
|
582
|
+
const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
|
|
583
|
+
const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
|
|
584
|
+
function collectWorkflowDocument(source, options) {
|
|
585
|
+
let parsed;
|
|
947
586
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
status: 'success',
|
|
952
|
-
continuedAfterError: false,
|
|
953
|
-
...void 0 === output ? {} : {
|
|
954
|
-
output
|
|
955
|
-
}
|
|
956
|
-
};
|
|
587
|
+
parsed = (0, external_js_yaml_namespaceObject.load)((0, external_node_fs_.readFileSync)(source.absolutePath, 'utf8'), {
|
|
588
|
+
schema: external_js_yaml_namespaceObject.JSON_SCHEMA
|
|
589
|
+
});
|
|
957
590
|
} catch (error) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
continuedAfterError: step.meta.continueOnError,
|
|
962
|
-
error: normalizeNodeExecutionError(error, step.node)
|
|
963
|
-
};
|
|
964
|
-
} finally{
|
|
965
|
-
if (void 0 !== timeout) clearTimeout(timeout);
|
|
966
|
-
if (void 0 !== abortGraceTimeout) clearTimeout(abortGraceTimeout);
|
|
967
|
-
if (void 0 !== settleOnAbort) abortController.signal.removeEventListener('abort', settleOnAbort);
|
|
968
|
-
parentSignal?.removeEventListener('abort', abortFromParent);
|
|
591
|
+
throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
592
|
+
sourcePath: source.sourcePath
|
|
593
|
+
}, error);
|
|
969
594
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
async function parseNodeInput(node, input) {
|
|
996
|
-
if (!node.inputSchema) return input;
|
|
997
|
-
const parsed = await node.inputSchema.safeParseAsync(input);
|
|
998
|
-
if (!parsed.success) throw NodeInputValidationError.fromZod(node.name, parsed.error);
|
|
999
|
-
return parsed.data;
|
|
1000
|
-
}
|
|
1001
|
-
async function executeStep(step, node, target, context, execution = {}) {
|
|
1002
|
-
const phase = 'case' === target.scope ? target.case.phase : target.document.phase;
|
|
1003
|
-
const stepIndex = 'case' === target.scope ? target.case.stepIndex : target.document.stepIndex;
|
|
1004
|
-
const reportCollector = createNodeReportCollector(step.node);
|
|
1005
|
-
let result;
|
|
1006
|
-
try {
|
|
1007
|
-
result = await executeNode(step, async (signal)=>{
|
|
1008
|
-
const input = await parseNodeInput(node, step.input);
|
|
1009
|
-
signal.throwIfAborted();
|
|
1010
|
-
const common = {
|
|
1011
|
-
input,
|
|
1012
|
-
$: step.meta,
|
|
1013
|
-
signal,
|
|
1014
|
-
context,
|
|
1015
|
-
report: reportCollector.collector,
|
|
1016
|
-
onTeardown: (teardown)=>{
|
|
1017
|
-
if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
|
|
1018
|
-
execution.onTeardown(step.node, teardown);
|
|
595
|
+
if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
|
|
596
|
+
const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
|
|
597
|
+
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
598
|
+
collect_rejectUnknownKeys(parsed, [
|
|
599
|
+
'beforeAll',
|
|
600
|
+
'beforeEach',
|
|
601
|
+
'cases',
|
|
602
|
+
'afterEach',
|
|
603
|
+
'afterAll'
|
|
604
|
+
], 'Workflow document');
|
|
605
|
+
if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
|
|
606
|
+
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
607
|
+
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
608
|
+
...step,
|
|
609
|
+
input: resolveWorkflowVariables(step.input, {
|
|
610
|
+
variables: options.variables,
|
|
611
|
+
env: options.env ?? process.env,
|
|
612
|
+
location: {
|
|
613
|
+
projectName: source.projectName ?? source.projectId,
|
|
614
|
+
sourcePath,
|
|
615
|
+
phase,
|
|
616
|
+
stepIndex,
|
|
617
|
+
...void 0 === caseIndex ? {} : {
|
|
618
|
+
caseIndex
|
|
619
|
+
}
|
|
1019
620
|
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
621
|
+
})
|
|
622
|
+
});
|
|
623
|
+
const normalizeLifecycle = (phase)=>{
|
|
624
|
+
const value = parsed[phase];
|
|
625
|
+
if (void 0 === value) return [];
|
|
626
|
+
if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
|
|
627
|
+
phase
|
|
628
|
+
});
|
|
629
|
+
return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
|
|
630
|
+
const resolved = options.resolveNode(normalized.node);
|
|
631
|
+
if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
632
|
+
phase,
|
|
633
|
+
stepIndex,
|
|
634
|
+
node: normalized.node
|
|
1029
635
|
});
|
|
1030
|
-
|
|
1031
|
-
parentSignal: execution.signal,
|
|
1032
|
-
defaultTimeoutMs: execution.defaultTimeoutMs
|
|
1033
|
-
});
|
|
1034
|
-
} finally{
|
|
1035
|
-
const traces = reportCollector.close();
|
|
1036
|
-
if (traces.length > 0 && result) result = {
|
|
1037
|
-
...result,
|
|
1038
|
-
report: {
|
|
1039
|
-
traces
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
if (!result) throw new NodeExecutionError(step.node, new Error('Step execution completed without a result.'));
|
|
1044
|
-
return result;
|
|
1045
|
-
}
|
|
1046
|
-
const reportPathsFromTeardown = (result)=>{
|
|
1047
|
-
if (void 0 === result) return [];
|
|
1048
|
-
if ('object' != typeof result || null === result || Array.isArray(result)) throw new TypeError('Node teardown result must be an object or undefined.');
|
|
1049
|
-
const paths = result.reportPaths;
|
|
1050
|
-
if (void 0 === paths) return [];
|
|
1051
|
-
if (!Array.isArray(paths)) throw new TypeError('Node teardown reportPaths must be an array.');
|
|
1052
|
-
return paths.map((path, index)=>{
|
|
1053
|
-
if ('string' != typeof path || 0 === path.trim().length) throw new TypeError(`Node teardown reportPaths[${index}] must be a non-empty string.`);
|
|
1054
|
-
if (!(0, external_node_path_.isAbsolute)(path)) throw new TypeError(`Node teardown reportPaths[${index}] must be an absolute path.`);
|
|
1055
|
-
return path;
|
|
1056
|
-
});
|
|
1057
|
-
};
|
|
1058
|
-
function createDocumentRuntime(document, options) {
|
|
1059
|
-
const documentRunId = options.createDocumentRunId?.() ?? (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1060
|
-
const startedAt = new Date();
|
|
1061
|
-
const beforeAll = [];
|
|
1062
|
-
const afterAll = [];
|
|
1063
|
-
const nodes = {
|
|
1064
|
-
beforeAll: document.lifecycle.beforeAll.map((step)=>options.resolveNode(step.node)),
|
|
1065
|
-
afterAll: document.lifecycle.afterAll.map((step)=>options.resolveNode(step.node))
|
|
1066
|
-
};
|
|
1067
|
-
const project = options.project ?? Object.freeze({
|
|
1068
|
-
projectId: document.projectId,
|
|
1069
|
-
name: document.projectId,
|
|
1070
|
-
tags: Object.freeze({
|
|
1071
|
-
include: [],
|
|
1072
|
-
exclude: []
|
|
1073
|
-
}),
|
|
1074
|
-
retry: 0,
|
|
1075
|
-
variables: Object.freeze({})
|
|
1076
|
-
});
|
|
1077
|
-
const context = options.projectContext;
|
|
1078
|
-
const signal = options.signal ?? new AbortController().signal;
|
|
1079
|
-
const nodeTeardownStack = [];
|
|
1080
|
-
let acceptingNodeTeardowns = true;
|
|
1081
|
-
let started = false;
|
|
1082
|
-
let finishedResult;
|
|
1083
|
-
const reportPaths = new Set();
|
|
1084
|
-
const createResult = (teardownErrors = [])=>{
|
|
1085
|
-
const endedAt = new Date();
|
|
1086
|
-
return {
|
|
1087
|
-
documentId: document.documentId,
|
|
1088
|
-
documentRunId,
|
|
1089
|
-
projectId: document.projectId,
|
|
1090
|
-
projectName: project.name,
|
|
1091
|
-
sourcePath: document.sourcePath,
|
|
1092
|
-
status: beforeAll.some((step)=>'failed' === step.status) || afterAll.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
1093
|
-
startedAt: startedAt.toISOString(),
|
|
1094
|
-
endedAt: endedAt.toISOString(),
|
|
1095
|
-
durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
|
|
1096
|
-
beforeAll: [
|
|
1097
|
-
...beforeAll
|
|
1098
|
-
],
|
|
1099
|
-
afterAll: [
|
|
1100
|
-
...afterAll
|
|
1101
|
-
],
|
|
1102
|
-
...teardownErrors.length > 0 ? {
|
|
1103
|
-
teardownErrors
|
|
1104
|
-
} : {},
|
|
1105
|
-
...reportPaths.size > 0 ? {
|
|
1106
|
-
reportPaths: [
|
|
1107
|
-
...reportPaths
|
|
1108
|
-
]
|
|
1109
|
-
} : {}
|
|
1110
|
-
};
|
|
1111
|
-
};
|
|
1112
|
-
const runPhase = async (phase, results, executionSignal = signal)=>{
|
|
1113
|
-
for (const [stepIndex, step] of document.lifecycle[phase].entries()){
|
|
1114
|
-
const documentContext = {
|
|
1115
|
-
documentId: document.documentId,
|
|
1116
|
-
documentRunId,
|
|
1117
|
-
projectId: document.projectId,
|
|
1118
|
-
projectName: project.name,
|
|
1119
|
-
sourcePath: document.sourcePath,
|
|
1120
|
-
phase,
|
|
1121
|
-
stepIndex
|
|
1122
|
-
};
|
|
1123
|
-
const stepInfo = {
|
|
1124
|
-
scope: 'document',
|
|
1125
|
-
node: step.node,
|
|
1126
|
-
stepCount: document.lifecycle[phase].length,
|
|
1127
|
-
document: documentContext
|
|
1128
|
-
};
|
|
1129
|
-
await options.onStepStart?.(stepInfo);
|
|
1130
|
-
const result = await executeStep(step, nodes[phase][stepIndex], {
|
|
1131
|
-
scope: 'document',
|
|
1132
|
-
document: documentContext
|
|
1133
|
-
}, context, {
|
|
1134
|
-
signal: executionSignal,
|
|
1135
|
-
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
1136
|
-
onTeardown (node, teardown) {
|
|
1137
|
-
if (!acceptingNodeTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a workflow document is running.', {
|
|
1138
|
-
documentId: document.documentId,
|
|
1139
|
-
documentRunId,
|
|
1140
|
-
node
|
|
1141
|
-
});
|
|
1142
|
-
if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
|
|
1143
|
-
documentId: document.documentId,
|
|
1144
|
-
documentRunId,
|
|
1145
|
-
node
|
|
1146
|
-
});
|
|
1147
|
-
nodeTeardownStack.push({
|
|
1148
|
-
registrationIndex: nodeTeardownStack.length,
|
|
1149
|
-
node,
|
|
1150
|
-
teardown
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
});
|
|
1154
|
-
await options.onStepResult?.(stepInfo, result);
|
|
1155
|
-
results.push(result);
|
|
1156
|
-
if ('failed' === result.status && !result.continuedAfterError) break;
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
return {
|
|
1160
|
-
get context () {
|
|
1161
|
-
return context;
|
|
1162
|
-
},
|
|
1163
|
-
get canRunCases () {
|
|
1164
|
-
return started && !signal.aborted && !beforeAll.some((step)=>'failed' === step.status);
|
|
1165
|
-
},
|
|
1166
|
-
async start () {
|
|
1167
|
-
if (started) throw new WorkflowLifecycleError('Workflow document runtime has already started.', {
|
|
1168
|
-
documentId: document.documentId,
|
|
1169
|
-
documentRunId
|
|
1170
|
-
});
|
|
1171
|
-
started = true;
|
|
1172
|
-
if (!signal.aborted) await runPhase('beforeAll', beforeAll);
|
|
1173
|
-
return createResult();
|
|
1174
|
-
},
|
|
1175
|
-
async finish () {
|
|
1176
|
-
if (finishedResult) return finishedResult;
|
|
1177
|
-
if (!started) throw new WorkflowLifecycleError('Workflow document runtime must start before it can finish.', {
|
|
1178
|
-
documentId: document.documentId,
|
|
1179
|
-
documentRunId
|
|
1180
|
-
});
|
|
1181
|
-
let finishError;
|
|
1182
|
-
try {
|
|
1183
|
-
await runPhase('afterAll', afterAll, signal.aborted ? new AbortController().signal : signal);
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
finishError = error;
|
|
1186
|
-
}
|
|
1187
|
-
acceptingNodeTeardowns = false;
|
|
1188
|
-
const teardownErrors = [];
|
|
1189
|
-
for (const { registrationIndex, node, teardown } of nodeTeardownStack.reverse())try {
|
|
1190
|
-
for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
|
|
1191
|
-
} catch (error) {
|
|
1192
|
-
teardownErrors.push(new NodeScopeTeardownError(error, {
|
|
1193
|
-
scope: 'document',
|
|
1194
|
-
scopeId: documentRunId,
|
|
1195
|
-
node,
|
|
1196
|
-
registrationIndex
|
|
1197
|
-
}));
|
|
1198
|
-
}
|
|
1199
|
-
finishedResult = createResult(teardownErrors);
|
|
1200
|
-
if (finishError) throw finishError;
|
|
1201
|
-
await options.onResult?.(finishedResult);
|
|
1202
|
-
return finishedResult;
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
async function runCollectedCase(collectedCase, options) {
|
|
1207
|
-
const phases = {
|
|
1208
|
-
beforeEach: options.beforeEach ?? [],
|
|
1209
|
-
steps: collectedCase.definition.steps,
|
|
1210
|
-
afterEach: options.afterEach ?? []
|
|
1211
|
-
};
|
|
1212
|
-
const nodes = {
|
|
1213
|
-
beforeEach: phases.beforeEach.map((step)=>options.resolveNode(step.node)),
|
|
1214
|
-
steps: phases.steps.map((step)=>options.resolveNode(step.node)),
|
|
1215
|
-
afterEach: phases.afterEach.map((step)=>options.resolveNode(step.node))
|
|
1216
|
-
};
|
|
1217
|
-
const runId = options.createRunId?.() ?? (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1218
|
-
const startedAt = new Date();
|
|
1219
|
-
const beforeEach = [];
|
|
1220
|
-
const steps = [];
|
|
1221
|
-
const afterEach = [];
|
|
1222
|
-
const teardownStack = [];
|
|
1223
|
-
let acceptingTeardowns = true;
|
|
1224
|
-
const onTeardown = (node, teardown)=>{
|
|
1225
|
-
if (!acceptingTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a case attempt is running.', {
|
|
1226
|
-
runId,
|
|
1227
|
-
node
|
|
1228
|
-
});
|
|
1229
|
-
if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
|
|
1230
|
-
runId,
|
|
1231
|
-
node
|
|
1232
|
-
});
|
|
1233
|
-
teardownStack.push({
|
|
1234
|
-
registrationIndex: teardownStack.length,
|
|
1235
|
-
node,
|
|
1236
|
-
teardown
|
|
1237
|
-
});
|
|
1238
|
-
};
|
|
1239
|
-
const runPhase = async (phase, results, signal = options.signal)=>{
|
|
1240
|
-
for (const [stepIndex, step] of phases[phase].entries()){
|
|
1241
|
-
const caseContext = {
|
|
1242
|
-
caseId: collectedCase.caseId,
|
|
1243
|
-
runId,
|
|
1244
|
-
projectName: options.projectName ?? collectedCase.projectId,
|
|
1245
|
-
attemptIndex: options.attemptIndex ?? 0,
|
|
1246
|
-
name: collectedCase.definition.name,
|
|
1247
|
-
sourcePath: collectedCase.sourcePath,
|
|
1248
|
-
caseIndex: collectedCase.caseIndex,
|
|
1249
|
-
phase,
|
|
1250
|
-
stepIndex
|
|
1251
|
-
};
|
|
1252
|
-
const stepInfo = {
|
|
1253
|
-
scope: 'case',
|
|
1254
|
-
node: step.node,
|
|
1255
|
-
stepCount: phases[phase].length,
|
|
1256
|
-
case: caseContext
|
|
1257
|
-
};
|
|
1258
|
-
await options.onStepStart?.(stepInfo);
|
|
1259
|
-
const result = await executeStep(step, nodes[phase][stepIndex], {
|
|
1260
|
-
scope: 'case',
|
|
1261
|
-
case: caseContext
|
|
1262
|
-
}, options.context, {
|
|
1263
|
-
signal,
|
|
1264
|
-
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
1265
|
-
onTeardown
|
|
1266
|
-
});
|
|
1267
|
-
await options.onStepResult?.(stepInfo, result);
|
|
1268
|
-
results.push(result);
|
|
1269
|
-
if ('failed' === result.status && !result.continuedAfterError) break;
|
|
1270
|
-
}
|
|
1271
|
-
};
|
|
1272
|
-
let executionError;
|
|
1273
|
-
try {
|
|
1274
|
-
await runPhase('beforeEach', beforeEach);
|
|
1275
|
-
if (!beforeEach.some((step)=>'failed' === step.status)) await runPhase('steps', steps);
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
executionError = error;
|
|
1278
|
-
}
|
|
1279
|
-
try {
|
|
1280
|
-
const cleanupSignal = options.signal?.aborted ? new AbortController().signal : options.signal;
|
|
1281
|
-
await runPhase('afterEach', afterEach, cleanupSignal);
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
executionError ??= error;
|
|
1284
|
-
}
|
|
1285
|
-
acceptingTeardowns = false;
|
|
1286
|
-
const teardownErrors = [];
|
|
1287
|
-
const reportPaths = new Set();
|
|
1288
|
-
for (const { registrationIndex, node, teardown } of teardownStack.reverse())try {
|
|
1289
|
-
for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
teardownErrors.push(new NodeScopeTeardownError(error, {
|
|
1292
|
-
scope: 'case',
|
|
1293
|
-
scopeId: runId,
|
|
1294
|
-
node,
|
|
1295
|
-
registrationIndex
|
|
1296
|
-
}));
|
|
1297
|
-
}
|
|
1298
|
-
if (executionError) throw executionError;
|
|
1299
|
-
const endedAt = new Date();
|
|
1300
|
-
const result = {
|
|
1301
|
-
caseId: collectedCase.caseId,
|
|
1302
|
-
runId,
|
|
1303
|
-
projectName: options.projectName ?? collectedCase.projectId,
|
|
1304
|
-
attemptIndex: options.attemptIndex ?? 0,
|
|
1305
|
-
name: collectedCase.definition.name,
|
|
1306
|
-
sourcePath: collectedCase.sourcePath,
|
|
1307
|
-
caseIndex: collectedCase.caseIndex,
|
|
1308
|
-
status: beforeEach.some((step)=>'failed' === step.status) || steps.some((step)=>'failed' === step.status) || afterEach.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
1309
|
-
beforeEach,
|
|
1310
|
-
steps,
|
|
1311
|
-
afterEach,
|
|
1312
|
-
...teardownErrors.length > 0 ? {
|
|
1313
|
-
teardownErrors
|
|
1314
|
-
} : {},
|
|
1315
|
-
...reportPaths.size > 0 ? {
|
|
1316
|
-
reportPaths: [
|
|
1317
|
-
...reportPaths
|
|
1318
|
-
]
|
|
1319
|
-
} : {},
|
|
1320
|
-
startedAt: startedAt.toISOString(),
|
|
1321
|
-
endedAt: endedAt.toISOString(),
|
|
1322
|
-
durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
|
|
1323
|
-
};
|
|
1324
|
-
await options.onResult?.(result);
|
|
1325
|
-
return result;
|
|
1326
|
-
}
|
|
1327
|
-
const asNotRun = (collectedCase, projectName, reason)=>({
|
|
1328
|
-
caseId: collectedCase.caseId,
|
|
1329
|
-
projectName,
|
|
1330
|
-
name: collectedCase.definition.name,
|
|
1331
|
-
sourcePath: collectedCase.sourcePath,
|
|
1332
|
-
caseIndex: collectedCase.caseIndex,
|
|
1333
|
-
status: 'not-run',
|
|
1334
|
-
notRunReason: reason
|
|
1335
|
-
});
|
|
1336
|
-
async function runWorkflowDocument(document, options) {
|
|
1337
|
-
const createDocumentRunId = options.createDocumentRunId;
|
|
1338
|
-
const createCaseRunId = options.createCaseRunId;
|
|
1339
|
-
const projectName = options.project?.name ?? document.projectId;
|
|
1340
|
-
const retry = options.retry ?? options.project?.retry ?? 0;
|
|
1341
|
-
if (!Number.isInteger(retry) || retry < 0) throw new TypeError('Workflow document retry must be a non-negative integer.');
|
|
1342
|
-
const runtime = createDocumentRuntime(document, {
|
|
1343
|
-
resolveNode: options.resolveNode,
|
|
1344
|
-
project: options.project,
|
|
1345
|
-
projectContext: options.projectContext,
|
|
1346
|
-
signal: options.signal,
|
|
1347
|
-
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
1348
|
-
onStepStart: options.onStepStart,
|
|
1349
|
-
onStepResult: options.onStepResult,
|
|
1350
|
-
onResult: options.onDocumentResult,
|
|
1351
|
-
createDocumentRunId: createDocumentRunId ? ()=>createDocumentRunId(document) : void 0
|
|
1352
|
-
});
|
|
1353
|
-
const cases = [];
|
|
1354
|
-
let runtimeStarted = false;
|
|
1355
|
-
let documentResult;
|
|
1356
|
-
let executionError;
|
|
1357
|
-
let fatalError = false;
|
|
1358
|
-
try {
|
|
1359
|
-
runtimeStarted = true;
|
|
1360
|
-
await runtime.start();
|
|
1361
|
-
if (runtime.canRunCases) for (const collectedCase of document.cases){
|
|
1362
|
-
if (fatalError || options.shouldStop?.()) {
|
|
1363
|
-
const outcome = asNotRun(collectedCase, projectName, fatalError ? 'fatal-error' : options.stopReason?.() ?? 'interrupted');
|
|
1364
|
-
cases.push(outcome);
|
|
1365
|
-
await options.onCaseOutcome?.(outcome);
|
|
1366
|
-
continue;
|
|
1367
|
-
}
|
|
1368
|
-
await options.onCaseStart?.(collectedCase);
|
|
1369
|
-
const attempts = [];
|
|
1370
|
-
for(let attemptIndex = 0; attemptIndex <= retry; attemptIndex += 1){
|
|
1371
|
-
const run = await runCollectedCase(collectedCase, {
|
|
1372
|
-
resolveNode: options.resolveNode,
|
|
1373
|
-
beforeEach: document.lifecycle.beforeEach,
|
|
1374
|
-
afterEach: document.lifecycle.afterEach,
|
|
1375
|
-
context: runtime.context,
|
|
1376
|
-
projectName,
|
|
1377
|
-
attemptIndex,
|
|
1378
|
-
signal: options.signal,
|
|
1379
|
-
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
1380
|
-
onStepStart: options.onStepStart,
|
|
1381
|
-
onStepResult: options.onStepResult,
|
|
1382
|
-
onResult: options.onCaseResult,
|
|
1383
|
-
createRunId: createCaseRunId ? ()=>createCaseRunId(collectedCase, attemptIndex) : void 0
|
|
1384
|
-
});
|
|
1385
|
-
attempts.push(run);
|
|
1386
|
-
fatalError = options.isFatalError?.(run) ?? false;
|
|
1387
|
-
if ('success' === run.status || fatalError) break;
|
|
1388
|
-
}
|
|
1389
|
-
const run = attempts.at(-1);
|
|
1390
|
-
const outcome = {
|
|
1391
|
-
caseId: collectedCase.caseId,
|
|
1392
|
-
projectName,
|
|
1393
|
-
name: collectedCase.definition.name,
|
|
1394
|
-
sourcePath: collectedCase.sourcePath,
|
|
1395
|
-
caseIndex: collectedCase.caseIndex,
|
|
1396
|
-
status: run.status,
|
|
1397
|
-
run,
|
|
1398
|
-
attempts
|
|
1399
|
-
};
|
|
1400
|
-
cases.push(outcome);
|
|
1401
|
-
await options.onCaseOutcome?.(outcome);
|
|
1402
|
-
}
|
|
1403
|
-
else {
|
|
1404
|
-
const reason = options.signal?.aborted ? options.stopReason?.() ?? 'interrupted' : 'document-start-failed';
|
|
1405
|
-
cases.push(...document.cases.map((collectedCase)=>asNotRun(collectedCase, projectName, reason)));
|
|
1406
|
-
}
|
|
1407
|
-
} catch (error) {
|
|
1408
|
-
executionError = error;
|
|
1409
|
-
} finally{
|
|
1410
|
-
if (runtimeStarted) try {
|
|
1411
|
-
documentResult = await runtime.finish();
|
|
1412
|
-
} catch (error) {
|
|
1413
|
-
executionError ??= error;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
if (executionError) throw executionError;
|
|
1417
|
-
if (!documentResult) throw new Error(`Workflow document "${document.sourcePath}" did not produce a result.`);
|
|
1418
|
-
return {
|
|
1419
|
-
document: documentResult,
|
|
1420
|
-
cases
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
1424
|
-
const supportedMetaKeys = new Set([
|
|
1425
|
-
'timeout',
|
|
1426
|
-
'continue-on-error'
|
|
1427
|
-
]);
|
|
1428
|
-
function isMapping(value) {
|
|
1429
|
-
return 'object' == typeof value && null !== value && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || null === Object.getPrototypeOf(value));
|
|
1430
|
-
}
|
|
1431
|
-
function formatStep(index) {
|
|
1432
|
-
return `step ${index + 1}`;
|
|
1433
|
-
}
|
|
1434
|
-
function normalizeMeta(value, index) {
|
|
1435
|
-
if (void 0 === value) return {
|
|
1436
|
-
continueOnError: false
|
|
1437
|
-
};
|
|
1438
|
-
if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} "$" must be a mapping.`, {
|
|
1439
|
-
index
|
|
1440
|
-
});
|
|
1441
|
-
for (const key of Object.keys(value))if (!supportedMetaKeys.has(key)) throw new WorkflowParseError(`${formatStep(index)} has unsupported engine metadata "${key}".`, {
|
|
1442
|
-
index,
|
|
1443
|
-
key
|
|
1444
|
-
});
|
|
1445
|
-
const timeout = value.timeout;
|
|
1446
|
-
if (void 0 !== timeout && ('number' != typeof timeout || !Number.isFinite(timeout) || timeout <= 0)) throw new WorkflowParseError(`${formatStep(index)} "$.timeout" must be a positive number of milliseconds.`, {
|
|
1447
|
-
index,
|
|
1448
|
-
timeout
|
|
1449
|
-
});
|
|
1450
|
-
const continueOnError = value['continue-on-error'];
|
|
1451
|
-
if (void 0 !== continueOnError && 'boolean' != typeof continueOnError) throw new WorkflowParseError(`${formatStep(index)} "$.continue-on-error" must be a boolean.`, {
|
|
1452
|
-
index,
|
|
1453
|
-
continueOnError
|
|
1454
|
-
});
|
|
1455
|
-
return {
|
|
1456
|
-
...void 0 === timeout ? {} : {
|
|
1457
|
-
timeoutMs: timeout
|
|
1458
|
-
},
|
|
1459
|
-
continueOnError: continueOnError ?? false
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
function normalizeStep(value, index = 0, resolveNode) {
|
|
1463
|
-
if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} must be a mapping.`, {
|
|
1464
|
-
index
|
|
1465
|
-
});
|
|
1466
|
-
const entries = Object.entries(value);
|
|
1467
|
-
if (1 !== entries.length) throw new WorkflowParseError(`${formatStep(index)} must contain exactly one node.`, {
|
|
1468
|
-
index,
|
|
1469
|
-
nodes: Object.keys(value)
|
|
1470
|
-
});
|
|
1471
|
-
const [node, rawValue] = entries[0];
|
|
1472
|
-
if (0 === node.trim().length) throw new WorkflowParseError(`${formatStep(index)} node name must not be empty.`, {
|
|
1473
|
-
index
|
|
1474
|
-
});
|
|
1475
|
-
if ('string' == typeof rawValue) {
|
|
1476
|
-
const resolvedNode = resolveNode?.(node);
|
|
1477
|
-
const stringInputKey = resolvedNode ? resolvedNode.stringInputKey : 'prompt';
|
|
1478
|
-
if (false === stringInputKey || void 0 === stringInputKey) throw new WorkflowParseError(`${formatStep(index)} node "${node}" does not accept string shorthand.`, {
|
|
1479
|
-
index,
|
|
1480
|
-
node
|
|
1481
|
-
});
|
|
1482
|
-
return {
|
|
1483
|
-
node,
|
|
1484
|
-
input: {
|
|
1485
|
-
[stringInputKey]: rawValue
|
|
1486
|
-
},
|
|
1487
|
-
meta: {
|
|
1488
|
-
continueOnError: false
|
|
1489
|
-
}
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
if (!isMapping(rawValue)) throw new WorkflowParseError(`${formatStep(index)} value must be a string shorthand or a mapping.`, {
|
|
1493
|
-
index,
|
|
1494
|
-
node
|
|
1495
|
-
});
|
|
1496
|
-
const { $: rawMeta, ...input } = rawValue;
|
|
1497
|
-
return {
|
|
1498
|
-
node,
|
|
1499
|
-
input,
|
|
1500
|
-
meta: normalizeMeta(rawMeta, index)
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
function normalizeSteps(steps, resolveNode) {
|
|
1504
|
-
if (!Array.isArray(steps)) throw new WorkflowParseError('Steps must be an array.');
|
|
1505
|
-
return steps.map((step, index)=>normalizeStep(step, index, resolveNode));
|
|
1506
|
-
}
|
|
1507
|
-
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
1508
|
-
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
1509
|
-
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
1510
|
-
const missingVariable = (kind, name, options)=>{
|
|
1511
|
-
throw new WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
|
|
1512
|
-
...options.location,
|
|
1513
|
-
variable: name,
|
|
1514
|
-
variableKind: kind
|
|
1515
|
-
});
|
|
1516
|
-
};
|
|
1517
|
-
const projectVariable = (name, options)=>{
|
|
1518
|
-
if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
|
|
1519
|
-
return options.variables[name];
|
|
1520
|
-
};
|
|
1521
|
-
const environmentVariable = (name, options)=>{
|
|
1522
|
-
const value = options.env?.[name];
|
|
1523
|
-
if (void 0 === value) return missingVariable('environment', name, options);
|
|
1524
|
-
return value;
|
|
1525
|
-
};
|
|
1526
|
-
const interpolationValue = (name, value, options)=>{
|
|
1527
|
-
if ('object' == typeof value && null !== value) throw new WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
|
|
1528
|
-
...options.location,
|
|
1529
|
-
variable: name,
|
|
1530
|
-
variableKind: 'project'
|
|
1531
|
-
});
|
|
1532
|
-
return String(value);
|
|
1533
|
-
};
|
|
1534
|
-
const resolveString = (value, options)=>{
|
|
1535
|
-
const exactEnv = exactEnvExpression.exec(value);
|
|
1536
|
-
if (exactEnv) return environmentVariable(exactEnv[1], options);
|
|
1537
|
-
const exactProject = exactProjectExpression.exec(value);
|
|
1538
|
-
if (exactProject) return projectVariable(exactProject[1], options);
|
|
1539
|
-
return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
|
|
1540
|
-
};
|
|
1541
|
-
const resolveWorkflowVariables = (value, options)=>{
|
|
1542
|
-
if ('string' == typeof value) return resolveString(value, options);
|
|
1543
|
-
if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
|
|
1544
|
-
if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
|
|
1545
|
-
key,
|
|
1546
|
-
resolveWorkflowVariables(child, options)
|
|
1547
|
-
]));
|
|
1548
|
-
return value;
|
|
1549
|
-
};
|
|
1550
|
-
const collect_isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
1551
|
-
const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
1552
|
-
const allowedSet = new Set(allowed);
|
|
1553
|
-
const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
|
|
1554
|
-
if (unknown.length) throw new WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
|
|
1555
|
-
field: unknown[0]
|
|
1556
|
-
});
|
|
1557
|
-
};
|
|
1558
|
-
const createCaseId = (projectId, sourcePath, caseIndex)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
1559
|
-
projectId,
|
|
1560
|
-
sourcePath,
|
|
1561
|
-
caseIndex
|
|
1562
|
-
])).digest('hex');
|
|
1563
|
-
const createWorkflowDocumentId = (projectId, sourcePath)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
1564
|
-
projectId,
|
|
1565
|
-
sourcePath
|
|
1566
|
-
])).digest('hex');
|
|
1567
|
-
function collectWorkflowDocument(source, options) {
|
|
1568
|
-
let parsed;
|
|
1569
|
-
try {
|
|
1570
|
-
parsed = (0, external_js_yaml_namespaceObject.load)((0, external_node_fs_.readFileSync)(source.absolutePath, 'utf8'), {
|
|
1571
|
-
schema: external_js_yaml_namespaceObject.JSON_SCHEMA
|
|
1572
|
-
});
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
throw new WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
1575
|
-
sourcePath: source.sourcePath
|
|
1576
|
-
}, error);
|
|
1577
|
-
}
|
|
1578
|
-
if (!collect_isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
|
|
1579
|
-
collect_rejectUnknownKeys(parsed, [
|
|
1580
|
-
'beforeAll',
|
|
1581
|
-
'beforeEach',
|
|
1582
|
-
'cases',
|
|
1583
|
-
'afterEach',
|
|
1584
|
-
'afterAll'
|
|
1585
|
-
], 'Workflow document');
|
|
1586
|
-
if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new WorkflowParseError('Workflow document must contain a non-empty cases array.');
|
|
1587
|
-
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
1588
|
-
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
1589
|
-
...step,
|
|
1590
|
-
input: resolveWorkflowVariables(step.input, {
|
|
1591
|
-
variables: options.variables,
|
|
1592
|
-
env: options.env ?? process.env,
|
|
1593
|
-
location: {
|
|
1594
|
-
projectName: source.projectName ?? source.projectId,
|
|
1595
|
-
sourcePath,
|
|
1596
|
-
phase,
|
|
1597
|
-
stepIndex,
|
|
1598
|
-
...void 0 === caseIndex ? {} : {
|
|
1599
|
-
caseIndex
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
})
|
|
1603
|
-
});
|
|
1604
|
-
const normalizeLifecycle = (phase)=>{
|
|
1605
|
-
const value = parsed[phase];
|
|
1606
|
-
if (void 0 === value) return [];
|
|
1607
|
-
if (!Array.isArray(value)) throw new WorkflowParseError(`Workflow document ${phase} must be an array.`, {
|
|
1608
|
-
phase
|
|
1609
|
-
});
|
|
1610
|
-
return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
|
|
1611
|
-
const resolved = options.resolveNode(normalized.node);
|
|
1612
|
-
if (!resolved) throw new WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
1613
|
-
phase,
|
|
1614
|
-
stepIndex,
|
|
1615
|
-
node: normalized.node
|
|
1616
|
-
});
|
|
1617
|
-
return resolveStepVariables(normalized, phase, stepIndex);
|
|
636
|
+
return resolveStepVariables(normalized, phase, stepIndex);
|
|
1618
637
|
});
|
|
1619
638
|
};
|
|
1620
639
|
const lifecycle = {
|
|
@@ -1625,7 +644,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
1625
644
|
};
|
|
1626
645
|
const ids = new Set();
|
|
1627
646
|
const cases = parsed.cases.map((definition, caseIndex)=>{
|
|
1628
|
-
if (!
|
|
647
|
+
if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
|
|
1629
648
|
caseIndex
|
|
1630
649
|
});
|
|
1631
650
|
collect_rejectUnknownKeys(definition, [
|
|
@@ -1633,18 +652,18 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
1633
652
|
'tags',
|
|
1634
653
|
'steps'
|
|
1635
654
|
], `Case ${caseIndex + 1}`);
|
|
1636
|
-
if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
|
|
655
|
+
if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
|
|
1637
656
|
caseIndex
|
|
1638
657
|
});
|
|
1639
|
-
if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
|
|
658
|
+
if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
|
|
1640
659
|
caseIndex
|
|
1641
660
|
});
|
|
1642
661
|
const tags = definition.tags ?? [];
|
|
1643
|
-
if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
|
|
662
|
+
if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
|
|
1644
663
|
caseIndex
|
|
1645
664
|
});
|
|
1646
665
|
const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
1647
|
-
if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
666
|
+
if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
1648
667
|
caseIndex,
|
|
1649
668
|
stepIndex,
|
|
1650
669
|
node: normalized.node
|
|
@@ -1652,7 +671,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
1652
671
|
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
1653
672
|
});
|
|
1654
673
|
const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
|
|
1655
|
-
if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
674
|
+
if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
1656
675
|
caseId
|
|
1657
676
|
});
|
|
1658
677
|
ids.add(caseId);
|
|
@@ -1669,435 +688,27 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
1669
688
|
};
|
|
1670
689
|
});
|
|
1671
690
|
return {
|
|
1672
|
-
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
1673
|
-
projectId: source.projectId,
|
|
1674
|
-
sourcePath,
|
|
1675
|
-
lifecycle,
|
|
1676
|
-
cases
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
1680
|
-
const MAX_DEPTH = 6;
|
|
1681
|
-
const MAX_STRING_LENGTH = 2000;
|
|
1682
|
-
const MAX_ARRAY_ITEMS = 50;
|
|
1683
|
-
const MAX_OBJECT_KEYS = 50;
|
|
1684
|
-
const MAX_SERIALIZED_BYTES = 65536;
|
|
1685
|
-
const TRUNCATED_VALUE = '[Truncated: display value exceeded 64 KiB]';
|
|
1686
|
-
const normalizedSecretKeys = [
|
|
1687
|
-
'password',
|
|
1688
|
-
'passwd',
|
|
1689
|
-
'secret',
|
|
1690
|
-
'token',
|
|
1691
|
-
'authorization',
|
|
1692
|
-
'cookie',
|
|
1693
|
-
'apikey'
|
|
1694
|
-
];
|
|
1695
|
-
const normalizedKey = (key)=>key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1696
|
-
const isSecretKey = (key)=>{
|
|
1697
|
-
const normalized = normalizedKey(key);
|
|
1698
|
-
return normalizedSecretKeys.some((secret)=>normalized.includes(secret));
|
|
1699
|
-
};
|
|
1700
|
-
const objectPath = (parent, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${parent}.${key}` : `${parent}[${JSON.stringify(key)}]`;
|
|
1701
|
-
const stringByteLength = (value)=>Buffer.byteLength(value, 'utf8');
|
|
1702
|
-
function sanitizeReportText(value, maximumLength = MAX_STRING_LENGTH) {
|
|
1703
|
-
return value.length <= maximumLength ? value : `${value.slice(0, maximumLength)}… [truncated]`;
|
|
1704
|
-
}
|
|
1705
|
-
function sanitizeReportValue(value) {
|
|
1706
|
-
const truncatedPaths = new Set();
|
|
1707
|
-
const redactedPaths = new Set();
|
|
1708
|
-
const ancestors = new WeakSet();
|
|
1709
|
-
let remainingBytes = MAX_SERIALIZED_BYTES;
|
|
1710
|
-
const consume = (path, text)=>{
|
|
1711
|
-
remainingBytes -= stringByteLength(text);
|
|
1712
|
-
if (remainingBytes >= 0) return true;
|
|
1713
|
-
truncatedPaths.add(path);
|
|
1714
|
-
return false;
|
|
1715
|
-
};
|
|
1716
|
-
const visit = (current, path, depth)=>{
|
|
1717
|
-
if (remainingBytes <= 0) {
|
|
1718
|
-
truncatedPaths.add(path);
|
|
1719
|
-
return '[Truncated: size limit]';
|
|
1720
|
-
}
|
|
1721
|
-
if (null === current || 'boolean' == typeof current) {
|
|
1722
|
-
consume(path, String(current));
|
|
1723
|
-
return current;
|
|
1724
|
-
}
|
|
1725
|
-
if ('string' == typeof current) {
|
|
1726
|
-
if (current.length > MAX_STRING_LENGTH) {
|
|
1727
|
-
truncatedPaths.add(path);
|
|
1728
|
-
const shortened = `${current.slice(0, MAX_STRING_LENGTH)}… [truncated]`;
|
|
1729
|
-
consume(path, shortened);
|
|
1730
|
-
return shortened;
|
|
1731
|
-
}
|
|
1732
|
-
consume(path, current);
|
|
1733
|
-
return current;
|
|
1734
|
-
}
|
|
1735
|
-
if ('number' == typeof current) {
|
|
1736
|
-
if (Number.isFinite(current)) {
|
|
1737
|
-
consume(path, String(current));
|
|
1738
|
-
return current;
|
|
1739
|
-
}
|
|
1740
|
-
truncatedPaths.add(path);
|
|
1741
|
-
return `[Unsupported number: ${String(current)}]`;
|
|
1742
|
-
}
|
|
1743
|
-
if ('bigint' == typeof current) {
|
|
1744
|
-
truncatedPaths.add(path);
|
|
1745
|
-
return `[Unsupported bigint: ${String(current)}]`;
|
|
1746
|
-
}
|
|
1747
|
-
if (void 0 === current || 'function' == typeof current || 'symbol' == typeof current) {
|
|
1748
|
-
truncatedPaths.add(path);
|
|
1749
|
-
return `[Unsupported ${typeof current}]`;
|
|
1750
|
-
}
|
|
1751
|
-
if (depth >= MAX_DEPTH) {
|
|
1752
|
-
truncatedPaths.add(path);
|
|
1753
|
-
return '[Truncated: maximum depth]';
|
|
1754
|
-
}
|
|
1755
|
-
if (current instanceof Date) {
|
|
1756
|
-
const date = Number.isNaN(current.getTime()) ? '[Invalid Date]' : current.toISOString();
|
|
1757
|
-
consume(path, date);
|
|
1758
|
-
return date;
|
|
1759
|
-
}
|
|
1760
|
-
if (ancestors.has(current)) {
|
|
1761
|
-
truncatedPaths.add(path);
|
|
1762
|
-
return '[Unsupported circular value]';
|
|
1763
|
-
}
|
|
1764
|
-
if (Array.isArray(current)) {
|
|
1765
|
-
ancestors.add(current);
|
|
1766
|
-
const output = [];
|
|
1767
|
-
const itemCount = Math.min(current.length, MAX_ARRAY_ITEMS);
|
|
1768
|
-
for(let index = 0; index < itemCount; index += 1){
|
|
1769
|
-
if (remainingBytes <= 0) break;
|
|
1770
|
-
output.push(visit(current[index], `${path}[${index}]`, depth + 1));
|
|
1771
|
-
}
|
|
1772
|
-
if (current.length > output.length) truncatedPaths.add(path);
|
|
1773
|
-
ancestors.delete(current);
|
|
1774
|
-
return output;
|
|
1775
|
-
}
|
|
1776
|
-
const prototype = Object.getPrototypeOf(current);
|
|
1777
|
-
if (prototype !== Object.prototype && null !== prototype) {
|
|
1778
|
-
truncatedPaths.add(path);
|
|
1779
|
-
const constructorName = current.constructor?.name ?? 'object';
|
|
1780
|
-
return `[Unsupported object: ${constructorName}]`;
|
|
1781
|
-
}
|
|
1782
|
-
ancestors.add(current);
|
|
1783
|
-
const output = {};
|
|
1784
|
-
const keys = Object.keys(current);
|
|
1785
|
-
for (const key of keys.slice(0, MAX_OBJECT_KEYS)){
|
|
1786
|
-
const childPath = objectPath(path, key);
|
|
1787
|
-
if (!consume(childPath, key)) break;
|
|
1788
|
-
if (isSecretKey(key)) {
|
|
1789
|
-
output[key] = '[REDACTED]';
|
|
1790
|
-
redactedPaths.add(childPath);
|
|
1791
|
-
} else output[key] = visit(current[key], childPath, depth + 1);
|
|
1792
|
-
if (remainingBytes <= 0) break;
|
|
1793
|
-
}
|
|
1794
|
-
if (keys.length > Object.keys(output).length) truncatedPaths.add(path);
|
|
1795
|
-
ancestors.delete(current);
|
|
1796
|
-
return output;
|
|
1797
|
-
};
|
|
1798
|
-
const sanitized = visit(value, '$', 0);
|
|
1799
|
-
const serialized = JSON.stringify(sanitized);
|
|
1800
|
-
if (void 0 === serialized || stringByteLength(serialized) > MAX_SERIALIZED_BYTES) return {
|
|
1801
|
-
value: TRUNCATED_VALUE,
|
|
1802
|
-
truncatedPaths: [
|
|
1803
|
-
'$'
|
|
1804
|
-
]
|
|
1805
|
-
};
|
|
1806
|
-
return {
|
|
1807
|
-
value: sanitized,
|
|
1808
|
-
...truncatedPaths.size > 0 ? {
|
|
1809
|
-
truncatedPaths: [
|
|
1810
|
-
...truncatedPaths
|
|
1811
|
-
]
|
|
1812
|
-
} : {},
|
|
1813
|
-
...redactedPaths.size > 0 ? {
|
|
1814
|
-
redactedPaths: [
|
|
1815
|
-
...redactedPaths
|
|
1816
|
-
]
|
|
1817
|
-
} : {}
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
function sanitizeReportError(error) {
|
|
1821
|
-
if (error instanceof Error) {
|
|
1822
|
-
const workflowLike = error;
|
|
1823
|
-
return {
|
|
1824
|
-
name: sanitizeReportText(error.name || 'Error', 200),
|
|
1825
|
-
message: sanitizeReportText(error.message || String(error)),
|
|
1826
|
-
...'string' == typeof workflowLike.code ? {
|
|
1827
|
-
code: sanitizeReportText(workflowLike.code, 200)
|
|
1828
|
-
} : {},
|
|
1829
|
-
...void 0 === workflowLike.details ? {} : {
|
|
1830
|
-
details: sanitizeReportValue(workflowLike.details)
|
|
1831
|
-
}
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
return {
|
|
1835
|
-
name: 'Error',
|
|
1836
|
-
message: sanitizeReportText(String(error))
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
const warnReport = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
|
|
1840
|
-
console: true
|
|
1841
|
-
});
|
|
1842
|
-
const exactMidsceneDetailNodes = new Set([
|
|
1843
|
-
'aiAct',
|
|
1844
|
-
'aiAssert',
|
|
1845
|
-
'recordToReport'
|
|
1846
|
-
]);
|
|
1847
|
-
const scopedExecutionKey = (scopeId, executionId)=>`${scopeId}\u0000${executionId}`;
|
|
1848
|
-
const reportSourcesForScope = (context, scopeId)=>context.sourcesByScope.get(scopeId) ?? [];
|
|
1849
|
-
const addDiagnostic = (context, diagnostic)=>{
|
|
1850
|
-
context.diagnostics.push(diagnostic);
|
|
1851
|
-
const key = JSON.stringify([
|
|
1852
|
-
diagnostic.code,
|
|
1853
|
-
diagnostic.scopeId,
|
|
1854
|
-
diagnostic.executionId
|
|
1855
|
-
]);
|
|
1856
|
-
if (!context.warnedDiagnostics.has(key)) {
|
|
1857
|
-
context.warnedDiagnostics.add(key);
|
|
1858
|
-
warnReport(diagnostic.message);
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
const buildStep = (step, scopeId, context)=>{
|
|
1862
|
-
const details = [];
|
|
1863
|
-
const unresolvedExecutionIds = [];
|
|
1864
|
-
for (const trace of step.report?.traces ?? []){
|
|
1865
|
-
const previousScope = context.tracedScopeByExecution.get(trace.executionId);
|
|
1866
|
-
if (previousScope && previousScope !== scopeId) throw new Error(`Agent execution ${trace.executionId} was traced by multiple test scopes (${previousScope}, ${scopeId}).`);
|
|
1867
|
-
context.tracedScopeByExecution.set(trace.executionId, scopeId);
|
|
1868
|
-
const detail = context.detailsByScopeAndExecution.get(scopedExecutionKey(scopeId, trace.executionId));
|
|
1869
|
-
if (detail) details.push(detail);
|
|
1870
|
-
else unresolvedExecutionIds.push(trace.executionId);
|
|
1871
|
-
}
|
|
1872
|
-
let agentDetailDiagnostic;
|
|
1873
|
-
if (unresolvedExecutionIds.length > 0) {
|
|
1874
|
-
agentDetailDiagnostic = `Agent detail could not be resolved for ${unresolvedExecutionIds.join(', ')}.`;
|
|
1875
|
-
const sources = reportSourcesForScope(context, scopeId);
|
|
1876
|
-
for (const executionId of unresolvedExecutionIds)addDiagnostic(context, {
|
|
1877
|
-
level: 'warning',
|
|
1878
|
-
code: 'agent-detail-unresolved',
|
|
1879
|
-
message: `Agent detail could not be resolved for execution ${executionId} in test scope ${scopeId}.`,
|
|
1880
|
-
scopeId,
|
|
1881
|
-
executionId,
|
|
1882
|
-
...sources.length > 0 ? {
|
|
1883
|
-
sourcePaths: sources.map((source)=>source.sourcePath)
|
|
1884
|
-
} : {}
|
|
1885
|
-
});
|
|
1886
|
-
} else if (0 === details.length && exactMidsceneDetailNodes.has(step.node)) {
|
|
1887
|
-
agentDetailDiagnostic = 'This Agent did not expose a stable execution reference for the Step.';
|
|
1888
|
-
addDiagnostic(context, {
|
|
1889
|
-
level: 'warning',
|
|
1890
|
-
code: 'agent-detail-unavailable',
|
|
1891
|
-
message: `No stable Agent execution reference was captured for ${step.node} in test scope ${scopeId}.`,
|
|
1892
|
-
scopeId,
|
|
1893
|
-
...reportSourcesForScope(context, scopeId).length > 0 ? {
|
|
1894
|
-
sourcePaths: reportSourcesForScope(context, scopeId).map((source)=>source.sourcePath)
|
|
1895
|
-
} : {}
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
const prompt = 'string' == typeof step.input?.prompt ? step.input.prompt : void 0;
|
|
1899
|
-
return {
|
|
1900
|
-
id: `${scopeId}:${step.phase}:${step.stepIndex}`,
|
|
1901
|
-
phase: step.phase,
|
|
1902
|
-
stepIndex: step.stepIndex,
|
|
1903
|
-
node: step.node,
|
|
1904
|
-
...prompt ? {
|
|
1905
|
-
title: sanitizeReportText(prompt, 240)
|
|
1906
|
-
} : {},
|
|
1907
|
-
status: step.status,
|
|
1908
|
-
continuedAfterError: step.continuedAfterError,
|
|
1909
|
-
startedAt: step.startedAt,
|
|
1910
|
-
endedAt: step.endedAt,
|
|
1911
|
-
durationMs: step.durationMs,
|
|
1912
|
-
input: sanitizeReportValue(step.input),
|
|
1913
|
-
...step.output ? {
|
|
1914
|
-
output: {
|
|
1915
|
-
...void 0 === step.output.summary ? {} : {
|
|
1916
|
-
summary: sanitizeReportText(step.output.summary)
|
|
1917
|
-
},
|
|
1918
|
-
...void 0 === step.output.data ? {} : {
|
|
1919
|
-
data: sanitizeReportValue(step.output.data)
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
} : {},
|
|
1923
|
-
...step.error ? {
|
|
1924
|
-
error: sanitizeReportError(step.error)
|
|
1925
|
-
} : {},
|
|
1926
|
-
...details.length > 0 ? {
|
|
1927
|
-
agentDetails: details
|
|
1928
|
-
} : {},
|
|
1929
|
-
...agentDetailDiagnostic ? {
|
|
1930
|
-
agentDetailDiagnostic
|
|
1931
|
-
} : {}
|
|
1932
|
-
};
|
|
1933
|
-
};
|
|
1934
|
-
const buildAttempt = (attempt, context)=>({
|
|
1935
|
-
attemptId: attempt.runId,
|
|
1936
|
-
attemptIndex: attempt.attemptIndex,
|
|
1937
|
-
status: attempt.status,
|
|
1938
|
-
startedAt: attempt.startedAt,
|
|
1939
|
-
endedAt: attempt.endedAt,
|
|
1940
|
-
durationMs: attempt.durationMs,
|
|
1941
|
-
beforeEach: attempt.beforeEach.map((step)=>buildStep(step, attempt.runId, context)),
|
|
1942
|
-
steps: attempt.steps.map((step)=>buildStep(step, attempt.runId, context)),
|
|
1943
|
-
afterEach: attempt.afterEach.map((step)=>buildStep(step, attempt.runId, context)),
|
|
1944
|
-
...attempt.teardownErrors?.length ? {
|
|
1945
|
-
teardownErrors: attempt.teardownErrors.map(sanitizeReportError)
|
|
1946
|
-
} : {},
|
|
1947
|
-
...reportSourcesForScope(context, attempt.runId).length > 0 ? {
|
|
1948
|
-
scopeReportIds: reportSourcesForScope(context, attempt.runId).map((source)=>source.reportId)
|
|
1949
|
-
} : {}
|
|
1950
|
-
});
|
|
1951
|
-
const buildCase = (outcome, context)=>({
|
|
1952
|
-
caseId: outcome.caseId,
|
|
1953
|
-
name: outcome.name,
|
|
1954
|
-
caseIndex: outcome.caseIndex,
|
|
1955
|
-
status: outcome.status,
|
|
1956
|
-
...outcome.notRunReason ? {
|
|
1957
|
-
notRunReason: outcome.notRunReason
|
|
1958
|
-
} : {},
|
|
1959
|
-
attempts: (outcome.attempts ?? (outcome.run ? [
|
|
1960
|
-
outcome.run
|
|
1961
|
-
] : [])).map((attempt)=>buildAttempt(attempt, context))
|
|
1962
|
-
});
|
|
1963
|
-
const buildDocument = (documentId, document, cases, context)=>{
|
|
1964
|
-
const sourcePath = document?.sourcePath ?? cases[0]?.sourcePath ?? '<unknown>';
|
|
1965
|
-
const status = document?.status ?? (cases.some((item)=>'success' !== item.status) ? 'failed' : 'success');
|
|
1966
|
-
return {
|
|
1967
|
-
documentId,
|
|
1968
|
-
sourcePath,
|
|
1969
|
-
status,
|
|
1970
|
-
...document ? {
|
|
1971
|
-
startedAt: document.startedAt,
|
|
1972
|
-
endedAt: document.endedAt,
|
|
1973
|
-
durationMs: document.durationMs
|
|
1974
|
-
} : {},
|
|
1975
|
-
beforeAll: (document?.beforeAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
|
|
1976
|
-
cases: cases.map((outcome)=>buildCase(outcome, context)),
|
|
1977
|
-
afterAll: (document?.afterAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
|
|
1978
|
-
...document?.teardownErrors?.length ? {
|
|
1979
|
-
teardownErrors: document.teardownErrors.map(sanitizeReportError)
|
|
1980
|
-
} : {},
|
|
1981
|
-
...document && reportSourcesForScope(context, document.documentRunId).length > 0 ? {
|
|
1982
|
-
scopeReportIds: reportSourcesForScope(context, document.documentRunId).map((source)=>source.reportId)
|
|
1983
|
-
} : {}
|
|
1984
|
-
};
|
|
1985
|
-
};
|
|
1986
|
-
const buildProject = (project, context)=>{
|
|
1987
|
-
const documentById = new Map(project.documents.map((document)=>[
|
|
1988
|
-
document.documentId,
|
|
1989
|
-
document
|
|
1990
|
-
]));
|
|
1991
|
-
const casesByDocumentId = new Map();
|
|
1992
|
-
for (const outcome of project.cases){
|
|
1993
|
-
const current = casesByDocumentId.get(outcome.documentId) ?? [];
|
|
1994
|
-
current.push(outcome);
|
|
1995
|
-
casesByDocumentId.set(outcome.documentId, current);
|
|
1996
|
-
}
|
|
1997
|
-
const documentIds = new Set([
|
|
1998
|
-
...project.documents.map((document)=>document.documentId),
|
|
1999
|
-
...project.cases.map((outcome)=>outcome.documentId)
|
|
2000
|
-
]);
|
|
2001
|
-
return {
|
|
2002
|
-
projectId: project.projectId,
|
|
2003
|
-
name: project.name,
|
|
2004
|
-
status: project.status,
|
|
2005
|
-
retry: project.retry,
|
|
2006
|
-
...project.lifecycle ? {
|
|
2007
|
-
lifecycle: {
|
|
2008
|
-
status: project.lifecycle.status,
|
|
2009
|
-
startedAt: project.lifecycle.startedAt,
|
|
2010
|
-
endedAt: project.lifecycle.endedAt,
|
|
2011
|
-
durationMs: project.lifecycle.durationMs,
|
|
2012
|
-
...project.lifecycle.setupError ? {
|
|
2013
|
-
setupError: sanitizeReportError(project.lifecycle.setupError)
|
|
2014
|
-
} : {},
|
|
2015
|
-
...project.lifecycle.teardownErrors?.length ? {
|
|
2016
|
-
teardownErrors: project.lifecycle.teardownErrors.map(sanitizeReportError)
|
|
2017
|
-
} : {}
|
|
2018
|
-
}
|
|
2019
|
-
} : {},
|
|
2020
|
-
documents: [
|
|
2021
|
-
...documentIds
|
|
2022
|
-
].map((documentId)=>buildDocument(documentId, documentById.get(documentId), casesByDocumentId.get(documentId) ?? [], context)),
|
|
2023
|
-
collectionErrors: project.collectionErrors.map((collectionError)=>({
|
|
2024
|
-
sourcePath: collectionError.sourcePath,
|
|
2025
|
-
error: sanitizeReportError(collectionError.error)
|
|
2026
|
-
}))
|
|
691
|
+
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
692
|
+
projectId: source.projectId,
|
|
693
|
+
sourcePath,
|
|
694
|
+
lifecycle,
|
|
695
|
+
cases
|
|
2027
696
|
};
|
|
2028
|
-
};
|
|
2029
|
-
function collectTestRunReportSources(result) {
|
|
2030
|
-
const sources = [];
|
|
2031
|
-
for (const project of result.projects){
|
|
2032
|
-
for (const document of project.documents)for (const sourcePath of document.reportPaths ?? [])sources.push({
|
|
2033
|
-
scopeId: document.documentRunId,
|
|
2034
|
-
sourcePath
|
|
2035
|
-
});
|
|
2036
|
-
for (const outcome of project.cases)for (const attempt of outcome.attempts ?? (outcome.run ? [
|
|
2037
|
-
outcome.run
|
|
2038
|
-
] : []))for (const sourcePath of attempt.reportPaths ?? [])sources.push({
|
|
2039
|
-
scopeId: attempt.runId,
|
|
2040
|
-
sourcePath
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
return sources;
|
|
2044
697
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
scopeSources.push(source);
|
|
2051
|
-
sourcesByScope.set(source.scopeId, scopeSources);
|
|
2052
|
-
for (const executionId of source.executionIds)detailsByScopeAndExecution.set(scopedExecutionKey(source.scopeId, executionId), {
|
|
2053
|
-
reportId: source.reportId,
|
|
2054
|
-
executionId
|
|
698
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
699
|
+
const writeJson = async (path, value)=>{
|
|
700
|
+
try {
|
|
701
|
+
await (0, promises_namespaceObject.mkdir)((0, external_node_path_.dirname)(path), {
|
|
702
|
+
recursive: true
|
|
2055
703
|
});
|
|
704
|
+
await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
|
|
2056
707
|
}
|
|
2057
|
-
const context = {
|
|
2058
|
-
diagnostics: [],
|
|
2059
|
-
sourcesByScope,
|
|
2060
|
-
detailsByScopeAndExecution,
|
|
2061
|
-
tracedScopeByExecution: new Map(),
|
|
2062
|
-
warnedDiagnostics: new Set()
|
|
2063
|
-
};
|
|
2064
|
-
for (const source of index.sources)if (0 === source.executionIds.length) addDiagnostic(context, {
|
|
2065
|
-
level: 'warning',
|
|
2066
|
-
code: 'source-without-executions',
|
|
2067
|
-
message: `Agent report contains no executions: ${source.sourcePath}`,
|
|
2068
|
-
scopeId: source.scopeId,
|
|
2069
|
-
sourcePaths: [
|
|
2070
|
-
source.sourcePath
|
|
2071
|
-
]
|
|
2072
|
-
});
|
|
2073
|
-
const projects = result.projects.map((project)=>buildProject(project, context));
|
|
2074
|
-
return {
|
|
2075
|
-
schemaVersion: 1,
|
|
2076
|
-
kind: 'test-runner',
|
|
2077
|
-
runId: result.runId,
|
|
2078
|
-
status: result.status,
|
|
2079
|
-
startedAt: result.startedAt,
|
|
2080
|
-
endedAt: result.endedAt,
|
|
2081
|
-
durationMs: result.durationMs,
|
|
2082
|
-
summary: {
|
|
2083
|
-
...result.summary
|
|
2084
|
-
},
|
|
2085
|
-
metrics: index.metrics,
|
|
2086
|
-
projects,
|
|
2087
|
-
...context.diagnostics.length > 0 ? {
|
|
2088
|
-
diagnostics: context.diagnostics
|
|
2089
|
-
} : {}
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
const writeJson = (path, value)=>{
|
|
2093
|
-
(0, external_node_fs_.mkdirSync)((0, external_node_path_.dirname)(path), {
|
|
2094
|
-
recursive: true
|
|
2095
|
-
});
|
|
2096
|
-
(0, external_node_fs_.writeFileSync)(path, JSON.stringify(value, null, 2));
|
|
2097
708
|
};
|
|
2098
709
|
const toPosix = (value)=>value.split(external_node_path_.sep).join('/');
|
|
2099
710
|
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
2100
|
-
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_.join)(result.projectId, 'documents', result.documentId, 'document.json'));
|
|
711
|
+
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
2101
712
|
const collectionErrorPath = (projectId, sourcePath)=>{
|
|
2102
713
|
const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
2103
714
|
projectId,
|
|
@@ -2107,21 +718,19 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2107
718
|
};
|
|
2108
719
|
const writeWorkflowDocumentResult = (runDir, result)=>{
|
|
2109
720
|
const { documentRunId: _documentRunId, ...persistedResult } = result;
|
|
2110
|
-
writeJson((0, external_node_path_.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
|
|
721
|
+
return writeJson((0, external_node_path_.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
|
|
2111
722
|
};
|
|
2112
723
|
const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
|
|
2113
724
|
const { runId: attemptId, ...persistedResult } = result;
|
|
2114
|
-
writeJson((0, external_node_path_.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
|
|
725
|
+
return writeJson((0, external_node_path_.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
|
|
2115
726
|
...persistedResult,
|
|
2116
727
|
attemptId
|
|
2117
728
|
});
|
|
2118
729
|
};
|
|
2119
|
-
const writeCollectionError = (runDir, collectionError)=>{
|
|
2120
|
-
writeJson((0, external_node_path_.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
|
|
730
|
+
const writeCollectionError = (runDir, collectionError)=>writeJson((0, external_node_path_.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
|
|
2121
731
|
kind: 'collection-error',
|
|
2122
732
|
...collectionError
|
|
2123
733
|
});
|
|
2124
|
-
};
|
|
2125
734
|
const relativeToSummary = (summaryPath, target)=>{
|
|
2126
735
|
const value = (0, external_node_path_.relative)((0, external_node_path_.dirname)(summaryPath), target);
|
|
2127
736
|
return toPosix(value || '.');
|
|
@@ -2130,7 +739,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2130
739
|
const writeTestProjectRunResult = (options)=>{
|
|
2131
740
|
const { result } = options;
|
|
2132
741
|
const fact = (path)=>toPosix(path);
|
|
2133
|
-
writeJson(result.summaryPath, {
|
|
742
|
+
return writeJson(result.summaryPath, {
|
|
2134
743
|
schemaVersion: result.schemaVersion,
|
|
2135
744
|
runId: result.runId,
|
|
2136
745
|
startedAt: result.startedAt,
|
|
@@ -2138,6 +747,9 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2138
747
|
durationMs: result.durationMs,
|
|
2139
748
|
status: result.status,
|
|
2140
749
|
exitCode: result.exitCode,
|
|
750
|
+
...result.errors?.length ? {
|
|
751
|
+
errors: result.errors.map(errorJson)
|
|
752
|
+
} : {},
|
|
2141
753
|
projectRoot: options.projectRoot,
|
|
2142
754
|
...options.configPath ? {
|
|
2143
755
|
configPath: options.configPath
|
|
@@ -2173,6 +785,9 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2173
785
|
}
|
|
2174
786
|
} : {},
|
|
2175
787
|
cases: project.cases.map((outcome)=>({
|
|
788
|
+
...outcome.documentRunId ? {
|
|
789
|
+
documentRunId: outcome.documentRunId
|
|
790
|
+
} : {},
|
|
2176
791
|
documentId: outcome.documentId,
|
|
2177
792
|
caseId: outcome.caseId,
|
|
2178
793
|
sourcePath: outcome.sourcePath,
|
|
@@ -2196,6 +811,10 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2196
811
|
})),
|
|
2197
812
|
documents: project.documents.map((document)=>({
|
|
2198
813
|
documentId: document.documentId,
|
|
814
|
+
...void 0 === document.attemptIndex ? {} : {
|
|
815
|
+
attemptIndex: document.attemptIndex,
|
|
816
|
+
documentRunId: document.documentRunId
|
|
817
|
+
},
|
|
2199
818
|
sourcePath: document.sourcePath,
|
|
2200
819
|
status: document.status,
|
|
2201
820
|
resultFile: fact(workflowDocumentResultPath(document)),
|
|
@@ -2230,29 +849,28 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2230
849
|
'**/*.{yaml,yml}'
|
|
2231
850
|
]
|
|
2232
851
|
};
|
|
2233
|
-
const
|
|
2234
|
-
const
|
|
852
|
+
const project_preparation_toPosix = (value)=>value.split(external_node_path_.sep).join('/');
|
|
853
|
+
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
2235
854
|
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
}).filter((file)=>/\.ya?ml$/i.test(file));
|
|
855
|
+
const match = (patterns)=>(0, external_tinyglobby_namespaceObject.globSync)(patterns, {
|
|
856
|
+
absolute: true,
|
|
857
|
+
caseSensitiveMatch: false,
|
|
858
|
+
cwd: root,
|
|
859
|
+
dot: true,
|
|
860
|
+
expandDirectories: false,
|
|
861
|
+
followSymbolicLinks: false,
|
|
862
|
+
ignore: [
|
|
863
|
+
...ALWAYS_IGNORED_PATTERNS,
|
|
864
|
+
...selection.exclude ?? []
|
|
865
|
+
],
|
|
866
|
+
onlyFiles: true
|
|
867
|
+
}).filter((file)=>/\.ya?ml$/i.test(file));
|
|
868
|
+
const files = match(selection.include);
|
|
2251
869
|
return [
|
|
2252
870
|
...new Set(files.map((file)=>(0, external_node_path_.resolve)(file)))
|
|
2253
871
|
].sort((a, b)=>{
|
|
2254
|
-
const relativeA =
|
|
2255
|
-
const relativeB =
|
|
872
|
+
const relativeA = project_preparation_toPosix((0, external_node_path_.relative)(root, a));
|
|
873
|
+
const relativeB = project_preparation_toPosix((0, external_node_path_.relative)(root, b));
|
|
2256
874
|
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
2257
875
|
});
|
|
2258
876
|
};
|
|
@@ -2273,27 +891,11 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2273
891
|
].join('\n'));
|
|
2274
892
|
return supported[0] ? (0, external_node_path_.join)(root, supported[0]) : void 0;
|
|
2275
893
|
};
|
|
2276
|
-
const defaultResultDir = (projectRoot)=>(0, external_node_path_.join)(projectRoot, '.midscene', 'test-results');
|
|
2277
|
-
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
2278
|
-
const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
|
|
2279
|
-
const timestamp = [
|
|
2280
|
-
date.getFullYear(),
|
|
2281
|
-
padDatePart(date.getMonth() + 1),
|
|
2282
|
-
padDatePart(date.getDate()),
|
|
2283
|
-
padDatePart(date.getHours()),
|
|
2284
|
-
padDatePart(date.getMinutes()),
|
|
2285
|
-
padDatePart(date.getSeconds())
|
|
2286
|
-
].join('');
|
|
2287
|
-
return `${timestamp}-${uuid.slice(0, 8)}`;
|
|
2288
|
-
};
|
|
2289
|
-
const assertDirectory = (path, label)=>{
|
|
2290
|
-
if (!(0, external_node_fs_.existsSync)(path) || !(0, external_node_fs_.statSync)(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
|
|
2291
|
-
};
|
|
2292
894
|
const asCollectionError = (projectId, projectName, sourcePath, error)=>({
|
|
2293
895
|
projectId,
|
|
2294
896
|
projectName,
|
|
2295
897
|
sourcePath,
|
|
2296
|
-
error: error instanceof WorkflowError ? error : new WorkflowParseError(`Failed to collect workflow document "${sourcePath}": ${error instanceof Error ? error.message : String(error)}`, {
|
|
898
|
+
error: error instanceof test_runner_namespaceObject.WorkflowError ? error : new test_runner_namespaceObject.WorkflowParseError(`Failed to collect workflow document "${sourcePath}": ${error instanceof Error ? error.message : String(error)}`, {
|
|
2297
899
|
projectName,
|
|
2298
900
|
sourcePath
|
|
2299
901
|
}, error)
|
|
@@ -2315,52 +917,6 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2315
917
|
filtered
|
|
2316
918
|
};
|
|
2317
919
|
};
|
|
2318
|
-
const test_project_runner_asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
2319
|
-
documentId,
|
|
2320
|
-
caseId: collectedCase.caseId,
|
|
2321
|
-
projectName,
|
|
2322
|
-
name: collectedCase.definition.name,
|
|
2323
|
-
sourcePath: collectedCase.sourcePath,
|
|
2324
|
-
caseIndex: collectedCase.caseIndex,
|
|
2325
|
-
status: 'not-run',
|
|
2326
|
-
notRunReason: reason
|
|
2327
|
-
});
|
|
2328
|
-
const summarize = (projects)=>{
|
|
2329
|
-
const cases = projects.flatMap((project)=>project.cases);
|
|
2330
|
-
const documents = projects.flatMap((project)=>project.documents);
|
|
2331
|
-
return {
|
|
2332
|
-
total: cases.length,
|
|
2333
|
-
passed: cases.filter((item)=>'success' === item.status).length,
|
|
2334
|
-
failed: cases.filter((item)=>'failed' === item.status).length,
|
|
2335
|
-
notRun: cases.filter((item)=>'not-run' === item.status).length,
|
|
2336
|
-
filtered: projects.reduce((total, project)=>total + project.filteredCaseCount, 0),
|
|
2337
|
-
collectionErrors: projects.reduce((total, project)=>total + project.collectionErrors.length, 0),
|
|
2338
|
-
documentFailures: documents.filter((document)=>'failed' === document.status).length,
|
|
2339
|
-
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
2340
|
-
};
|
|
2341
|
-
};
|
|
2342
|
-
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
2343
|
-
const test_project_runner_formatStep = (info)=>{
|
|
2344
|
-
const position = stepPosition(info);
|
|
2345
|
-
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
2346
|
-
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
2347
|
-
};
|
|
2348
|
-
const formatStepResult = (info, result)=>{
|
|
2349
|
-
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
2350
|
-
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
2351
|
-
const error = result.error ? ` — ${result.error.message}` : '';
|
|
2352
|
-
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
2353
|
-
return `${indent}${symbol} ${test_project_runner_formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
2354
|
-
};
|
|
2355
|
-
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
2356
|
-
...attempt.beforeEach,
|
|
2357
|
-
...attempt.steps,
|
|
2358
|
-
...attempt.afterEach
|
|
2359
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError));
|
|
2360
|
-
const documentHasFatalError = (result)=>[
|
|
2361
|
-
...result.beforeAll,
|
|
2362
|
-
...result.afterAll
|
|
2363
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError);
|
|
2364
920
|
const selectProjects = (projects, names)=>{
|
|
2365
921
|
if (!names || 0 === names.length) return projects;
|
|
2366
922
|
const requested = new Set(names);
|
|
@@ -2370,74 +926,316 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2370
926
|
if (unknown) throw new Error(`Unknown Midscene project: ${unknown}`);
|
|
2371
927
|
return projects.filter((project)=>requested.has(project.name));
|
|
2372
928
|
};
|
|
2373
|
-
|
|
929
|
+
async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, options = {}) {
|
|
2374
930
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
2375
|
-
const
|
|
2376
|
-
const
|
|
931
|
+
const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_.resolve)(options.prerequisiteFile) : void 0;
|
|
932
|
+
const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
933
|
+
const files = [
|
|
934
|
+
...prerequisiteFile ? [
|
|
935
|
+
prerequisiteFile
|
|
936
|
+
] : [],
|
|
937
|
+
...mainFiles.filter((file)=>file !== prerequisiteFile)
|
|
938
|
+
];
|
|
939
|
+
const occurrences = new Map();
|
|
940
|
+
const sources = files.map((absolutePath)=>{
|
|
941
|
+
const invocationIndex = occurrences.get(absolutePath) ?? 0;
|
|
942
|
+
occurrences.set(absolutePath, invocationIndex + 1);
|
|
943
|
+
return {
|
|
2377
944
|
projectId: project.projectId,
|
|
2378
945
|
projectName: project.name,
|
|
2379
|
-
sourcePath:
|
|
2380
|
-
absolutePath
|
|
2381
|
-
|
|
946
|
+
sourcePath: project_preparation_toPosix((0, external_node_path_.relative)(projectRoot, absolutePath)),
|
|
947
|
+
absolutePath,
|
|
948
|
+
invocationIndex
|
|
949
|
+
};
|
|
950
|
+
});
|
|
2382
951
|
const collectionErrors = [];
|
|
2383
|
-
const
|
|
952
|
+
const invocations = [];
|
|
2384
953
|
let filteredCaseCount = 0;
|
|
2385
954
|
if (0 === sources.length) {
|
|
2386
|
-
const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
955
|
+
const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
2387
956
|
projectName: project.name,
|
|
2388
957
|
projectRoot
|
|
2389
958
|
}));
|
|
2390
959
|
collectionErrors.push(error);
|
|
2391
|
-
writeCollectionError(runDir, error);
|
|
960
|
+
await writeCollectionError(runDir, error);
|
|
2392
961
|
}
|
|
2393
962
|
for (const source of sources)try {
|
|
2394
|
-
const collected =
|
|
963
|
+
const collected = await options.collectDocument?.(source, project);
|
|
964
|
+
if (collected) {
|
|
965
|
+
filteredCaseCount += collected.filteredCaseCount;
|
|
966
|
+
if (collected.invocation) invocations.push(collected.invocation);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const document = collectWorkflowDocument(source, {
|
|
2395
970
|
resolveNode: project.nodes.get.bind(project.nodes),
|
|
2396
971
|
variables: project.variables,
|
|
2397
972
|
env: process.env
|
|
2398
973
|
});
|
|
2399
|
-
const filtered =
|
|
974
|
+
const filtered = source.absolutePath === prerequisiteFile ? {
|
|
975
|
+
document,
|
|
976
|
+
filtered: 0
|
|
977
|
+
} : filterDocumentCases(document, project);
|
|
2400
978
|
filteredCaseCount += filtered.filtered;
|
|
2401
|
-
if (filtered.document)
|
|
979
|
+
if (filtered.document) invocations.push({
|
|
980
|
+
document: filtered.document,
|
|
981
|
+
retry: {
|
|
982
|
+
scope: 'case',
|
|
983
|
+
count: project.retry
|
|
984
|
+
},
|
|
985
|
+
defaultTimeoutMs,
|
|
986
|
+
reportEnabled: true,
|
|
987
|
+
bindings: {
|
|
988
|
+
resolveNode: project.nodes.require.bind(project.nodes)
|
|
989
|
+
}
|
|
990
|
+
});
|
|
2402
991
|
} catch (error) {
|
|
2403
992
|
const collectionError = asCollectionError(project.projectId, project.name, source.sourcePath, error);
|
|
2404
993
|
collectionErrors.push(collectionError);
|
|
2405
|
-
writeCollectionError(runDir, collectionError);
|
|
994
|
+
await writeCollectionError(runDir, collectionError);
|
|
2406
995
|
}
|
|
996
|
+
const documents = invocations.map((invocation)=>invocation.document);
|
|
997
|
+
const prerequisiteSource = sources.find((source)=>source.absolutePath === prerequisiteFile);
|
|
998
|
+
const prerequisiteDocument = documents.find((document)=>document.sourcePath === prerequisiteSource?.sourcePath);
|
|
2407
999
|
return {
|
|
2408
1000
|
project,
|
|
1001
|
+
platform: options.platform ?? 'test',
|
|
1002
|
+
documentConcurrency: options.documentConcurrency ?? 1,
|
|
1003
|
+
...prerequisiteDocument ? {
|
|
1004
|
+
prerequisiteDocumentId: prerequisiteDocument.documentId
|
|
1005
|
+
} : {},
|
|
2409
1006
|
fileSelection,
|
|
2410
1007
|
sources,
|
|
2411
1008
|
documents,
|
|
1009
|
+
invocations,
|
|
2412
1010
|
collectionErrors,
|
|
2413
1011
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
2414
1012
|
filteredCaseCount
|
|
2415
1013
|
};
|
|
1014
|
+
}
|
|
1015
|
+
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
1016
|
+
const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
|
|
1017
|
+
const timestamp = [
|
|
1018
|
+
date.getFullYear(),
|
|
1019
|
+
padDatePart(date.getMonth() + 1),
|
|
1020
|
+
padDatePart(date.getDate()),
|
|
1021
|
+
padDatePart(date.getHours()),
|
|
1022
|
+
padDatePart(date.getMinutes()),
|
|
1023
|
+
padDatePart(date.getSeconds())
|
|
1024
|
+
].join('');
|
|
1025
|
+
return `${timestamp}-${uuid.slice(0, 8)}`;
|
|
2416
1026
|
};
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
1027
|
+
const assertDirectory = (path, label)=>{
|
|
1028
|
+
if (!(0, external_node_fs_.existsSync)(path) || !(0, external_node_fs_.statSync)(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
|
|
1029
|
+
};
|
|
1030
|
+
const resolveRunInput = (options)=>{
|
|
2421
1031
|
const cwd = (0, external_node_path_.resolve)(options.cwd ?? process.cwd());
|
|
2422
1032
|
assertDirectory(cwd, 'Test working directory');
|
|
2423
|
-
const
|
|
1033
|
+
const inputPath = options.projectRoot ? (0, external_node_path_.resolve)(cwd, options.projectRoot) : void 0;
|
|
1034
|
+
const singleFile = inputPath && (0, external_node_fs_.existsSync)(inputPath) && (0, external_node_fs_.statSync)(inputPath).isFile() ? inputPath : void 0;
|
|
1035
|
+
if (singleFile && !/\.ya?ml$/i.test(singleFile)) throw new Error(`Test input must be a YAML file or project directory: ${singleFile}`);
|
|
1036
|
+
const cliProjectRoot = singleFile ? (0, external_node_path_.dirname)(singleFile) : inputPath;
|
|
2424
1037
|
if (cliProjectRoot) assertDirectory(cliProjectRoot, 'Test project directory');
|
|
2425
|
-
const configSearchRoot = cliProjectRoot ?? cwd;
|
|
2426
|
-
const configPath = options.configPath ? (0, external_node_path_.resolve)(configSearchRoot, options.configPath) : discoverTestConfig(configSearchRoot);
|
|
2427
|
-
if (options.configPath && (!configPath || !(0, external_node_fs_.existsSync)(configPath))) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
2428
|
-
const definition = await loadTestProject(configPath);
|
|
2429
1038
|
const projectRoot = cliProjectRoot ?? cwd;
|
|
2430
|
-
|
|
1039
|
+
return {
|
|
1040
|
+
cwd,
|
|
1041
|
+
projectRoot,
|
|
1042
|
+
configSearchRoot: projectRoot,
|
|
1043
|
+
...singleFile ? {
|
|
1044
|
+
singleFile
|
|
1045
|
+
} : {}
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
async function prepareTestRun(options) {
|
|
1049
|
+
const startedAt = new Date();
|
|
1050
|
+
const runId = createTestRunId(startedAt);
|
|
1051
|
+
const input = resolveRunInput(options);
|
|
1052
|
+
const configPath = options.configPath ? (0, external_node_path_.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
1053
|
+
if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test --config only accepts a native TypeScript or JavaScript project config: ${configPath}. Run legacy batch YAML with \`midscene --config ${options.configPath}\`.`);
|
|
1054
|
+
if (options.configPath && !(0, external_node_fs_.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
1055
|
+
const definition = await loadTestProject(configPath);
|
|
1056
|
+
const resultDir = options.resultDir ? (0, external_node_path_.resolve)(input.cwd, options.resultDir) : (0, external_node_path_.join)(input.projectRoot, '.midscene', 'test-results');
|
|
2431
1057
|
const runDir = (0, external_node_path_.join)(resultDir, runId);
|
|
2432
1058
|
const summaryPath = (0, external_node_path_.join)(runDir, 'summary.json');
|
|
2433
|
-
const reportDir = (0, external_node_path_.resolve)(projectRoot, definition.output.reportDir);
|
|
1059
|
+
const reportDir = (0, external_node_path_.resolve)(input.projectRoot, definition.output.reportDir);
|
|
2434
1060
|
(0, external_node_fs_.mkdirSync)(resultDir, {
|
|
2435
1061
|
recursive: true
|
|
2436
1062
|
});
|
|
2437
1063
|
(0, external_node_fs_.mkdirSync)(runDir);
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
|
|
1064
|
+
const projects = [];
|
|
1065
|
+
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
|
|
1066
|
+
files: [
|
|
1067
|
+
input.singleFile
|
|
1068
|
+
]
|
|
1069
|
+
} : void 0));
|
|
1070
|
+
const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
|
|
1071
|
+
return {
|
|
1072
|
+
startedAt,
|
|
1073
|
+
runId,
|
|
1074
|
+
projectRoot: input.projectRoot,
|
|
1075
|
+
...configPath ? {
|
|
1076
|
+
configPath
|
|
1077
|
+
} : {},
|
|
1078
|
+
resultDir,
|
|
1079
|
+
runDir,
|
|
1080
|
+
summaryPath,
|
|
1081
|
+
reportDir,
|
|
1082
|
+
definition,
|
|
1083
|
+
projects,
|
|
1084
|
+
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1085
|
+
reportEnabled: true,
|
|
1086
|
+
publications: [],
|
|
1087
|
+
onProgress: options.onProgress ?? (()=>{})
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
const external_node_events_namespaceObject = require("node:events");
|
|
1091
|
+
const report_namespaceObject = require("@midscene/core/report");
|
|
1092
|
+
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
1093
|
+
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1094
|
+
const formatStep = (info)=>{
|
|
1095
|
+
const position = stepPosition(info);
|
|
1096
|
+
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
1097
|
+
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
1098
|
+
};
|
|
1099
|
+
const formatStepResult = (info, result)=>{
|
|
1100
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1101
|
+
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1102
|
+
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1103
|
+
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
1104
|
+
return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
1105
|
+
};
|
|
1106
|
+
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
1107
|
+
...attempt.beforeEach,
|
|
1108
|
+
...attempt.steps,
|
|
1109
|
+
...attempt.afterEach
|
|
1110
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (attempt.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError));
|
|
1111
|
+
const documentHasFatalError = (result)=>[
|
|
1112
|
+
...result.beforeAll,
|
|
1113
|
+
...result.afterAll
|
|
1114
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (result.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError);
|
|
1115
|
+
async function executeDocumentInvocation(options) {
|
|
1116
|
+
const { invocation, project, sinks } = options;
|
|
1117
|
+
const { document } = invocation;
|
|
1118
|
+
const documentRetry = 'document' === invocation.retry.scope;
|
|
1119
|
+
const attempts = [];
|
|
1120
|
+
await (0, test_runner_namespaceObject.runDocumentAttempts)({
|
|
1121
|
+
retry: documentRetry ? invocation.retry.count : 0,
|
|
1122
|
+
signal: options.signal,
|
|
1123
|
+
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1124
|
+
}, async (documentAttemptIndex)=>{
|
|
1125
|
+
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1126
|
+
const execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(document, {
|
|
1127
|
+
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
1128
|
+
...invocation.bindings,
|
|
1129
|
+
project,
|
|
1130
|
+
projectContext: options.projectContext,
|
|
1131
|
+
retry: documentRetry ? 0 : invocation.retry.count,
|
|
1132
|
+
signal: options.signal,
|
|
1133
|
+
defaultTimeoutMs: invocation.defaultTimeoutMs,
|
|
1134
|
+
shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
|
|
1135
|
+
stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
|
|
1136
|
+
isFatalError: (run)=>[
|
|
1137
|
+
...run.beforeEach,
|
|
1138
|
+
...run.steps,
|
|
1139
|
+
...run.afterEach
|
|
1140
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (run.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError),
|
|
1141
|
+
onCaseStart: (collectedCase)=>{
|
|
1142
|
+
options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
1143
|
+
},
|
|
1144
|
+
onStepStart: (info)=>{
|
|
1145
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1146
|
+
options.onProgress(`${indent}→ ${formatStep(info)}`);
|
|
1147
|
+
},
|
|
1148
|
+
onStepResult: async (info, result)=>{
|
|
1149
|
+
await invocation.bindings.onStepResult?.(info, result);
|
|
1150
|
+
options.onProgress(formatStepResult(info, result));
|
|
1151
|
+
},
|
|
1152
|
+
onCaseResult: async (attempt)=>{
|
|
1153
|
+
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1154
|
+
options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
1155
|
+
},
|
|
1156
|
+
onCaseOutcome: (outcome)=>{
|
|
1157
|
+
if (caseHasFatalError(outcome)) options.markProjectFatal();
|
|
1158
|
+
if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
|
|
1159
|
+
},
|
|
1160
|
+
onDocumentResult: options.onDocumentResult
|
|
1161
|
+
}).catch(async (error)=>{
|
|
1162
|
+
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) {
|
|
1163
|
+
const partial = error.result;
|
|
1164
|
+
attempts.push(partial);
|
|
1165
|
+
sinks.documents.push(partial.document);
|
|
1166
|
+
sinks.cases.push(...partial.cases.map((outcome)=>({
|
|
1167
|
+
...outcome,
|
|
1168
|
+
documentId: document.documentId,
|
|
1169
|
+
documentRunId: partial.document.documentRunId
|
|
1170
|
+
})));
|
|
1171
|
+
try {
|
|
1172
|
+
await writeWorkflowDocumentResult(options.runDir, partial.document);
|
|
1173
|
+
} catch (publicationError) {
|
|
1174
|
+
throw new test_runner_namespaceObject.WorkflowExecutionFailure(partial, [
|
|
1175
|
+
...error.errors,
|
|
1176
|
+
publicationError
|
|
1177
|
+
]);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
throw error;
|
|
1181
|
+
});
|
|
1182
|
+
attempts.push(execution);
|
|
1183
|
+
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1184
|
+
...outcome,
|
|
1185
|
+
documentId: document.documentId,
|
|
1186
|
+
documentRunId: execution.document.documentRunId
|
|
1187
|
+
})));
|
|
1188
|
+
sinks.documents.push(execution.document);
|
|
1189
|
+
await writeWorkflowDocumentResult(options.runDir, execution.document);
|
|
1190
|
+
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1191
|
+
const cleanupErrors = [
|
|
1192
|
+
...execution.document.teardownErrors ?? [],
|
|
1193
|
+
...execution.cases.flatMap((outcome)=>(outcome.attempts ?? []).flatMap((attempt)=>attempt.teardownErrors ?? []))
|
|
1194
|
+
];
|
|
1195
|
+
if (cleanupErrors.length) throw new test_runner_namespaceObject.WorkflowExecutionFailure(execution, cleanupErrors);
|
|
1196
|
+
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1197
|
+
return failed ? 'failed' : 'success';
|
|
1198
|
+
});
|
|
1199
|
+
const final = attempts.at(-1);
|
|
1200
|
+
if (!final) return;
|
|
1201
|
+
const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
|
|
1202
|
+
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1203
|
+
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1204
|
+
}
|
|
1205
|
+
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1206
|
+
documentId,
|
|
1207
|
+
caseId: collectedCase.caseId,
|
|
1208
|
+
projectName,
|
|
1209
|
+
name: collectedCase.definition.name,
|
|
1210
|
+
sourcePath: collectedCase.sourcePath,
|
|
1211
|
+
caseIndex: collectedCase.caseIndex,
|
|
1212
|
+
status: 'not-run',
|
|
1213
|
+
notRunReason: reason
|
|
1214
|
+
});
|
|
1215
|
+
const summarize = (projects)=>{
|
|
1216
|
+
const cases = projects.flatMap((project)=>latestById(project.cases, (item)=>item.caseId));
|
|
1217
|
+
const documents = projects.flatMap((project)=>latestById(project.documents, (item)=>item.documentId));
|
|
1218
|
+
return {
|
|
1219
|
+
total: cases.length,
|
|
1220
|
+
passed: cases.filter((item)=>'success' === item.status).length,
|
|
1221
|
+
failed: cases.filter((item)=>'failed' === item.status).length,
|
|
1222
|
+
notRun: cases.filter((item)=>'not-run' === item.status).length,
|
|
1223
|
+
filtered: projects.reduce((total, project)=>total + project.filteredCaseCount, 0),
|
|
1224
|
+
collectionErrors: projects.reduce((total, project)=>total + project.collectionErrors.length, 0),
|
|
1225
|
+
documentFailures: documents.filter((document)=>'failed' === document.status).length,
|
|
1226
|
+
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
1227
|
+
};
|
|
1228
|
+
};
|
|
1229
|
+
const latestById = (items, id)=>[
|
|
1230
|
+
...new Map(items.map((item)=>[
|
|
1231
|
+
id(item),
|
|
1232
|
+
item
|
|
1233
|
+
])).values()
|
|
1234
|
+
];
|
|
1235
|
+
const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
|
|
1236
|
+
async function runPreparedTestProject(plan) {
|
|
1237
|
+
const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
|
|
1238
|
+
const progress = plan.onProgress;
|
|
2441
1239
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
2442
1240
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
2443
1241
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
@@ -2455,11 +1253,13 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2455
1253
|
let failedCaseCount = 0;
|
|
2456
1254
|
const bailReached = ()=>definition.test.bail > 0 && failedCaseCount >= definition.test.bail;
|
|
2457
1255
|
let hasInfrastructureError = false;
|
|
2458
|
-
|
|
1256
|
+
const infrastructureErrors = [];
|
|
2459
1257
|
const recordInfrastructureError = (error)=>{
|
|
2460
|
-
|
|
1258
|
+
const errors = error instanceof test_runner_namespaceObject.WorkflowExecutionFailure ? error.errors : [
|
|
1259
|
+
error
|
|
1260
|
+
];
|
|
1261
|
+
for (const item of errors)if (!infrastructureErrors.includes(item)) infrastructureErrors.push(item);
|
|
2461
1262
|
hasInfrastructureError = true;
|
|
2462
|
-
firstInfrastructureError = error;
|
|
2463
1263
|
rootController.abort(error);
|
|
2464
1264
|
};
|
|
2465
1265
|
const runInfrastructureCallback = (callback)=>{
|
|
@@ -2476,10 +1276,11 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2476
1276
|
};
|
|
2477
1277
|
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
2478
1278
|
const { project } = prepared;
|
|
2479
|
-
const projectFailed = prepared.collectionErrors.length > 0 || cases.some((item)=>'success' !== item.status) || documents.some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
1279
|
+
const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
2480
1280
|
return {
|
|
2481
1281
|
projectId: project.projectId,
|
|
2482
1282
|
name: project.name,
|
|
1283
|
+
platform: prepared.platform,
|
|
2483
1284
|
status: projectFailed ? 'failed' : 'success',
|
|
2484
1285
|
retry: project.retry,
|
|
2485
1286
|
fileSelection: prepared.fileSelection,
|
|
@@ -2507,106 +1308,111 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2507
1308
|
const documents = [];
|
|
2508
1309
|
let lifecycle;
|
|
2509
1310
|
let projectFatal = false;
|
|
2510
|
-
if (prepared.collectionErrors.length > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
|
|
1311
|
+
if (prepared.collectionErrors.length > 0 || 'run' === plan.preflightScope && totalErrors > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
|
|
2511
1312
|
else if (rootController.signal.aborted) cases.push(...notRunSuite(prepared, 'interrupted'));
|
|
2512
1313
|
else if (bailReached()) cases.push(...notRunSuite(prepared, 'bail'));
|
|
2513
1314
|
else {
|
|
2514
|
-
const runtime = createProjectRuntime({
|
|
1315
|
+
const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
|
|
2515
1316
|
project,
|
|
2516
1317
|
setup: project.setup,
|
|
2517
1318
|
signal: rootController.signal
|
|
2518
1319
|
});
|
|
2519
1320
|
let hasProjectExecutionError = false;
|
|
2520
|
-
let projectExecutionError;
|
|
2521
1321
|
try {
|
|
2522
1322
|
await runtime.start();
|
|
2523
|
-
if (runtime.canRun)
|
|
2524
|
-
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
1323
|
+
if (runtime.canRun) {
|
|
1324
|
+
const runDocument = async (invocation, documentIndex)=>{
|
|
1325
|
+
const { document } = invocation;
|
|
1326
|
+
if (rootController.signal.aborted || bailReached() || projectFatal) {
|
|
1327
|
+
const reason = rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1328
|
+
cases.push(...document.cases.map((item)=>asNotRun(document.documentId, item, project.name, reason)));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
|
|
1332
|
+
await executeDocumentInvocation({
|
|
1333
|
+
invocation,
|
|
1334
|
+
project,
|
|
1335
|
+
projectContext: runtime.context,
|
|
1336
|
+
runDir,
|
|
1337
|
+
signal: runtime.signal,
|
|
1338
|
+
rootSignal: rootController.signal,
|
|
1339
|
+
shouldBail: bailReached,
|
|
1340
|
+
isProjectFatal: ()=>projectFatal,
|
|
1341
|
+
markProjectFatal: ()=>{
|
|
1342
|
+
projectFatal = true;
|
|
1343
|
+
},
|
|
1344
|
+
addFailedCases: (count)=>{
|
|
1345
|
+
failedCaseCount += count;
|
|
1346
|
+
},
|
|
1347
|
+
onProgress: projectProgress,
|
|
1348
|
+
sinks: {
|
|
1349
|
+
cases,
|
|
1350
|
+
documents
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
};
|
|
1354
|
+
let prerequisiteFailed = false;
|
|
1355
|
+
const prerequisite = prepared.invocations.find((invocation)=>invocation.document.documentId === prepared.prerequisiteDocumentId);
|
|
1356
|
+
const prerequisiteCount = prerequisite ? 1 : 0;
|
|
1357
|
+
if (prerequisite) {
|
|
1358
|
+
await runDocument(prerequisite, 0);
|
|
1359
|
+
prerequisiteFailed = documents.at(-1)?.status === 'failed' || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status);
|
|
2528
1360
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
isFatalError: (run)=>[
|
|
2540
|
-
...run.beforeEach,
|
|
2541
|
-
...run.steps,
|
|
2542
|
-
...run.afterEach
|
|
2543
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
|
|
2544
|
-
onCaseStart: (collectedCase)=>{
|
|
2545
|
-
projectProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
2546
|
-
},
|
|
2547
|
-
onStepStart: (info)=>{
|
|
2548
|
-
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
2549
|
-
projectProgress(`${indent}→ ${test_project_runner_formatStep(info)}`);
|
|
2550
|
-
},
|
|
2551
|
-
onStepResult: (info, result)=>projectProgress(formatStepResult(info, result)),
|
|
2552
|
-
onCaseResult: (attempt)=>{
|
|
2553
|
-
runInfrastructureCallback(()=>writeCaseAttemptResult(runDir, project.projectId, document.documentId, attempt));
|
|
2554
|
-
projectProgress(` ${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
2555
|
-
},
|
|
2556
|
-
onCaseOutcome: (outcome)=>{
|
|
2557
|
-
if ('failed' === outcome.status) failedCaseCount += 1;
|
|
2558
|
-
if (caseHasFatalError(outcome)) projectFatal = true;
|
|
2559
|
-
},
|
|
2560
|
-
onDocumentResult: (documentResult)=>runInfrastructureCallback(()=>writeWorkflowDocumentResult(runDir, documentResult))
|
|
1361
|
+
if (!prerequisiteFailed) await (0, test_runner_namespaceObject.runConcurrentJobs)(prepared.invocations.filter((invocation)=>invocation !== prerequisite), {
|
|
1362
|
+
concurrency: prepared.documentConcurrency,
|
|
1363
|
+
shouldStop: ()=>rootController.signal.aborted || bailReached() || projectFatal
|
|
1364
|
+
}, async (invocation, index)=>{
|
|
1365
|
+
try {
|
|
1366
|
+
await runDocument(invocation, index + prerequisiteCount);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
recordInfrastructureError(error);
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
2561
1371
|
});
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
if (documentHasFatalError(execution.document)) projectFatal = true;
|
|
2567
|
-
documents.push(execution.document);
|
|
2568
|
-
}
|
|
2569
|
-
else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
|
|
1372
|
+
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1373
|
+
const reason = prerequisiteFailed ? 'project-setup-failed' : rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1374
|
+
cases.push(...notRunSuite(prepared, reason).filter((item)=>!completed.has(item.caseId)));
|
|
1375
|
+
} else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
|
|
2570
1376
|
} catch (error) {
|
|
2571
1377
|
hasProjectExecutionError = true;
|
|
2572
|
-
projectExecutionError = error;
|
|
2573
1378
|
recordInfrastructureError(error);
|
|
2574
1379
|
} finally{
|
|
2575
|
-
const hasFailure = hasProjectExecutionError || cases.some((item)=>'success' !== item.status) || documents.some((item)=>'failed' === item.status) || projectFatal || rootController.signal.aborted;
|
|
1380
|
+
const hasFailure = hasProjectExecutionError || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || projectFatal || rootController.signal.aborted;
|
|
2576
1381
|
lifecycle = await runtime.finish(hasFailure ? 'failed' : 'success');
|
|
2577
1382
|
}
|
|
2578
|
-
if (hasProjectExecutionError)
|
|
1383
|
+
if (hasProjectExecutionError) {
|
|
1384
|
+
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1385
|
+
cases.push(...notRunSuite(prepared, 'interrupted').filter((item)=>!completed.has(item.caseId)));
|
|
1386
|
+
}
|
|
2579
1387
|
}
|
|
1388
|
+
const order = new Map(prepared.documents.map((document, index)=>[
|
|
1389
|
+
document.documentId,
|
|
1390
|
+
index
|
|
1391
|
+
]));
|
|
1392
|
+
const documentOrder = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1393
|
+
cases.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1394
|
+
documents.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || (a.attemptIndex ?? 0) - (b.attemptIndex ?? 0));
|
|
2580
1395
|
return buildProjectResult(prepared, cases, documents, lifecycle);
|
|
2581
1396
|
};
|
|
2582
1397
|
const projectResults = new Array(preparedProjects.length);
|
|
2583
|
-
let nextProjectIndex = 0;
|
|
2584
|
-
const claimNextProject = ()=>{
|
|
2585
|
-
if (hasInfrastructureError || rootController.signal.aborted || bailReached() || nextProjectIndex >= preparedProjects.length) return;
|
|
2586
|
-
const projectIndex = nextProjectIndex;
|
|
2587
|
-
nextProjectIndex += 1;
|
|
2588
|
-
return projectIndex;
|
|
2589
|
-
};
|
|
2590
|
-
const worker = async ()=>{
|
|
2591
|
-
try {
|
|
2592
|
-
while(true){
|
|
2593
|
-
const projectIndex = claimNextProject();
|
|
2594
|
-
if (void 0 === projectIndex) return;
|
|
2595
|
-
projectResults[projectIndex] = await runPreparedProject(preparedProjects[projectIndex], projectIndex);
|
|
2596
|
-
}
|
|
2597
|
-
} catch (error) {
|
|
2598
|
-
recordInfrastructureError(error);
|
|
2599
|
-
}
|
|
2600
|
-
};
|
|
2601
1398
|
try {
|
|
2602
|
-
await
|
|
2603
|
-
|
|
2604
|
-
|
|
1399
|
+
await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
|
|
1400
|
+
concurrency: effectiveConcurrency,
|
|
1401
|
+
shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
|
|
1402
|
+
}, async (prepared, index)=>{
|
|
1403
|
+
try {
|
|
1404
|
+
projectResults[index] = await runPreparedProject(prepared, index);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
recordInfrastructureError(error);
|
|
1407
|
+
throw error;
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
recordInfrastructureError(error);
|
|
2605
1412
|
} finally{
|
|
2606
1413
|
process.off('SIGINT', sigint);
|
|
2607
1414
|
process.off('SIGTERM', sigterm);
|
|
2608
1415
|
}
|
|
2609
|
-
if (hasInfrastructureError) throw firstInfrastructureError;
|
|
2610
1416
|
const completedProjectResults = preparedProjects.map((prepared, projectIndex)=>{
|
|
2611
1417
|
const result = projectResults[projectIndex];
|
|
2612
1418
|
if (result) return result;
|
|
@@ -2615,7 +1421,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2615
1421
|
return buildSkippedProjectResult(prepared, projectIndex, reason);
|
|
2616
1422
|
});
|
|
2617
1423
|
const summary = summarize(completedProjectResults);
|
|
2618
|
-
const failed = rootController.signal.aborted || summary.failed > 0 || summary.notRun > 0 || summary.collectionErrors > 0 || summary.documentFailures > 0 || summary.projectFailures > 0;
|
|
1424
|
+
const failed = hasInfrastructureError || rootController.signal.aborted || summary.failed > 0 || summary.notRun > 0 || summary.collectionErrors > 0 || summary.documentFailures > 0 || summary.projectFailures > 0;
|
|
2619
1425
|
const endedAt = new Date();
|
|
2620
1426
|
const result = {
|
|
2621
1427
|
schemaVersion: 3,
|
|
@@ -2632,34 +1438,66 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2632
1438
|
projects: completedProjectResults,
|
|
2633
1439
|
cases: completedProjectResults.flatMap((project)=>project.cases),
|
|
2634
1440
|
documents: completedProjectResults.flatMap((project)=>project.documents),
|
|
2635
|
-
collectionErrors: completedProjectResults.flatMap((project)=>project.collectionErrors)
|
|
1441
|
+
collectionErrors: completedProjectResults.flatMap((project)=>project.collectionErrors),
|
|
1442
|
+
...hasInfrastructureError ? {
|
|
1443
|
+
errors: infrastructureErrors.map(test_runner_namespaceObject.asExecutionError)
|
|
1444
|
+
} : {}
|
|
2636
1445
|
};
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
reportPath
|
|
1446
|
+
let completedResult = result;
|
|
1447
|
+
const publish = async (operation, path, callback)=>{
|
|
1448
|
+
try {
|
|
1449
|
+
await callback();
|
|
1450
|
+
return true;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
recordInfrastructureError(error instanceof test_runner_namespaceObject.WorkflowPublicationError || error instanceof test_runner_namespaceObject.WorkflowExecutionFailure ? error : new test_runner_namespaceObject.WorkflowPublicationError(operation, path, error));
|
|
1453
|
+
completedResult = {
|
|
1454
|
+
...completedResult,
|
|
1455
|
+
status: 'failed',
|
|
1456
|
+
exitCode: 1,
|
|
1457
|
+
errors: infrastructureErrors.map(test_runner_namespaceObject.asExecutionError)
|
|
1458
|
+
};
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
2653
1461
|
};
|
|
2654
|
-
writeTestProjectRunResult({
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
1462
|
+
const writeSummary = ()=>publish('write-result', summaryPath, ()=>writeTestProjectRunResult({
|
|
1463
|
+
projectRoot,
|
|
1464
|
+
...configPath ? {
|
|
1465
|
+
configPath
|
|
1466
|
+
} : {},
|
|
1467
|
+
result: completedResult
|
|
1468
|
+
}));
|
|
1469
|
+
const writeReport = ()=>publish('write-report', reportDir, async ()=>{
|
|
1470
|
+
if (!plan.reportEnabled) return;
|
|
1471
|
+
const reportPath = await new report_namespaceObject.TestRunReportAssembler().assembleAsync({
|
|
1472
|
+
outputDir: reportDir,
|
|
1473
|
+
reportFileName: `midscene-e2e-${runId}`,
|
|
1474
|
+
overwrite: false,
|
|
1475
|
+
sources: (0, test_runner_namespaceObject.collectTestRunReportSources)(completedResult),
|
|
1476
|
+
buildRunnerDump: (index)=>{
|
|
1477
|
+
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, index);
|
|
1478
|
+
const warn = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
|
|
1479
|
+
console: true
|
|
1480
|
+
});
|
|
1481
|
+
for (const diagnostic of dump.diagnostics ?? [])warn(diagnostic.message);
|
|
1482
|
+
return dump;
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
completedResult = {
|
|
1486
|
+
...completedResult,
|
|
1487
|
+
reportPath
|
|
1488
|
+
};
|
|
1489
|
+
});
|
|
1490
|
+
await writeReport();
|
|
1491
|
+
for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
|
|
1492
|
+
await publication.publish(completedResult);
|
|
2660
1493
|
});
|
|
1494
|
+
await writeSummary();
|
|
1495
|
+
if (hasInfrastructureError) throw new test_runner_namespaceObject.WorkflowExecutionFailure(completedResult, infrastructureErrors);
|
|
2661
1496
|
return completedResult;
|
|
2662
1497
|
}
|
|
1498
|
+
async function runTestProject(options = {}) {
|
|
1499
|
+
return runPreparedTestProject(await prepareTestRun(options));
|
|
1500
|
+
}
|
|
2663
1501
|
const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
2664
1502
|
const command = 'nodes' === args[0] ? args[0] : void 0;
|
|
2665
1503
|
const commandOffset = command ? 1 : 0;
|
|
@@ -2668,20 +1506,23 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2668
1506
|
let resultDir;
|
|
2669
1507
|
const projectNames = [];
|
|
2670
1508
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
2671
|
-
const
|
|
1509
|
+
const token = args[index];
|
|
1510
|
+
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
1511
|
+
const arg = equal < 0 ? token : token.slice(0, equal);
|
|
1512
|
+
const inlineValue = equal < 0 ? void 0 : token.slice(equal + 1);
|
|
2672
1513
|
if (!arg.startsWith('-')) {
|
|
2673
1514
|
if (projectRoot) throw new Error('Only one test project directory is allowed.');
|
|
2674
1515
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
2675
1516
|
continue;
|
|
2676
1517
|
}
|
|
2677
1518
|
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
2678
|
-
const value = args[index + 1];
|
|
2679
|
-
if (!value) throw new Error(`${arg} requires a value.`);
|
|
1519
|
+
const value = inlineValue ?? args[index + 1];
|
|
1520
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
2680
1521
|
if ('--config' === arg) configPath = value;
|
|
2681
1522
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
2682
1523
|
else projectNames.push(value);
|
|
2683
|
-
index += 1;
|
|
2684
|
-
} else throw new Error(`Unknown option: ${
|
|
1524
|
+
if (void 0 === inlineValue) index += 1;
|
|
1525
|
+
} else throw new Error(`Unknown option: ${token}`);
|
|
2685
1526
|
}
|
|
2686
1527
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
2687
1528
|
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
@@ -2703,6 +1544,23 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2703
1544
|
error: console.error,
|
|
2704
1545
|
write: (message)=>process.stdout.write(message)
|
|
2705
1546
|
};
|
|
1547
|
+
const testCliHelp = `Midscene Test: run native cases/steps Test projects.
|
|
1548
|
+
|
|
1549
|
+
Usage:
|
|
1550
|
+
midscene-test [file.yaml | directory] [options]
|
|
1551
|
+
midscene-test --config <midscene.config.ts> [options]
|
|
1552
|
+
midscene-test nodes [directory] [--config midscene.config.ts]
|
|
1553
|
+
midscene-test create --help
|
|
1554
|
+
|
|
1555
|
+
Options:
|
|
1556
|
+
--config <path> Native TypeScript or JavaScript Test config
|
|
1557
|
+
--project <name> Select a native execution Project (repeatable)
|
|
1558
|
+
--result-dir <path> Store Test result files in this directory
|
|
1559
|
+
--help, -h Show this help
|
|
1560
|
+
--version Show the package version
|
|
1561
|
+
|
|
1562
|
+
Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
1563
|
+
`;
|
|
2706
1564
|
const test_command_assertDirectory = (path, label)=>{
|
|
2707
1565
|
if (!(0, external_node_fs_.existsSync)(path) || !(0, external_node_fs_.statSync)(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
|
|
2708
1566
|
};
|
|
@@ -2713,6 +1571,7 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2713
1571
|
if (projectRoot) test_command_assertDirectory(projectRoot, 'Test project directory');
|
|
2714
1572
|
const configSearchRoot = projectRoot ?? cwd;
|
|
2715
1573
|
const configPath = options.configPath ? (0, external_node_path_.resolve)(configSearchRoot, options.configPath) : discoverTestConfig(configSearchRoot);
|
|
1574
|
+
if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test nodes --config only accepts a native TypeScript or JavaScript project config: ${configPath}.`);
|
|
2716
1575
|
if (options.configPath && (!configPath || !(0, external_node_fs_.existsSync)(configPath))) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
2717
1576
|
const project = await loadTestProject(configPath);
|
|
2718
1577
|
const projectName = options.projectNames?.[0];
|
|
@@ -2740,10 +1599,18 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2740
1599
|
async function runTestCli(args, io = defaultCliIO) {
|
|
2741
1600
|
try {
|
|
2742
1601
|
if ('create' === args[0]) {
|
|
2743
|
-
const { runCreateCommand } = await __webpack_require__.e("
|
|
1602
|
+
const { runCreateCommand } = await __webpack_require__.e("160").then(__webpack_require__.bind(__webpack_require__, "./src/cli/create-command.ts"));
|
|
2744
1603
|
await runCreateCommand(args.slice(1), io);
|
|
2745
1604
|
return 0;
|
|
2746
1605
|
}
|
|
1606
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1607
|
+
io.log(testCliHelp);
|
|
1608
|
+
return 0;
|
|
1609
|
+
}
|
|
1610
|
+
if (args.includes('--version')) {
|
|
1611
|
+
io.log(package_0.rE);
|
|
1612
|
+
return 0;
|
|
1613
|
+
}
|
|
2747
1614
|
const options = parseTestCliArgs(args);
|
|
2748
1615
|
if ('nodes' === options.command) {
|
|
2749
1616
|
await runNodesCommand(options, io);
|
|
@@ -2753,6 +1620,25 @@ A Node may map a string step value to one declared input field. Each Node sectio
|
|
|
2753
1620
|
...options,
|
|
2754
1621
|
onProgress: (message)=>io.log(message)
|
|
2755
1622
|
});
|
|
1623
|
+
for (const failure of result.collectionErrors)io.error(`midscene-test: ${failure.projectName}/${failure.sourcePath}: ${failure.error.message}`);
|
|
1624
|
+
const finalCases = new Map(result.cases.map((outcome)=>[
|
|
1625
|
+
outcome.caseId,
|
|
1626
|
+
outcome
|
|
1627
|
+
]));
|
|
1628
|
+
for (const outcome of finalCases.values()){
|
|
1629
|
+
if ('failed' !== outcome.status || !outcome.run) continue;
|
|
1630
|
+
const run = outcome.run;
|
|
1631
|
+
const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
|
|
1632
|
+
for (const step of [
|
|
1633
|
+
...run.beforeEach,
|
|
1634
|
+
...run.steps,
|
|
1635
|
+
...run.afterEach
|
|
1636
|
+
])if (step.error) io.error(`midscene-test: ${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}: ${step.error.message}`);
|
|
1637
|
+
for (const error of [
|
|
1638
|
+
...run.executionErrors ?? [],
|
|
1639
|
+
...run.teardownErrors ?? []
|
|
1640
|
+
])io.error(`midscene-test: ${source}: ${error.message}`);
|
|
1641
|
+
}
|
|
2756
1642
|
io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
|
|
2757
1643
|
io.log(`Results: ${result.resultDir}`);
|
|
2758
1644
|
io.log(`Summary: ${result.summaryPath}`);
|