@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/index.mjs CHANGED
@@ -1,1103 +1,32 @@
1
- import { z } from "zod/v4";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { isAbsolute, sep } from "node:path";
1
+ import { CaseExecutionError, CaseRunner, DuplicateNodeError, FatalDeviceError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError, buildTestRunReportDump, classifyWorkflowDocumentFormat, collectTestRunReportSources, createCaseRunner, createDocumentRuntime, createProjectRuntime, defineNode, isFatalDeviceError, normalizeNodeExecutionError, normalizeStep, normalizeSteps, runWorkflowDocument, sanitizeReportError, sanitizeReportText, sanitizeReportValue } from "@midscene/core/internal/test-runner";
4
2
  import { readFileSync } from "node:fs";
3
+ import { sep } from "node:path";
5
4
  import { JSON_SCHEMA, load } from "js-yaml";
6
- import { getDebug } from "@midscene/shared/logger";
7
- function _define_property(obj, key, value) {
8
- if (key in obj) Object.defineProperty(obj, key, {
9
- value: value,
10
- enumerable: true,
11
- configurable: true,
12
- writable: true
13
- });
14
- else obj[key] = value;
15
- return obj;
16
- }
17
- class WorkflowError extends Error {
18
- toJSON() {
19
- return {
20
- name: this.name,
21
- message: this.message,
22
- code: this.code,
23
- ...void 0 === this.details ? {} : {
24
- details: this.details
25
- }
26
- };
27
- }
28
- constructor(message, options = {}){
29
- super(message, {
30
- cause: options.cause
31
- }), _define_property(this, "code", void 0), _define_property(this, "details", void 0);
32
- this.name = new.target.name;
33
- this.code = options.code ?? 'WORKFLOW_ERROR';
34
- this.details = options.details;
35
- }
36
- }
37
- class WorkflowParseError extends WorkflowError {
38
- constructor(message, details, cause){
39
- super(message, {
40
- code: 'WORKFLOW_PARSE_ERROR',
41
- details,
42
- cause
43
- });
44
- }
45
- }
46
- class NodeDefinitionError extends WorkflowError {
47
- constructor(message, details){
48
- super(message, {
49
- code: 'NODE_DEFINITION_ERROR',
50
- details
51
- });
52
- }
53
- }
54
- class DuplicateNodeError extends WorkflowError {
55
- constructor(node){
56
- super(`Node "${node}" is already registered.`, {
57
- code: 'DUPLICATE_NODE',
58
- details: {
59
- node
60
- }
61
- }), _define_property(this, "node", void 0);
62
- this.node = node;
63
- }
64
- }
65
- class NodeNotFoundError extends WorkflowError {
66
- constructor(node){
67
- super(`Node "${node}" is not registered.`, {
68
- code: 'NODE_NOT_FOUND',
69
- details: {
70
- node
71
- }
72
- }), _define_property(this, "node", void 0);
73
- this.node = node;
74
- }
75
- }
76
- class NodeInputValidationError extends WorkflowError {
77
- static fromZod(node, error) {
78
- const issues = error.issues.map((issue)=>({
79
- code: issue.code,
80
- path: issue.path.map(String).join('.'),
81
- message: issue.message
82
- }));
83
- const firstIssue = issues[0];
84
- const path = firstIssue?.path || '<root>';
85
- const message = firstIssue?.message ?? 'invalid input';
86
- return new NodeInputValidationError(`Node "${node}" input validation failed at "${path}": ${message}`, {
87
- node,
88
- issues
89
- });
90
- }
91
- constructor(message, details){
92
- super(message, {
93
- code: 'NODE_INPUT_VALIDATION_ERROR',
94
- details
95
- });
96
- }
97
- }
98
- class StepTimeoutError extends WorkflowError {
99
- constructor(timeoutMs, node){
100
- super(node ? `Node "${node}" timed out after ${timeoutMs}ms.` : `Step timed out after ${timeoutMs}ms.`, {
101
- code: 'STEP_TIMEOUT',
102
- details: {
103
- timeoutMs,
104
- ...void 0 === node ? {} : {
105
- node
106
- }
107
- }
108
- }), _define_property(this, "timeoutMs", void 0), _define_property(this, "node", void 0);
109
- this.timeoutMs = timeoutMs;
110
- this.node = node;
111
- }
112
- }
113
- class NodeExecutionError extends WorkflowError {
114
- constructor(node, cause){
115
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
116
- super(`Node "${node}" failed: ${causeMessage}`, {
117
- code: 'NODE_EXECUTION_ERROR',
118
- details: {
119
- node
120
- },
121
- cause
122
- }), _define_property(this, "node", void 0);
123
- this.node = node;
124
- }
125
- }
126
- class WorkflowLifecycleError extends WorkflowError {
127
- constructor(message, details){
128
- super(message, {
129
- code: 'WORKFLOW_LIFECYCLE_ERROR',
130
- details
131
- });
132
- }
133
- }
134
- class ProjectSetupError extends WorkflowError {
135
- constructor(cause, details){
136
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
137
- super(`Project "${details.projectName}" setup failed: ${causeMessage}`, {
138
- code: 'PROJECT_SETUP_ERROR',
139
- details,
140
- cause
141
- });
142
- }
143
- }
144
- class ProjectTeardownError extends WorkflowError {
145
- constructor(cause, details){
146
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
147
- super(`Project "${details.projectName}" teardown failed: ${causeMessage}`, {
148
- code: 'PROJECT_TEARDOWN_ERROR',
149
- details,
150
- cause
151
- });
152
- }
153
- }
154
- class NodeScopeTeardownError extends WorkflowError {
155
- constructor(cause, details){
156
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
157
- super(`${'case' === details.scope ? 'Case attempt' : 'Workflow document'} node teardown failed for "${details.node}": ${causeMessage}`, {
158
- code: 'NODE_SCOPE_TEARDOWN_ERROR',
159
- details,
160
- cause
161
- });
162
- }
163
- }
164
- class FatalDeviceError extends WorkflowError {
165
- constructor(message, cause){
166
- super(message, {
167
- code: 'FATAL_DEVICE_ERROR',
168
- cause
169
- });
170
- }
171
- }
172
- const isFatalDeviceError = (error)=>{
173
- if (error instanceof FatalDeviceError) return true;
174
- if (error instanceof WorkflowError && 'FATAL_DEVICE_ERROR' === error.code) return true;
175
- if (error instanceof Error && /device offline|device not found|(?:adb|bdc|device) connection (?:was )?closed/i.test(error.message)) return true;
176
- return error instanceof Error && void 0 !== error.cause ? isFatalDeviceError(error.cause) : false;
177
- };
178
- class CaseExecutionError extends WorkflowError {
179
- constructor(result){
180
- super(`Case "${result.name}" failed.`, {
181
- code: 'CASE_EXECUTION_FAILED',
182
- details: {
183
- caseId: result.caseId,
184
- runId: result.runId
185
- }
186
- }), _define_property(this, "result", void 0);
187
- this.result = result;
188
- }
189
- }
190
- class WorkflowDocumentExecutionError extends WorkflowError {
191
- constructor(result){
192
- super(`Workflow document "${result.sourcePath}" failed.`, {
193
- code: 'WORKFLOW_DOCUMENT_EXECUTION_FAILED',
194
- details: {
195
- documentId: result.documentId,
196
- documentRunId: result.documentRunId
197
- }
198
- }), _define_property(this, "result", void 0);
199
- this.result = result;
200
- }
201
- }
202
- function normalizeNodeExecutionError(error, node) {
203
- return error instanceof WorkflowError ? error : new NodeExecutionError(node, error);
204
- }
205
- const supportedMetaKeys = new Set([
206
- 'timeout',
207
- 'continue-on-error'
208
- ]);
209
- function isMapping(value) {
210
- return 'object' == typeof value && null !== value && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || null === Object.getPrototypeOf(value));
211
- }
212
- function formatStep(index) {
213
- return `step ${index + 1}`;
214
- }
215
- function normalizeMeta(value, index) {
216
- if (void 0 === value) return {
217
- continueOnError: false
218
- };
219
- if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} "$" must be a mapping.`, {
220
- index
221
- });
222
- for (const key of Object.keys(value))if (!supportedMetaKeys.has(key)) throw new WorkflowParseError(`${formatStep(index)} has unsupported engine metadata "${key}".`, {
223
- index,
224
- key
225
- });
226
- const timeout = value.timeout;
227
- 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.`, {
228
- index,
229
- timeout
230
- });
231
- const continueOnError = value['continue-on-error'];
232
- if (void 0 !== continueOnError && 'boolean' != typeof continueOnError) throw new WorkflowParseError(`${formatStep(index)} "$.continue-on-error" must be a boolean.`, {
233
- index,
234
- continueOnError
235
- });
236
- return {
237
- ...void 0 === timeout ? {} : {
238
- timeoutMs: timeout
239
- },
240
- continueOnError: continueOnError ?? false
241
- };
242
- }
243
- function normalizeStep(value, index = 0, resolveNode) {
244
- if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} must be a mapping.`, {
245
- index
246
- });
247
- const entries = Object.entries(value);
248
- if (1 !== entries.length) throw new WorkflowParseError(`${formatStep(index)} must contain exactly one node.`, {
249
- index,
250
- nodes: Object.keys(value)
251
- });
252
- const [node, rawValue] = entries[0];
253
- if (0 === node.trim().length) throw new WorkflowParseError(`${formatStep(index)} node name must not be empty.`, {
254
- index
255
- });
256
- if ('string' == typeof rawValue) {
257
- const resolvedNode = resolveNode?.(node);
258
- const stringInputKey = resolvedNode ? resolvedNode.stringInputKey : 'prompt';
259
- if (false === stringInputKey || void 0 === stringInputKey) throw new WorkflowParseError(`${formatStep(index)} node "${node}" does not accept string shorthand.`, {
260
- index,
261
- node
262
- });
263
- return {
264
- node,
265
- input: {
266
- [stringInputKey]: rawValue
267
- },
268
- meta: {
269
- continueOnError: false
270
- }
271
- };
272
- }
273
- if (!isMapping(rawValue)) throw new WorkflowParseError(`${formatStep(index)} value must be a string shorthand or a mapping.`, {
274
- index,
275
- node
276
- });
277
- const { $: rawMeta, ...input } = rawValue;
278
- return {
279
- node,
280
- input,
281
- meta: normalizeMeta(rawMeta, index)
282
- };
283
- }
284
- function normalizeSteps(steps, resolveNode) {
285
- if (!Array.isArray(steps)) throw new WorkflowParseError('Steps must be an array.');
286
- return steps.map((step, index)=>normalizeStep(step, index, resolveNode));
287
- }
288
- const validateOptionalText = (value, field, node)=>{
289
- if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
290
- node,
291
- field
292
- });
293
- };
294
- const validateInputSchema = (schema, node)=>{
295
- if (void 0 === schema) return;
296
- if (!(schema instanceof z.ZodObject)) throw new NodeDefinitionError(`Node "${node}" inputSchema must be a Zod object schema.`, {
297
- node,
298
- field: 'inputSchema'
299
- });
300
- if ('$' in schema.shape) throw new NodeDefinitionError(`Node "${node}" inputSchema must not declare "$" as an input property.`, {
301
- node,
302
- field: 'inputSchema.$'
303
- });
304
- };
305
- const validateDefinition = (options)=>{
306
- if (!options || 'object' != typeof options) throw new NodeDefinitionError('Node definition must be an object.');
307
- if ('string' != typeof options.name || 0 === options.name.trim().length) throw new NodeDefinitionError('Node name must be a non-empty string.');
308
- validateOptionalText(options.title, 'title', options.name);
309
- validateOptionalText(options.description, "description", options.name);
310
- 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.`, {
311
- node: options.name,
312
- field: 'stringInputKey'
313
- });
314
- validateInputSchema(options.inputSchema, options.name);
315
- if ('string' == typeof options.stringInputKey && options.inputSchema instanceof z.ZodObject && !(options.stringInputKey in options.inputSchema.shape)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must reference a field in inputSchema.`, {
316
- node: options.name,
317
- field: 'stringInputKey'
318
- });
319
- if ('function' != typeof options.execute) throw new NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
320
- node: options.name
321
- });
322
- };
323
- function defineNode(options) {
324
- validateDefinition(options);
325
- return options;
326
- }
327
- function _check_private_redeclaration(obj, privateCollection) {
328
- if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
329
- }
330
- function _class_apply_descriptor_get(receiver, descriptor) {
331
- if (descriptor.get) return descriptor.get.call(receiver);
332
- return descriptor.value;
333
- }
334
- function _class_extract_field_descriptor(receiver, privateMap, action) {
335
- if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
336
- return privateMap.get(receiver);
337
- }
338
- function _class_private_field_get(receiver, privateMap) {
339
- var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
340
- return _class_apply_descriptor_get(receiver, descriptor);
341
- }
342
- function _class_private_field_init(obj, privateMap, value) {
343
- _check_private_redeclaration(obj, privateMap);
344
- privateMap.set(obj, value);
345
- }
346
- var _nodes = /*#__PURE__*/ new WeakMap();
347
- class NodeRegistry {
348
- register(node) {
349
- defineNode(node);
350
- if (_class_private_field_get(this, _nodes).has(node.name)) throw new DuplicateNodeError(node.name);
351
- _class_private_field_get(this, _nodes).set(node.name, node);
352
- return this;
353
- }
354
- get(name) {
355
- return _class_private_field_get(this, _nodes).get(name);
356
- }
357
- require(name) {
358
- const node = this.get(name);
359
- if (!node) throw new NodeNotFoundError(name);
360
- return node;
361
- }
362
- has(name) {
363
- return _class_private_field_get(this, _nodes).has(name);
364
- }
365
- names() {
366
- return [
367
- ..._class_private_field_get(this, _nodes).keys()
368
- ];
369
- }
370
- definitions() {
371
- return [
372
- ..._class_private_field_get(this, _nodes).values()
373
- ];
374
- }
375
- constructor(nodes = []){
376
- _class_private_field_init(this, _nodes, {
377
- writable: true,
378
- value: new Map()
379
- });
380
- for (const node of nodes)this.register(node);
381
- }
382
- }
383
- const assertJsonSerializable = (value, label, node)=>{
384
- const visit = (candidate, path, seen)=>{
385
- if (null === candidate || 'string' == typeof candidate || 'boolean' == typeof candidate) return;
386
- if ('number' == typeof candidate) {
387
- if (Number.isFinite(candidate)) return;
388
- throw new TypeError(`${path} contains a non-finite number.`);
389
- }
390
- if (void 0 === candidate) throw new TypeError(`${path} contains undefined.`);
391
- if ('object' != typeof candidate) throw new TypeError(`${path} is not JSON-serializable.`);
392
- if (!Array.isArray(candidate) && Object.getPrototypeOf(candidate) !== Object.prototype && null !== Object.getPrototypeOf(candidate)) throw new TypeError(`${path} must contain only plain objects.`);
393
- if (seen.has(candidate)) throw new TypeError(`${path} contains a cycle.`);
394
- seen.add(candidate);
395
- if (Array.isArray(candidate)) candidate.forEach((child, index)=>visit(child, `${path}[${index}]`, seen));
396
- else for (const [key, child] of Object.entries(candidate))visit(child, `${path}.${key}`, seen);
397
- seen.delete(candidate);
398
- };
399
- try {
400
- visit(value, label, new Set());
401
- } catch (error) {
402
- throw new NodeExecutionError(node, error);
403
- }
404
- };
405
- function validateNodeOutput(output, node) {
406
- if (void 0 === output) return;
407
- if ('object' != typeof output || null === output || Array.isArray(output)) throw new NodeExecutionError(node, new TypeError('Node output must be an object or undefined.'));
408
- if ('summary' in output && void 0 !== output.summary && 'string' != typeof output.summary) throw new NodeExecutionError(node, new TypeError('Node output summary must be a string.'));
409
- if ('data' in output && void 0 !== output.data) assertJsonSerializable(output.data, 'output.data', node);
410
- return output;
411
- }
412
- function createStepResultBase(step, startedAt, phase, stepIndex) {
413
- const endedAt = new Date();
414
- return {
415
- node: step.node,
416
- phase,
417
- stepIndex,
418
- input: step.input,
419
- meta: step.meta,
420
- startedAt: startedAt.toISOString(),
421
- endedAt: endedAt.toISOString(),
422
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
423
- };
424
- }
425
- async function executeNode(step, execute, phase, stepIndex, options) {
426
- const startedAt = new Date();
427
- const abortController = new AbortController();
428
- const parentSignal = options.parentSignal;
429
- const abortFromParent = ()=>abortController.abort(parentSignal?.reason);
430
- if (parentSignal?.aborted) abortFromParent();
431
- else parentSignal?.addEventListener('abort', abortFromParent, {
432
- once: true
433
- });
434
- const timeoutMs = step.meta.timeoutMs ?? options.defaultTimeoutMs;
435
- let timeout;
436
- let abortGraceTimeout;
437
- const execution = Promise.resolve().then(()=>{
438
- abortController.signal.throwIfAborted();
439
- return execute(abortController.signal);
440
- });
441
- let settleOnAbort;
442
- const abortedExecution = new Promise((_, reject)=>{
443
- settleOnAbort = ()=>{
444
- abortGraceTimeout = setTimeout(()=>{
445
- reject(abortController.signal.reason);
446
- }, 0);
447
- };
448
- if (abortController.signal.aborted) settleOnAbort();
449
- else abortController.signal.addEventListener('abort', settleOnAbort, {
450
- once: true
451
- });
452
- });
453
- const timeoutExecution = void 0 === timeoutMs ? void 0 : new Promise((_, reject)=>{
454
- timeout = setTimeout(()=>{
455
- if (abortController.signal.aborted) return void reject(abortController.signal.reason);
456
- const timeoutError = new StepTimeoutError(timeoutMs, step.node);
457
- abortController.abort(timeoutError);
458
- reject(timeoutError);
459
- }, timeoutMs);
460
- });
461
- const settledExecution = Promise.race([
462
- execution,
463
- abortedExecution,
464
- ...void 0 === timeoutExecution ? [] : [
465
- timeoutExecution
466
- ]
467
- ]);
468
- try {
469
- const output = validateNodeOutput(await settledExecution, step.node);
470
- return {
471
- ...createStepResultBase(step, startedAt, phase, stepIndex),
472
- status: 'success',
473
- continuedAfterError: false,
474
- ...void 0 === output ? {} : {
475
- output
476
- }
477
- };
478
- } catch (error) {
479
- return {
480
- ...createStepResultBase(step, startedAt, phase, stepIndex),
481
- status: 'failed',
482
- continuedAfterError: step.meta.continueOnError,
483
- error: normalizeNodeExecutionError(error, step.node)
484
- };
485
- } finally{
486
- if (void 0 !== timeout) clearTimeout(timeout);
487
- if (void 0 !== abortGraceTimeout) clearTimeout(abortGraceTimeout);
488
- if (void 0 !== settleOnAbort) abortController.signal.removeEventListener('abort', settleOnAbort);
489
- parentSignal?.removeEventListener('abort', abortFromParent);
490
- }
491
- }
492
- function createNodeReportCollector(node) {
493
- const traces = new Map();
494
- let accepting = true;
495
- return {
496
- collector: Object.freeze({
497
- addTrace (trace) {
498
- if (!accepting) throw new NodeExecutionError(node, new Error('Report traces can only be added while the current Step is running.'));
499
- if (!trace || 'object' != typeof trace || 'midscene-execution' !== trace.type) throw new NodeExecutionError(node, new TypeError('Report trace type must be "midscene-execution".'));
500
- if ('string' != typeof trace.executionId || 0 === trace.executionId.trim().length) throw new NodeExecutionError(node, new TypeError('Report trace executionId must be non-empty.'));
501
- const normalized = Object.freeze({
502
- type: 'midscene-execution',
503
- executionId: trace.executionId
504
- });
505
- traces.set(`${normalized.type}:${normalized.executionId}`, normalized);
506
- }
507
- }),
508
- close () {
509
- accepting = false;
510
- return Object.freeze([
511
- ...traces.values()
512
- ]);
513
- }
514
- };
515
- }
516
- async function parseNodeInput(node, input) {
517
- if (!node.inputSchema) return input;
518
- const parsed = await node.inputSchema.safeParseAsync(input);
519
- if (!parsed.success) throw NodeInputValidationError.fromZod(node.name, parsed.error);
520
- return parsed.data;
521
- }
522
- async function executeStep(step, node, target, context, execution = {}) {
523
- const phase = 'case' === target.scope ? target.case.phase : target.document.phase;
524
- const stepIndex = 'case' === target.scope ? target.case.stepIndex : target.document.stepIndex;
525
- const reportCollector = createNodeReportCollector(step.node);
526
- let result;
527
- try {
528
- result = await executeNode(step, async (signal)=>{
529
- const input = await parseNodeInput(node, step.input);
530
- signal.throwIfAborted();
531
- const common = {
532
- input,
533
- $: step.meta,
534
- signal,
535
- context,
536
- report: reportCollector.collector,
537
- onTeardown: (teardown)=>{
538
- if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
539
- execution.onTeardown(step.node, teardown);
540
- }
541
- };
542
- return 'case' === target.scope ? node.execute({
543
- ...common,
544
- scope: 'case',
545
- case: target.case
546
- }) : node.execute({
547
- ...common,
548
- scope: 'document',
549
- document: target.document
550
- });
551
- }, phase, stepIndex, {
552
- parentSignal: execution.signal,
553
- defaultTimeoutMs: execution.defaultTimeoutMs
554
- });
555
- } finally{
556
- const traces = reportCollector.close();
557
- if (traces.length > 0 && result) result = {
558
- ...result,
559
- report: {
560
- traces
561
- }
562
- };
563
- }
564
- if (!result) throw new NodeExecutionError(step.node, new Error('Step execution completed without a result.'));
565
- return result;
566
- }
567
- const reportPathsFromTeardown = (result)=>{
568
- if (void 0 === result) return [];
569
- if ('object' != typeof result || null === result || Array.isArray(result)) throw new TypeError('Node teardown result must be an object or undefined.');
570
- const paths = result.reportPaths;
571
- if (void 0 === paths) return [];
572
- if (!Array.isArray(paths)) throw new TypeError('Node teardown reportPaths must be an array.');
573
- return paths.map((path, index)=>{
574
- if ('string' != typeof path || 0 === path.trim().length) throw new TypeError(`Node teardown reportPaths[${index}] must be a non-empty string.`);
575
- if (!isAbsolute(path)) throw new TypeError(`Node teardown reportPaths[${index}] must be an absolute path.`);
576
- return path;
577
- });
578
- };
579
- async function runCollectedCase(collectedCase, options) {
580
- const phases = {
581
- beforeEach: options.beforeEach ?? [],
582
- steps: collectedCase.definition.steps,
583
- afterEach: options.afterEach ?? []
584
- };
585
- const nodes = {
586
- beforeEach: phases.beforeEach.map((step)=>options.resolveNode(step.node)),
587
- steps: phases.steps.map((step)=>options.resolveNode(step.node)),
588
- afterEach: phases.afterEach.map((step)=>options.resolveNode(step.node))
589
- };
590
- const runId = options.createRunId?.() ?? randomUUID();
591
- const startedAt = new Date();
592
- const beforeEach = [];
593
- const steps = [];
594
- const afterEach = [];
595
- const teardownStack = [];
596
- let acceptingTeardowns = true;
597
- const onTeardown = (node, teardown)=>{
598
- if (!acceptingTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a case attempt is running.', {
599
- runId,
600
- node
601
- });
602
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
603
- runId,
604
- node
605
- });
606
- teardownStack.push({
607
- registrationIndex: teardownStack.length,
608
- node,
609
- teardown
610
- });
611
- };
612
- const runPhase = async (phase, results, signal = options.signal)=>{
613
- for (const [stepIndex, step] of phases[phase].entries()){
614
- const caseContext = {
615
- caseId: collectedCase.caseId,
616
- runId,
617
- projectName: options.projectName ?? collectedCase.projectId,
618
- attemptIndex: options.attemptIndex ?? 0,
619
- name: collectedCase.definition.name,
620
- sourcePath: collectedCase.sourcePath,
621
- caseIndex: collectedCase.caseIndex,
622
- phase,
623
- stepIndex
624
- };
625
- const stepInfo = {
626
- scope: 'case',
627
- node: step.node,
628
- stepCount: phases[phase].length,
629
- case: caseContext
630
- };
631
- await options.onStepStart?.(stepInfo);
632
- const result = await executeStep(step, nodes[phase][stepIndex], {
633
- scope: 'case',
634
- case: caseContext
635
- }, options.context, {
636
- signal,
637
- defaultTimeoutMs: options.defaultTimeoutMs,
638
- onTeardown
639
- });
640
- await options.onStepResult?.(stepInfo, result);
641
- results.push(result);
642
- if ('failed' === result.status && !result.continuedAfterError) break;
643
- }
644
- };
645
- let executionError;
646
- try {
647
- await runPhase('beforeEach', beforeEach);
648
- if (!beforeEach.some((step)=>'failed' === step.status)) await runPhase('steps', steps);
649
- } catch (error) {
650
- executionError = error;
651
- }
652
- try {
653
- const cleanupSignal = options.signal?.aborted ? new AbortController().signal : options.signal;
654
- await runPhase('afterEach', afterEach, cleanupSignal);
655
- } catch (error) {
656
- executionError ??= error;
657
- }
658
- acceptingTeardowns = false;
659
- const teardownErrors = [];
660
- const reportPaths = new Set();
661
- for (const { registrationIndex, node, teardown } of teardownStack.reverse())try {
662
- for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
663
- } catch (error) {
664
- teardownErrors.push(new NodeScopeTeardownError(error, {
665
- scope: 'case',
666
- scopeId: runId,
667
- node,
668
- registrationIndex
669
- }));
670
- }
671
- if (executionError) throw executionError;
672
- const endedAt = new Date();
673
- const result = {
674
- caseId: collectedCase.caseId,
675
- runId,
676
- projectName: options.projectName ?? collectedCase.projectId,
677
- attemptIndex: options.attemptIndex ?? 0,
678
- name: collectedCase.definition.name,
679
- sourcePath: collectedCase.sourcePath,
680
- caseIndex: collectedCase.caseIndex,
681
- status: beforeEach.some((step)=>'failed' === step.status) || steps.some((step)=>'failed' === step.status) || afterEach.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
682
- beforeEach,
683
- steps,
684
- afterEach,
685
- ...teardownErrors.length > 0 ? {
686
- teardownErrors
687
- } : {},
688
- ...reportPaths.size > 0 ? {
689
- reportPaths: [
690
- ...reportPaths
691
- ]
692
- } : {},
693
- startedAt: startedAt.toISOString(),
694
- endedAt: endedAt.toISOString(),
695
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
696
- };
697
- await options.onResult?.(result);
698
- return result;
699
- }
700
- function case_runner_define_property(obj, key, value) {
701
- if (key in obj) Object.defineProperty(obj, key, {
702
- value: value,
703
- enumerable: true,
704
- configurable: true,
705
- writable: true
706
- });
707
- else obj[key] = value;
708
- return obj;
709
- }
710
- class CaseRunner {
711
- register(node) {
712
- this.registry.register(node);
713
- return this;
714
- }
715
- async run(input) {
716
- const collectedCase = {
717
- caseId: 'standalone-case',
718
- projectId: 'standalone-project',
719
- sourcePath: '',
720
- caseIndex: 0,
721
- definition: {
722
- name: input.name ?? 'case',
723
- tags: input.tags ?? [],
724
- steps: normalizeSteps(input.steps, (name)=>this.registry.get(name))
725
- }
726
- };
727
- const result = await runCollectedCase(collectedCase, {
728
- resolveNode: (name)=>this.registry.require(name),
729
- context: this.context,
730
- createRunId: ()=>'standalone-case'
731
- });
732
- const fatalStepError = result.steps.find((step)=>'failed' === step.status && !step.continuedAfterError)?.error;
733
- if (fatalStepError) throw fatalStepError;
734
- return result;
735
- }
736
- constructor(options = {}){
737
- case_runner_define_property(this, "registry", void 0);
738
- case_runner_define_property(this, "context", void 0);
739
- this.registry = new NodeRegistry(options.nodes);
740
- this.context = options.context;
741
- }
742
- }
743
- function createCaseRunner(options = {}) {
744
- return new CaseRunner(options);
745
- }
746
- function createDocumentRuntime(document, options) {
747
- const documentRunId = options.createDocumentRunId?.() ?? randomUUID();
748
- const startedAt = new Date();
749
- const beforeAll = [];
750
- const afterAll = [];
751
- const nodes = {
752
- beforeAll: document.lifecycle.beforeAll.map((step)=>options.resolveNode(step.node)),
753
- afterAll: document.lifecycle.afterAll.map((step)=>options.resolveNode(step.node))
754
- };
755
- const project = options.project ?? Object.freeze({
756
- projectId: document.projectId,
757
- name: document.projectId,
758
- tags: Object.freeze({
759
- include: [],
760
- exclude: []
761
- }),
762
- retry: 0,
763
- variables: Object.freeze({})
764
- });
765
- const context = options.projectContext;
766
- const signal = options.signal ?? new AbortController().signal;
767
- const nodeTeardownStack = [];
768
- let acceptingNodeTeardowns = true;
769
- let started = false;
770
- let finishedResult;
771
- const reportPaths = new Set();
772
- const createResult = (teardownErrors = [])=>{
773
- const endedAt = new Date();
774
- return {
775
- documentId: document.documentId,
776
- documentRunId,
777
- projectId: document.projectId,
778
- projectName: project.name,
779
- sourcePath: document.sourcePath,
780
- status: beforeAll.some((step)=>'failed' === step.status) || afterAll.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
781
- startedAt: startedAt.toISOString(),
782
- endedAt: endedAt.toISOString(),
783
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
784
- beforeAll: [
785
- ...beforeAll
786
- ],
787
- afterAll: [
788
- ...afterAll
789
- ],
790
- ...teardownErrors.length > 0 ? {
791
- teardownErrors
792
- } : {},
793
- ...reportPaths.size > 0 ? {
794
- reportPaths: [
795
- ...reportPaths
796
- ]
797
- } : {}
798
- };
799
- };
800
- const runPhase = async (phase, results, executionSignal = signal)=>{
801
- for (const [stepIndex, step] of document.lifecycle[phase].entries()){
802
- const documentContext = {
803
- documentId: document.documentId,
804
- documentRunId,
805
- projectId: document.projectId,
806
- projectName: project.name,
807
- sourcePath: document.sourcePath,
808
- phase,
809
- stepIndex
810
- };
811
- const stepInfo = {
812
- scope: 'document',
813
- node: step.node,
814
- stepCount: document.lifecycle[phase].length,
815
- document: documentContext
816
- };
817
- await options.onStepStart?.(stepInfo);
818
- const result = await executeStep(step, nodes[phase][stepIndex], {
819
- scope: 'document',
820
- document: documentContext
821
- }, context, {
822
- signal: executionSignal,
823
- defaultTimeoutMs: options.defaultTimeoutMs,
824
- onTeardown (node, teardown) {
825
- if (!acceptingNodeTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a workflow document is running.', {
826
- documentId: document.documentId,
827
- documentRunId,
828
- node
829
- });
830
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
831
- documentId: document.documentId,
832
- documentRunId,
833
- node
834
- });
835
- nodeTeardownStack.push({
836
- registrationIndex: nodeTeardownStack.length,
837
- node,
838
- teardown
839
- });
840
- }
841
- });
842
- await options.onStepResult?.(stepInfo, result);
843
- results.push(result);
844
- if ('failed' === result.status && !result.continuedAfterError) break;
845
- }
846
- };
847
- return {
848
- get context () {
849
- return context;
850
- },
851
- get canRunCases () {
852
- return started && !signal.aborted && !beforeAll.some((step)=>'failed' === step.status);
853
- },
854
- async start () {
855
- if (started) throw new WorkflowLifecycleError('Workflow document runtime has already started.', {
856
- documentId: document.documentId,
857
- documentRunId
858
- });
859
- started = true;
860
- if (!signal.aborted) await runPhase('beforeAll', beforeAll);
861
- return createResult();
862
- },
863
- async finish () {
864
- if (finishedResult) return finishedResult;
865
- if (!started) throw new WorkflowLifecycleError('Workflow document runtime must start before it can finish.', {
866
- documentId: document.documentId,
867
- documentRunId
868
- });
869
- let finishError;
870
- try {
871
- await runPhase('afterAll', afterAll, signal.aborted ? new AbortController().signal : signal);
872
- } catch (error) {
873
- finishError = error;
874
- }
875
- acceptingNodeTeardowns = false;
876
- const teardownErrors = [];
877
- for (const { registrationIndex, node, teardown } of nodeTeardownStack.reverse())try {
878
- for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
879
- } catch (error) {
880
- teardownErrors.push(new NodeScopeTeardownError(error, {
881
- scope: 'document',
882
- scopeId: documentRunId,
883
- node,
884
- registrationIndex
885
- }));
886
- }
887
- finishedResult = createResult(teardownErrors);
888
- if (finishError) throw finishError;
889
- await options.onResult?.(finishedResult);
890
- return finishedResult;
891
- }
892
- };
893
- }
894
- const asNotRun = (collectedCase, projectName, reason)=>({
895
- caseId: collectedCase.caseId,
896
- projectName,
897
- name: collectedCase.definition.name,
898
- sourcePath: collectedCase.sourcePath,
899
- caseIndex: collectedCase.caseIndex,
900
- status: 'not-run',
901
- notRunReason: reason
902
- });
903
- async function runWorkflowDocument(document, options) {
904
- const createDocumentRunId = options.createDocumentRunId;
905
- const createCaseRunId = options.createCaseRunId;
906
- const projectName = options.project?.name ?? document.projectId;
907
- const retry = options.retry ?? options.project?.retry ?? 0;
908
- if (!Number.isInteger(retry) || retry < 0) throw new TypeError('Workflow document retry must be a non-negative integer.');
909
- const runtime = createDocumentRuntime(document, {
910
- resolveNode: options.resolveNode,
911
- project: options.project,
912
- projectContext: options.projectContext,
913
- signal: options.signal,
914
- defaultTimeoutMs: options.defaultTimeoutMs,
915
- onStepStart: options.onStepStart,
916
- onStepResult: options.onStepResult,
917
- onResult: options.onDocumentResult,
918
- createDocumentRunId: createDocumentRunId ? ()=>createDocumentRunId(document) : void 0
919
- });
920
- const cases = [];
921
- let runtimeStarted = false;
922
- let documentResult;
923
- let executionError;
924
- let fatalError = false;
925
- try {
926
- runtimeStarted = true;
927
- await runtime.start();
928
- if (runtime.canRunCases) for (const collectedCase of document.cases){
929
- if (fatalError || options.shouldStop?.()) {
930
- const outcome = asNotRun(collectedCase, projectName, fatalError ? 'fatal-error' : options.stopReason?.() ?? 'interrupted');
931
- cases.push(outcome);
932
- await options.onCaseOutcome?.(outcome);
933
- continue;
934
- }
935
- await options.onCaseStart?.(collectedCase);
936
- const attempts = [];
937
- for(let attemptIndex = 0; attemptIndex <= retry; attemptIndex += 1){
938
- const run = await runCollectedCase(collectedCase, {
939
- resolveNode: options.resolveNode,
940
- beforeEach: document.lifecycle.beforeEach,
941
- afterEach: document.lifecycle.afterEach,
942
- context: runtime.context,
943
- projectName,
944
- attemptIndex,
945
- signal: options.signal,
946
- defaultTimeoutMs: options.defaultTimeoutMs,
947
- onStepStart: options.onStepStart,
948
- onStepResult: options.onStepResult,
949
- onResult: options.onCaseResult,
950
- createRunId: createCaseRunId ? ()=>createCaseRunId(collectedCase, attemptIndex) : void 0
951
- });
952
- attempts.push(run);
953
- fatalError = options.isFatalError?.(run) ?? false;
954
- if ('success' === run.status || fatalError) break;
955
- }
956
- const run = attempts.at(-1);
957
- const outcome = {
958
- caseId: collectedCase.caseId,
959
- projectName,
960
- name: collectedCase.definition.name,
961
- sourcePath: collectedCase.sourcePath,
962
- caseIndex: collectedCase.caseIndex,
963
- status: run.status,
964
- run,
965
- attempts
966
- };
967
- cases.push(outcome);
968
- await options.onCaseOutcome?.(outcome);
969
- }
970
- else {
971
- const reason = options.signal?.aborted ? options.stopReason?.() ?? 'interrupted' : 'document-start-failed';
972
- cases.push(...document.cases.map((collectedCase)=>asNotRun(collectedCase, projectName, reason)));
973
- }
974
- } catch (error) {
975
- executionError = error;
976
- } finally{
977
- if (runtimeStarted) try {
978
- documentResult = await runtime.finish();
979
- } catch (error) {
980
- executionError ??= error;
981
- }
982
- }
983
- if (executionError) throw executionError;
984
- if (!documentResult) throw new Error(`Workflow document "${document.sourcePath}" did not produce a result.`);
985
- return {
986
- document: documentResult,
987
- cases
988
- };
989
- }
990
- const createProjectRuntime = (options)=>{
991
- const startedAt = new Date();
992
- const controller = new AbortController();
993
- const parentSignal = options.signal;
994
- const abortFromParent = ()=>controller.abort(parentSignal?.reason);
995
- if (parentSignal?.aborted) abortFromParent();
996
- else parentSignal?.addEventListener('abort', abortFromParent, {
997
- once: true
998
- });
999
- const teardownStack = [];
1000
- let context;
1001
- let setupError;
1002
- let started = false;
1003
- let acceptingTeardowns = void 0 !== options.setup;
1004
- let finishedResult;
1005
- const createResult = (requestedStatus, teardownErrors = [])=>{
1006
- const endedAt = new Date();
1007
- return {
1008
- projectName: options.project.name,
1009
- status: 'failed' === requestedStatus || setupError || teardownErrors.length > 0 ? 'failed' : 'success',
1010
- ...setupError ? {
1011
- setupError
1012
- } : {},
1013
- ...teardownErrors.length > 0 ? {
1014
- teardownErrors
1015
- } : {},
1016
- startedAt: startedAt.toISOString(),
1017
- endedAt: endedAt.toISOString(),
1018
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
1019
- };
1020
- };
1021
- const onTeardown = (teardown)=>{
1022
- if (!acceptingTeardowns) throw new WorkflowLifecycleError('onTeardown() can only be called while Project setup is running.', {
1023
- projectName: options.project.name
1024
- });
1025
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Project onTeardown() requires a teardown function.', {
1026
- projectName: options.project.name
1027
- });
1028
- teardownStack.push({
1029
- registrationIndex: teardownStack.length,
1030
- teardown
1031
- });
1032
- };
1033
- return {
1034
- get context () {
1035
- return context;
1036
- },
1037
- get signal () {
1038
- return controller.signal;
1039
- },
1040
- get canRun () {
1041
- return started && !setupError && !controller.signal.aborted;
1042
- },
1043
- async start () {
1044
- if (started) throw new WorkflowLifecycleError('Project runtime has already started.', {
1045
- projectName: options.project.name
1046
- });
1047
- started = true;
1048
- if (controller.signal.aborted) {
1049
- acceptingTeardowns = false;
1050
- return createResult('failed');
1051
- }
1052
- if (options.setup) try {
1053
- context = await options.setup.setup({
1054
- project: options.project,
1055
- env: Object.freeze({
1056
- ...process.env
1057
- }),
1058
- signal: controller.signal,
1059
- onTeardown
1060
- });
1061
- } catch (error) {
1062
- setupError = new ProjectSetupError(error, {
1063
- projectName: options.project.name
1064
- });
1065
- } finally{
1066
- acceptingTeardowns = false;
1067
- }
1068
- return createResult(setupError ? 'failed' : 'success');
1069
- },
1070
- async finish (requestedStatus = 'success') {
1071
- if (finishedResult) return finishedResult;
1072
- if (!started) throw new WorkflowLifecycleError('Project runtime must start before it can finish.', {
1073
- projectName: options.project.name
1074
- });
1075
- const statusBeforeTeardown = createResult(requestedStatus).status;
1076
- const teardownErrors = [];
1077
- for (const { registrationIndex, teardown } of teardownStack.reverse())try {
1078
- await teardown({
1079
- project: options.project,
1080
- context,
1081
- status: statusBeforeTeardown,
1082
- ...setupError ? {
1083
- setupError
1084
- } : {}
1085
- });
1086
- } catch (error) {
1087
- teardownErrors.push(new ProjectTeardownError(error, {
1088
- projectName: options.project.name,
1089
- registrationIndex
1090
- }));
1091
- }
1092
- parentSignal?.removeEventListener('abort', abortFromParent);
1093
- finishedResult = createResult(requestedStatus, teardownErrors);
1094
- return finishedResult;
1095
- },
1096
- abort (reason) {
1097
- controller.abort(reason);
1098
- }
1099
- };
1100
- };
5
+ import { createHash } from "node:crypto";
6
+ import { z } from "zod/v4";
7
+ const case_runner_CaseRunner = CaseRunner;
8
+ const case_runner_createCaseRunner = createCaseRunner;
9
+ const document_runtime_createDocumentRuntime = createDocumentRuntime;
10
+ const registry_NodeRegistry = NodeRegistry;
11
+ const run_workflow_document_runWorkflowDocument = runWorkflowDocument;
12
+ const define_node_defineNode = defineNode;
13
+ const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
14
+ projectId,
15
+ sourcePath,
16
+ caseIndex,
17
+ ...invocationIndex ? [
18
+ invocationIndex
19
+ ] : []
20
+ ])).digest('hex');
21
+ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
22
+ projectId,
23
+ sourcePath,
24
+ ...invocationIndex ? [
25
+ invocationIndex
26
+ ] : []
27
+ ])).digest('hex');
28
+ const normalize_normalizeStep = normalizeStep;
29
+ const normalize_normalizeSteps = normalizeSteps;
1101
30
  const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
1102
31
  const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
1103
32
  const exactProjectExpression = /^\$\{([^{}]+)\}$/;
@@ -1141,7 +70,27 @@ const resolveWorkflowVariables = (value, options)=>{
1141
70
  ]));
1142
71
  return value;
1143
72
  };
1144
- const collect_isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
73
+ function _define_property(obj, key, value) {
74
+ if (key in obj) Object.defineProperty(obj, key, {
75
+ value: value,
76
+ enumerable: true,
77
+ configurable: true,
78
+ writable: true
79
+ });
80
+ else obj[key] = value;
81
+ return obj;
82
+ }
83
+ const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
84
+ class NativeWorkflowFormatError extends WorkflowParseError {
85
+ constructor(actualFormat, sourcePath){
86
+ 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.`, {
87
+ sourcePath,
88
+ actualFormat,
89
+ expectedFormat: 'native'
90
+ }), _define_property(this, "actualFormat", void 0);
91
+ this.actualFormat = actualFormat;
92
+ }
93
+ }
1145
94
  const rejectUnknownKeys = (value, allowed, label)=>{
1146
95
  const allowedSet = new Set(allowed);
1147
96
  const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
@@ -1149,15 +98,8 @@ const rejectUnknownKeys = (value, allowed, label)=>{
1149
98
  field: unknown[0]
1150
99
  });
1151
100
  };
1152
- const createCaseId = (projectId, sourcePath, caseIndex)=>createHash('sha256').update(JSON.stringify([
1153
- projectId,
1154
- sourcePath,
1155
- caseIndex
1156
- ])).digest('hex');
1157
- const createWorkflowDocumentId = (projectId, sourcePath)=>createHash('sha256').update(JSON.stringify([
1158
- projectId,
1159
- sourcePath
1160
- ])).digest('hex');
101
+ const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
102
+ const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
1161
103
  function collectWorkflowDocument(source, options) {
1162
104
  let parsed;
1163
105
  try {
@@ -1169,7 +111,9 @@ function collectWorkflowDocument(source, options) {
1169
111
  sourcePath: source.sourcePath
1170
112
  }, error);
1171
113
  }
1172
- if (!collect_isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
114
+ if (!isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
115
+ const format = classifyWorkflowDocumentFormat(parsed);
116
+ if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
1173
117
  rejectUnknownKeys(parsed, [
1174
118
  'beforeAll',
1175
119
  'beforeEach',
@@ -1201,7 +145,7 @@ function collectWorkflowDocument(source, options) {
1201
145
  if (!Array.isArray(value)) throw new WorkflowParseError(`Workflow document ${phase} must be an array.`, {
1202
146
  phase
1203
147
  });
1204
- return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
148
+ return normalize_normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
1205
149
  const resolved = options.resolveNode(normalized.node);
1206
150
  if (!resolved) throw new WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1207
151
  phase,
@@ -1219,7 +163,7 @@ function collectWorkflowDocument(source, options) {
1219
163
  };
1220
164
  const ids = new Set();
1221
165
  const cases = parsed.cases.map((definition, caseIndex)=>{
1222
- if (!collect_isMapping(definition)) throw new WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
166
+ if (!isMapping(definition)) throw new WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
1223
167
  caseIndex
1224
168
  });
1225
169
  rejectUnknownKeys(definition, [
@@ -1237,7 +181,7 @@ function collectWorkflowDocument(source, options) {
1237
181
  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.`, {
1238
182
  caseIndex
1239
183
  });
1240
- const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
184
+ const steps = normalize_normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
1241
185
  if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1242
186
  caseIndex,
1243
187
  stepIndex,
@@ -1270,418 +214,6 @@ function collectWorkflowDocument(source, options) {
1270
214
  cases
1271
215
  };
1272
216
  }
1273
- const MAX_DEPTH = 6;
1274
- const MAX_STRING_LENGTH = 2000;
1275
- const MAX_ARRAY_ITEMS = 50;
1276
- const MAX_OBJECT_KEYS = 50;
1277
- const MAX_SERIALIZED_BYTES = 65536;
1278
- const TRUNCATED_VALUE = '[Truncated: display value exceeded 64 KiB]';
1279
- const normalizedSecretKeys = [
1280
- 'password',
1281
- 'passwd',
1282
- 'secret',
1283
- 'token',
1284
- 'authorization',
1285
- 'cookie',
1286
- 'apikey'
1287
- ];
1288
- const normalizedKey = (key)=>key.toLowerCase().replace(/[^a-z0-9]/g, '');
1289
- const isSecretKey = (key)=>{
1290
- const normalized = normalizedKey(key);
1291
- return normalizedSecretKeys.some((secret)=>normalized.includes(secret));
1292
- };
1293
- const objectPath = (parent, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${parent}.${key}` : `${parent}[${JSON.stringify(key)}]`;
1294
- const stringByteLength = (value)=>Buffer.byteLength(value, 'utf8');
1295
- function sanitizeReportText(value, maximumLength = MAX_STRING_LENGTH) {
1296
- return value.length <= maximumLength ? value : `${value.slice(0, maximumLength)}… [truncated]`;
1297
- }
1298
- function sanitizeReportValue(value) {
1299
- const truncatedPaths = new Set();
1300
- const redactedPaths = new Set();
1301
- const ancestors = new WeakSet();
1302
- let remainingBytes = MAX_SERIALIZED_BYTES;
1303
- const consume = (path, text)=>{
1304
- remainingBytes -= stringByteLength(text);
1305
- if (remainingBytes >= 0) return true;
1306
- truncatedPaths.add(path);
1307
- return false;
1308
- };
1309
- const visit = (current, path, depth)=>{
1310
- if (remainingBytes <= 0) {
1311
- truncatedPaths.add(path);
1312
- return '[Truncated: size limit]';
1313
- }
1314
- if (null === current || 'boolean' == typeof current) {
1315
- consume(path, String(current));
1316
- return current;
1317
- }
1318
- if ('string' == typeof current) {
1319
- if (current.length > MAX_STRING_LENGTH) {
1320
- truncatedPaths.add(path);
1321
- const shortened = `${current.slice(0, MAX_STRING_LENGTH)}… [truncated]`;
1322
- consume(path, shortened);
1323
- return shortened;
1324
- }
1325
- consume(path, current);
1326
- return current;
1327
- }
1328
- if ('number' == typeof current) {
1329
- if (Number.isFinite(current)) {
1330
- consume(path, String(current));
1331
- return current;
1332
- }
1333
- truncatedPaths.add(path);
1334
- return `[Unsupported number: ${String(current)}]`;
1335
- }
1336
- if ('bigint' == typeof current) {
1337
- truncatedPaths.add(path);
1338
- return `[Unsupported bigint: ${String(current)}]`;
1339
- }
1340
- if (void 0 === current || 'function' == typeof current || 'symbol' == typeof current) {
1341
- truncatedPaths.add(path);
1342
- return `[Unsupported ${typeof current}]`;
1343
- }
1344
- if (depth >= MAX_DEPTH) {
1345
- truncatedPaths.add(path);
1346
- return '[Truncated: maximum depth]';
1347
- }
1348
- if (current instanceof Date) {
1349
- const date = Number.isNaN(current.getTime()) ? '[Invalid Date]' : current.toISOString();
1350
- consume(path, date);
1351
- return date;
1352
- }
1353
- if (ancestors.has(current)) {
1354
- truncatedPaths.add(path);
1355
- return '[Unsupported circular value]';
1356
- }
1357
- if (Array.isArray(current)) {
1358
- ancestors.add(current);
1359
- const output = [];
1360
- const itemCount = Math.min(current.length, MAX_ARRAY_ITEMS);
1361
- for(let index = 0; index < itemCount; index += 1){
1362
- if (remainingBytes <= 0) break;
1363
- output.push(visit(current[index], `${path}[${index}]`, depth + 1));
1364
- }
1365
- if (current.length > output.length) truncatedPaths.add(path);
1366
- ancestors.delete(current);
1367
- return output;
1368
- }
1369
- const prototype = Object.getPrototypeOf(current);
1370
- if (prototype !== Object.prototype && null !== prototype) {
1371
- truncatedPaths.add(path);
1372
- const constructorName = current.constructor?.name ?? 'object';
1373
- return `[Unsupported object: ${constructorName}]`;
1374
- }
1375
- ancestors.add(current);
1376
- const output = {};
1377
- const keys = Object.keys(current);
1378
- for (const key of keys.slice(0, MAX_OBJECT_KEYS)){
1379
- const childPath = objectPath(path, key);
1380
- if (!consume(childPath, key)) break;
1381
- if (isSecretKey(key)) {
1382
- output[key] = '[REDACTED]';
1383
- redactedPaths.add(childPath);
1384
- } else output[key] = visit(current[key], childPath, depth + 1);
1385
- if (remainingBytes <= 0) break;
1386
- }
1387
- if (keys.length > Object.keys(output).length) truncatedPaths.add(path);
1388
- ancestors.delete(current);
1389
- return output;
1390
- };
1391
- const sanitized = visit(value, '$', 0);
1392
- const serialized = JSON.stringify(sanitized);
1393
- if (void 0 === serialized || stringByteLength(serialized) > MAX_SERIALIZED_BYTES) return {
1394
- value: TRUNCATED_VALUE,
1395
- truncatedPaths: [
1396
- '$'
1397
- ]
1398
- };
1399
- return {
1400
- value: sanitized,
1401
- ...truncatedPaths.size > 0 ? {
1402
- truncatedPaths: [
1403
- ...truncatedPaths
1404
- ]
1405
- } : {},
1406
- ...redactedPaths.size > 0 ? {
1407
- redactedPaths: [
1408
- ...redactedPaths
1409
- ]
1410
- } : {}
1411
- };
1412
- }
1413
- function sanitizeReportError(error) {
1414
- if (error instanceof Error) {
1415
- const workflowLike = error;
1416
- return {
1417
- name: sanitizeReportText(error.name || 'Error', 200),
1418
- message: sanitizeReportText(error.message || String(error)),
1419
- ...'string' == typeof workflowLike.code ? {
1420
- code: sanitizeReportText(workflowLike.code, 200)
1421
- } : {},
1422
- ...void 0 === workflowLike.details ? {} : {
1423
- details: sanitizeReportValue(workflowLike.details)
1424
- }
1425
- };
1426
- }
1427
- return {
1428
- name: 'Error',
1429
- message: sanitizeReportText(String(error))
1430
- };
1431
- }
1432
- const warnReport = getDebug('test-runner:report-assembler', {
1433
- console: true
1434
- });
1435
- const exactMidsceneDetailNodes = new Set([
1436
- 'aiAct',
1437
- 'aiAssert',
1438
- 'recordToReport'
1439
- ]);
1440
- const scopedExecutionKey = (scopeId, executionId)=>`${scopeId}\u0000${executionId}`;
1441
- const reportSourcesForScope = (context, scopeId)=>context.sourcesByScope.get(scopeId) ?? [];
1442
- const addDiagnostic = (context, diagnostic)=>{
1443
- context.diagnostics.push(diagnostic);
1444
- const key = JSON.stringify([
1445
- diagnostic.code,
1446
- diagnostic.scopeId,
1447
- diagnostic.executionId
1448
- ]);
1449
- if (!context.warnedDiagnostics.has(key)) {
1450
- context.warnedDiagnostics.add(key);
1451
- warnReport(diagnostic.message);
1452
- }
1453
- };
1454
- const buildStep = (step, scopeId, context)=>{
1455
- const details = [];
1456
- const unresolvedExecutionIds = [];
1457
- for (const trace of step.report?.traces ?? []){
1458
- const previousScope = context.tracedScopeByExecution.get(trace.executionId);
1459
- if (previousScope && previousScope !== scopeId) throw new Error(`Agent execution ${trace.executionId} was traced by multiple test scopes (${previousScope}, ${scopeId}).`);
1460
- context.tracedScopeByExecution.set(trace.executionId, scopeId);
1461
- const detail = context.detailsByScopeAndExecution.get(scopedExecutionKey(scopeId, trace.executionId));
1462
- if (detail) details.push(detail);
1463
- else unresolvedExecutionIds.push(trace.executionId);
1464
- }
1465
- let agentDetailDiagnostic;
1466
- if (unresolvedExecutionIds.length > 0) {
1467
- agentDetailDiagnostic = `Agent detail could not be resolved for ${unresolvedExecutionIds.join(', ')}.`;
1468
- const sources = reportSourcesForScope(context, scopeId);
1469
- for (const executionId of unresolvedExecutionIds)addDiagnostic(context, {
1470
- level: 'warning',
1471
- code: 'agent-detail-unresolved',
1472
- message: `Agent detail could not be resolved for execution ${executionId} in test scope ${scopeId}.`,
1473
- scopeId,
1474
- executionId,
1475
- ...sources.length > 0 ? {
1476
- sourcePaths: sources.map((source)=>source.sourcePath)
1477
- } : {}
1478
- });
1479
- } else if (0 === details.length && exactMidsceneDetailNodes.has(step.node)) {
1480
- agentDetailDiagnostic = 'This Agent did not expose a stable execution reference for the Step.';
1481
- addDiagnostic(context, {
1482
- level: 'warning',
1483
- code: 'agent-detail-unavailable',
1484
- message: `No stable Agent execution reference was captured for ${step.node} in test scope ${scopeId}.`,
1485
- scopeId,
1486
- ...reportSourcesForScope(context, scopeId).length > 0 ? {
1487
- sourcePaths: reportSourcesForScope(context, scopeId).map((source)=>source.sourcePath)
1488
- } : {}
1489
- });
1490
- }
1491
- const prompt = 'string' == typeof step.input?.prompt ? step.input.prompt : void 0;
1492
- return {
1493
- id: `${scopeId}:${step.phase}:${step.stepIndex}`,
1494
- phase: step.phase,
1495
- stepIndex: step.stepIndex,
1496
- node: step.node,
1497
- ...prompt ? {
1498
- title: sanitizeReportText(prompt, 240)
1499
- } : {},
1500
- status: step.status,
1501
- continuedAfterError: step.continuedAfterError,
1502
- startedAt: step.startedAt,
1503
- endedAt: step.endedAt,
1504
- durationMs: step.durationMs,
1505
- input: sanitizeReportValue(step.input),
1506
- ...step.output ? {
1507
- output: {
1508
- ...void 0 === step.output.summary ? {} : {
1509
- summary: sanitizeReportText(step.output.summary)
1510
- },
1511
- ...void 0 === step.output.data ? {} : {
1512
- data: sanitizeReportValue(step.output.data)
1513
- }
1514
- }
1515
- } : {},
1516
- ...step.error ? {
1517
- error: sanitizeReportError(step.error)
1518
- } : {},
1519
- ...details.length > 0 ? {
1520
- agentDetails: details
1521
- } : {},
1522
- ...agentDetailDiagnostic ? {
1523
- agentDetailDiagnostic
1524
- } : {}
1525
- };
1526
- };
1527
- const buildAttempt = (attempt, context)=>({
1528
- attemptId: attempt.runId,
1529
- attemptIndex: attempt.attemptIndex,
1530
- status: attempt.status,
1531
- startedAt: attempt.startedAt,
1532
- endedAt: attempt.endedAt,
1533
- durationMs: attempt.durationMs,
1534
- beforeEach: attempt.beforeEach.map((step)=>buildStep(step, attempt.runId, context)),
1535
- steps: attempt.steps.map((step)=>buildStep(step, attempt.runId, context)),
1536
- afterEach: attempt.afterEach.map((step)=>buildStep(step, attempt.runId, context)),
1537
- ...attempt.teardownErrors?.length ? {
1538
- teardownErrors: attempt.teardownErrors.map(sanitizeReportError)
1539
- } : {},
1540
- ...reportSourcesForScope(context, attempt.runId).length > 0 ? {
1541
- scopeReportIds: reportSourcesForScope(context, attempt.runId).map((source)=>source.reportId)
1542
- } : {}
1543
- });
1544
- const buildCase = (outcome, context)=>({
1545
- caseId: outcome.caseId,
1546
- name: outcome.name,
1547
- caseIndex: outcome.caseIndex,
1548
- status: outcome.status,
1549
- ...outcome.notRunReason ? {
1550
- notRunReason: outcome.notRunReason
1551
- } : {},
1552
- attempts: (outcome.attempts ?? (outcome.run ? [
1553
- outcome.run
1554
- ] : [])).map((attempt)=>buildAttempt(attempt, context))
1555
- });
1556
- const buildDocument = (documentId, document, cases, context)=>{
1557
- const sourcePath = document?.sourcePath ?? cases[0]?.sourcePath ?? '<unknown>';
1558
- const status = document?.status ?? (cases.some((item)=>'success' !== item.status) ? 'failed' : 'success');
1559
- return {
1560
- documentId,
1561
- sourcePath,
1562
- status,
1563
- ...document ? {
1564
- startedAt: document.startedAt,
1565
- endedAt: document.endedAt,
1566
- durationMs: document.durationMs
1567
- } : {},
1568
- beforeAll: (document?.beforeAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1569
- cases: cases.map((outcome)=>buildCase(outcome, context)),
1570
- afterAll: (document?.afterAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1571
- ...document?.teardownErrors?.length ? {
1572
- teardownErrors: document.teardownErrors.map(sanitizeReportError)
1573
- } : {},
1574
- ...document && reportSourcesForScope(context, document.documentRunId).length > 0 ? {
1575
- scopeReportIds: reportSourcesForScope(context, document.documentRunId).map((source)=>source.reportId)
1576
- } : {}
1577
- };
1578
- };
1579
- const buildProject = (project, context)=>{
1580
- const documentById = new Map(project.documents.map((document)=>[
1581
- document.documentId,
1582
- document
1583
- ]));
1584
- const casesByDocumentId = new Map();
1585
- for (const outcome of project.cases){
1586
- const current = casesByDocumentId.get(outcome.documentId) ?? [];
1587
- current.push(outcome);
1588
- casesByDocumentId.set(outcome.documentId, current);
1589
- }
1590
- const documentIds = new Set([
1591
- ...project.documents.map((document)=>document.documentId),
1592
- ...project.cases.map((outcome)=>outcome.documentId)
1593
- ]);
1594
- return {
1595
- projectId: project.projectId,
1596
- name: project.name,
1597
- status: project.status,
1598
- retry: project.retry,
1599
- ...project.lifecycle ? {
1600
- lifecycle: {
1601
- status: project.lifecycle.status,
1602
- startedAt: project.lifecycle.startedAt,
1603
- endedAt: project.lifecycle.endedAt,
1604
- durationMs: project.lifecycle.durationMs,
1605
- ...project.lifecycle.setupError ? {
1606
- setupError: sanitizeReportError(project.lifecycle.setupError)
1607
- } : {},
1608
- ...project.lifecycle.teardownErrors?.length ? {
1609
- teardownErrors: project.lifecycle.teardownErrors.map(sanitizeReportError)
1610
- } : {}
1611
- }
1612
- } : {},
1613
- documents: [
1614
- ...documentIds
1615
- ].map((documentId)=>buildDocument(documentId, documentById.get(documentId), casesByDocumentId.get(documentId) ?? [], context)),
1616
- collectionErrors: project.collectionErrors.map((collectionError)=>({
1617
- sourcePath: collectionError.sourcePath,
1618
- error: sanitizeReportError(collectionError.error)
1619
- }))
1620
- };
1621
- };
1622
- function collectTestRunReportSources(result) {
1623
- const sources = [];
1624
- for (const project of result.projects){
1625
- for (const document of project.documents)for (const sourcePath of document.reportPaths ?? [])sources.push({
1626
- scopeId: document.documentRunId,
1627
- sourcePath
1628
- });
1629
- for (const outcome of project.cases)for (const attempt of outcome.attempts ?? (outcome.run ? [
1630
- outcome.run
1631
- ] : []))for (const sourcePath of attempt.reportPaths ?? [])sources.push({
1632
- scopeId: attempt.runId,
1633
- sourcePath
1634
- });
1635
- }
1636
- return sources;
1637
- }
1638
- function buildTestRunReportDump(result, index) {
1639
- const sourcesByScope = new Map();
1640
- const detailsByScopeAndExecution = new Map();
1641
- for (const source of index.sources){
1642
- const scopeSources = sourcesByScope.get(source.scopeId) ?? [];
1643
- scopeSources.push(source);
1644
- sourcesByScope.set(source.scopeId, scopeSources);
1645
- for (const executionId of source.executionIds)detailsByScopeAndExecution.set(scopedExecutionKey(source.scopeId, executionId), {
1646
- reportId: source.reportId,
1647
- executionId
1648
- });
1649
- }
1650
- const context = {
1651
- diagnostics: [],
1652
- sourcesByScope,
1653
- detailsByScopeAndExecution,
1654
- tracedScopeByExecution: new Map(),
1655
- warnedDiagnostics: new Set()
1656
- };
1657
- for (const source of index.sources)if (0 === source.executionIds.length) addDiagnostic(context, {
1658
- level: 'warning',
1659
- code: 'source-without-executions',
1660
- message: `Agent report contains no executions: ${source.sourcePath}`,
1661
- scopeId: source.scopeId,
1662
- sourcePaths: [
1663
- source.sourcePath
1664
- ]
1665
- });
1666
- const projects = result.projects.map((project)=>buildProject(project, context));
1667
- return {
1668
- schemaVersion: 1,
1669
- kind: 'test-runner',
1670
- runId: result.runId,
1671
- status: result.status,
1672
- startedAt: result.startedAt,
1673
- endedAt: result.endedAt,
1674
- durationMs: result.durationMs,
1675
- summary: {
1676
- ...result.summary
1677
- },
1678
- metrics: index.metrics,
1679
- projects,
1680
- ...context.diagnostics.length > 0 ? {
1681
- diagnostics: context.diagnostics
1682
- } : {}
1683
- };
1684
- }
1685
- export { CaseExecutionError, CaseRunner, DuplicateNodeError, FatalDeviceError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError, buildTestRunReportDump, collectTestRunReportSources, collectWorkflowDocument, createCaseId, createCaseRunner, createDocumentRuntime, createProjectRuntime, createWorkflowDocumentId, defineNode, isFatalDeviceError, normalizeNodeExecutionError, normalizeStep, normalizeSteps, resolveWorkflowVariables, runWorkflowDocument, sanitizeReportError, sanitizeReportText, sanitizeReportValue, z };
217
+ export { CaseExecutionError, case_runner_CaseRunner as CaseRunner, DuplicateNodeError, FatalDeviceError, NativeWorkflowFormatError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, registry_NodeRegistry as NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError, buildTestRunReportDump, collectTestRunReportSources, collectWorkflowDocument, createCaseId, case_runner_createCaseRunner as createCaseRunner, document_runtime_createDocumentRuntime as createDocumentRuntime, createProjectRuntime, createWorkflowDocumentId, define_node_defineNode as defineNode, isFatalDeviceError, normalizeNodeExecutionError, normalize_normalizeStep as normalizeStep, normalize_normalizeSteps as normalizeSteps, resolveWorkflowVariables, run_workflow_document_runWorkflowDocument as runWorkflowDocument, sanitizeReportError, sanitizeReportText, sanitizeReportValue, z };
1686
218
 
1687
219
  //# sourceMappingURL=index.mjs.map