@midscene/test 0.0.0 → 1.10.4-beta-20260723055511.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.
@@ -0,0 +1,1267 @@
1
+ import { z } from "zod/v4";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { isAbsolute, sep } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+ import { JSON_SCHEMA, load } from "js-yaml";
6
+ function _define_property(obj, key, value) {
7
+ if (key in obj) Object.defineProperty(obj, key, {
8
+ value: value,
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true
12
+ });
13
+ else obj[key] = value;
14
+ return obj;
15
+ }
16
+ class WorkflowError extends Error {
17
+ toJSON() {
18
+ return {
19
+ name: this.name,
20
+ message: this.message,
21
+ code: this.code,
22
+ ...void 0 === this.details ? {} : {
23
+ details: this.details
24
+ }
25
+ };
26
+ }
27
+ constructor(message, options = {}){
28
+ super(message, {
29
+ cause: options.cause
30
+ }), _define_property(this, "code", void 0), _define_property(this, "details", void 0);
31
+ this.name = new.target.name;
32
+ this.code = options.code ?? 'WORKFLOW_ERROR';
33
+ this.details = options.details;
34
+ }
35
+ }
36
+ class WorkflowParseError extends WorkflowError {
37
+ constructor(message, details, cause){
38
+ super(message, {
39
+ code: 'WORKFLOW_PARSE_ERROR',
40
+ details,
41
+ cause
42
+ });
43
+ }
44
+ }
45
+ class NodeDefinitionError extends WorkflowError {
46
+ constructor(message, details){
47
+ super(message, {
48
+ code: 'NODE_DEFINITION_ERROR',
49
+ details
50
+ });
51
+ }
52
+ }
53
+ class DuplicateNodeError extends WorkflowError {
54
+ constructor(node){
55
+ super(`Node "${node}" is already registered.`, {
56
+ code: 'DUPLICATE_NODE',
57
+ details: {
58
+ node
59
+ }
60
+ }), _define_property(this, "node", void 0);
61
+ this.node = node;
62
+ }
63
+ }
64
+ class NodeNotFoundError extends WorkflowError {
65
+ constructor(node){
66
+ super(`Node "${node}" is not registered.`, {
67
+ code: 'NODE_NOT_FOUND',
68
+ details: {
69
+ node
70
+ }
71
+ }), _define_property(this, "node", void 0);
72
+ this.node = node;
73
+ }
74
+ }
75
+ class NodeInputValidationError extends WorkflowError {
76
+ static fromZod(node, error) {
77
+ const issues = error.issues.map((issue)=>({
78
+ code: issue.code,
79
+ path: issue.path.map(String).join('.'),
80
+ message: issue.message
81
+ }));
82
+ const firstIssue = issues[0];
83
+ const path = firstIssue?.path || '<root>';
84
+ const message = firstIssue?.message ?? 'invalid input';
85
+ return new NodeInputValidationError(`Node "${node}" input validation failed at "${path}": ${message}`, {
86
+ node,
87
+ issues
88
+ });
89
+ }
90
+ constructor(message, details){
91
+ super(message, {
92
+ code: 'NODE_INPUT_VALIDATION_ERROR',
93
+ details
94
+ });
95
+ }
96
+ }
97
+ class StepTimeoutError extends WorkflowError {
98
+ constructor(timeoutMs, node){
99
+ super(node ? `Node "${node}" timed out after ${timeoutMs}ms.` : `Step timed out after ${timeoutMs}ms.`, {
100
+ code: 'STEP_TIMEOUT',
101
+ details: {
102
+ timeoutMs,
103
+ ...void 0 === node ? {} : {
104
+ node
105
+ }
106
+ }
107
+ }), _define_property(this, "timeoutMs", void 0), _define_property(this, "node", void 0);
108
+ this.timeoutMs = timeoutMs;
109
+ this.node = node;
110
+ }
111
+ }
112
+ class NodeExecutionError extends WorkflowError {
113
+ constructor(node, cause){
114
+ const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
115
+ super(`Node "${node}" failed: ${causeMessage}`, {
116
+ code: 'NODE_EXECUTION_ERROR',
117
+ details: {
118
+ node
119
+ },
120
+ cause
121
+ }), _define_property(this, "node", void 0);
122
+ this.node = node;
123
+ }
124
+ }
125
+ class WorkflowLifecycleError extends WorkflowError {
126
+ constructor(message, details){
127
+ super(message, {
128
+ code: 'WORKFLOW_LIFECYCLE_ERROR',
129
+ details
130
+ });
131
+ }
132
+ }
133
+ class ProjectSetupError extends WorkflowError {
134
+ constructor(cause, details){
135
+ const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
136
+ super(`Project "${details.projectName}" setup failed: ${causeMessage}`, {
137
+ code: 'PROJECT_SETUP_ERROR',
138
+ details,
139
+ cause
140
+ });
141
+ }
142
+ }
143
+ class ProjectTeardownError extends WorkflowError {
144
+ constructor(cause, details){
145
+ const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
146
+ super(`Project "${details.projectName}" teardown failed: ${causeMessage}`, {
147
+ code: 'PROJECT_TEARDOWN_ERROR',
148
+ details,
149
+ cause
150
+ });
151
+ }
152
+ }
153
+ class NodeScopeTeardownError extends WorkflowError {
154
+ constructor(cause, details){
155
+ const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
156
+ super(`${'case' === details.scope ? 'Case attempt' : 'Workflow document'} node teardown failed for "${details.node}": ${causeMessage}`, {
157
+ code: 'NODE_SCOPE_TEARDOWN_ERROR',
158
+ details,
159
+ cause
160
+ });
161
+ }
162
+ }
163
+ class FatalDeviceError extends WorkflowError {
164
+ constructor(message, cause){
165
+ super(message, {
166
+ code: 'FATAL_DEVICE_ERROR',
167
+ cause
168
+ });
169
+ }
170
+ }
171
+ const isFatalDeviceError = (error)=>{
172
+ if (error instanceof FatalDeviceError) return true;
173
+ if (error instanceof WorkflowError && 'FATAL_DEVICE_ERROR' === error.code) return true;
174
+ if (error instanceof Error && /device offline|device not found|(?:adb|bdc|device) connection (?:was )?closed/i.test(error.message)) return true;
175
+ return error instanceof Error && void 0 !== error.cause ? isFatalDeviceError(error.cause) : false;
176
+ };
177
+ class CaseExecutionError extends WorkflowError {
178
+ constructor(result){
179
+ super(`Case "${result.name}" failed.`, {
180
+ code: 'CASE_EXECUTION_FAILED',
181
+ details: {
182
+ caseId: result.caseId,
183
+ runId: result.runId
184
+ }
185
+ }), _define_property(this, "result", void 0);
186
+ this.result = result;
187
+ }
188
+ }
189
+ class WorkflowDocumentExecutionError extends WorkflowError {
190
+ constructor(result){
191
+ super(`Workflow document "${result.sourcePath}" failed.`, {
192
+ code: 'WORKFLOW_DOCUMENT_EXECUTION_FAILED',
193
+ details: {
194
+ documentId: result.documentId,
195
+ documentRunId: result.documentRunId
196
+ }
197
+ }), _define_property(this, "result", void 0);
198
+ this.result = result;
199
+ }
200
+ }
201
+ function normalizeNodeExecutionError(error, node) {
202
+ return error instanceof WorkflowError ? error : new NodeExecutionError(node, error);
203
+ }
204
+ const supportedMetaKeys = new Set([
205
+ 'timeout',
206
+ 'continue-on-error'
207
+ ]);
208
+ function isMapping(value) {
209
+ return 'object' == typeof value && null !== value && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || null === Object.getPrototypeOf(value));
210
+ }
211
+ function formatStep(index) {
212
+ return `step ${index + 1}`;
213
+ }
214
+ function normalizeMeta(value, index) {
215
+ if (void 0 === value) return {
216
+ continueOnError: false
217
+ };
218
+ if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} "$" must be a mapping.`, {
219
+ index
220
+ });
221
+ for (const key of Object.keys(value))if (!supportedMetaKeys.has(key)) throw new WorkflowParseError(`${formatStep(index)} has unsupported engine metadata "${key}".`, {
222
+ index,
223
+ key
224
+ });
225
+ const timeout = value.timeout;
226
+ 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.`, {
227
+ index,
228
+ timeout
229
+ });
230
+ const continueOnError = value['continue-on-error'];
231
+ if (void 0 !== continueOnError && 'boolean' != typeof continueOnError) throw new WorkflowParseError(`${formatStep(index)} "$.continue-on-error" must be a boolean.`, {
232
+ index,
233
+ continueOnError
234
+ });
235
+ return {
236
+ ...void 0 === timeout ? {} : {
237
+ timeoutMs: timeout
238
+ },
239
+ continueOnError: continueOnError ?? false
240
+ };
241
+ }
242
+ function validateCommonNodeInput(input, index) {
243
+ if (void 0 !== input.prompt && 'string' != typeof input.prompt) throw new WorkflowParseError(`${formatStep(index)} "prompt" must be a string.`, {
244
+ index,
245
+ prompt: input.prompt
246
+ });
247
+ }
248
+ function normalizeStep(value, index = 0) {
249
+ if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} must be a mapping.`, {
250
+ index
251
+ });
252
+ const entries = Object.entries(value);
253
+ if (1 !== entries.length) throw new WorkflowParseError(`${formatStep(index)} must contain exactly one node.`, {
254
+ index,
255
+ nodes: Object.keys(value)
256
+ });
257
+ const [node, rawValue] = entries[0];
258
+ if (0 === node.trim().length) throw new WorkflowParseError(`${formatStep(index)} node name must not be empty.`, {
259
+ index
260
+ });
261
+ if ('string' == typeof rawValue) return {
262
+ node,
263
+ input: {
264
+ prompt: rawValue
265
+ },
266
+ meta: {
267
+ continueOnError: false
268
+ }
269
+ };
270
+ if (!isMapping(rawValue)) throw new WorkflowParseError(`${formatStep(index)} value must be a string shorthand or a mapping.`, {
271
+ index,
272
+ node
273
+ });
274
+ const { $: rawMeta, ...input } = rawValue;
275
+ validateCommonNodeInput(input, index);
276
+ return {
277
+ node,
278
+ input,
279
+ meta: normalizeMeta(rawMeta, index)
280
+ };
281
+ }
282
+ function normalizeSteps(steps) {
283
+ if (!Array.isArray(steps)) throw new WorkflowParseError('Steps must be an array.');
284
+ return steps.map((step, index)=>normalizeStep(step, index));
285
+ }
286
+ const validateOptionalText = (value, field, node)=>{
287
+ if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
288
+ node,
289
+ field
290
+ });
291
+ };
292
+ const validateInputSchema = (schema, node)=>{
293
+ if (void 0 === schema) return;
294
+ if (!(schema instanceof z.ZodObject)) throw new NodeDefinitionError(`Node "${node}" inputSchema must be a Zod object schema.`, {
295
+ node,
296
+ field: 'inputSchema'
297
+ });
298
+ if ('$' in schema.shape) throw new NodeDefinitionError(`Node "${node}" inputSchema must not declare "$" as an input property.`, {
299
+ node,
300
+ field: 'inputSchema.$'
301
+ });
302
+ };
303
+ const validateDefinition = (options)=>{
304
+ if (!options || 'object' != typeof options) throw new NodeDefinitionError('Node definition must be an object.');
305
+ if ('string' != typeof options.name || 0 === options.name.trim().length) throw new NodeDefinitionError('Node name must be a non-empty string.');
306
+ validateOptionalText(options.title, 'title', options.name);
307
+ validateOptionalText(options.description, "description", options.name);
308
+ validateInputSchema(options.inputSchema, options.name);
309
+ if ('function' != typeof options.execute) throw new NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
310
+ node: options.name
311
+ });
312
+ };
313
+ function defineNode(options) {
314
+ validateDefinition(options);
315
+ return options;
316
+ }
317
+ function _check_private_redeclaration(obj, privateCollection) {
318
+ if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
319
+ }
320
+ function _class_apply_descriptor_get(receiver, descriptor) {
321
+ if (descriptor.get) return descriptor.get.call(receiver);
322
+ return descriptor.value;
323
+ }
324
+ function _class_extract_field_descriptor(receiver, privateMap, action) {
325
+ if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
326
+ return privateMap.get(receiver);
327
+ }
328
+ function _class_private_field_get(receiver, privateMap) {
329
+ var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
330
+ return _class_apply_descriptor_get(receiver, descriptor);
331
+ }
332
+ function _class_private_field_init(obj, privateMap, value) {
333
+ _check_private_redeclaration(obj, privateMap);
334
+ privateMap.set(obj, value);
335
+ }
336
+ var _nodes = /*#__PURE__*/ new WeakMap();
337
+ class NodeRegistry {
338
+ register(node) {
339
+ defineNode(node);
340
+ if (_class_private_field_get(this, _nodes).has(node.name)) throw new DuplicateNodeError(node.name);
341
+ _class_private_field_get(this, _nodes).set(node.name, node);
342
+ return this;
343
+ }
344
+ get(name) {
345
+ return _class_private_field_get(this, _nodes).get(name);
346
+ }
347
+ require(name) {
348
+ const node = this.get(name);
349
+ if (!node) throw new NodeNotFoundError(name);
350
+ return node;
351
+ }
352
+ has(name) {
353
+ return _class_private_field_get(this, _nodes).has(name);
354
+ }
355
+ names() {
356
+ return [
357
+ ..._class_private_field_get(this, _nodes).keys()
358
+ ];
359
+ }
360
+ definitions() {
361
+ return [
362
+ ..._class_private_field_get(this, _nodes).values()
363
+ ];
364
+ }
365
+ constructor(nodes = []){
366
+ _class_private_field_init(this, _nodes, {
367
+ writable: true,
368
+ value: new Map()
369
+ });
370
+ for (const node of nodes)this.register(node);
371
+ }
372
+ }
373
+ const assertJsonSerializable = (value, label, node)=>{
374
+ const visit = (candidate, path, seen)=>{
375
+ if (null === candidate || 'string' == typeof candidate || 'boolean' == typeof candidate) return;
376
+ if ('number' == typeof candidate) {
377
+ if (Number.isFinite(candidate)) return;
378
+ throw new TypeError(`${path} contains a non-finite number.`);
379
+ }
380
+ if (void 0 === candidate) throw new TypeError(`${path} contains undefined.`);
381
+ if ('object' != typeof candidate) throw new TypeError(`${path} is not JSON-serializable.`);
382
+ if (!Array.isArray(candidate) && Object.getPrototypeOf(candidate) !== Object.prototype && null !== Object.getPrototypeOf(candidate)) throw new TypeError(`${path} must contain only plain objects.`);
383
+ if (seen.has(candidate)) throw new TypeError(`${path} contains a cycle.`);
384
+ seen.add(candidate);
385
+ if (Array.isArray(candidate)) candidate.forEach((child, index)=>visit(child, `${path}[${index}]`, seen));
386
+ else for (const [key, child] of Object.entries(candidate))visit(child, `${path}.${key}`, seen);
387
+ seen.delete(candidate);
388
+ };
389
+ try {
390
+ visit(value, label, new Set());
391
+ } catch (error) {
392
+ throw new NodeExecutionError(node, error);
393
+ }
394
+ };
395
+ const cloneAndFreezeJson = (value)=>{
396
+ if (Array.isArray(value)) return Object.freeze(value.map(cloneAndFreezeJson));
397
+ if ('object' == typeof value && null !== value) return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child])=>[
398
+ key,
399
+ cloneAndFreezeJson(child)
400
+ ])));
401
+ return value;
402
+ };
403
+ const toHistoryEntry = (result, scope)=>{
404
+ assertJsonSerializable(result.input, 'input', result.node);
405
+ if (result.output?.data !== void 0) assertJsonSerializable(result.output.data, 'output.data', result.node);
406
+ return Object.freeze({
407
+ scope,
408
+ phase: result.phase,
409
+ stepIndex: result.stepIndex,
410
+ node: result.node,
411
+ input: cloneAndFreezeJson(result.input),
412
+ ...'string' == typeof result.input?.prompt ? {
413
+ intent: result.input.prompt
414
+ } : {},
415
+ status: 'success' === result.status ? 'passed' : 'failed',
416
+ ...result.output?.summary === void 0 ? {} : {
417
+ summary: result.output.summary
418
+ },
419
+ ...result.output?.data === void 0 ? {} : {
420
+ data: cloneAndFreezeJson(result.output.data)
421
+ },
422
+ ...result.error ? {
423
+ error: Object.freeze({
424
+ name: result.error.name,
425
+ message: result.error.message
426
+ })
427
+ } : {}
428
+ });
429
+ };
430
+ const createHistory = (prefix, results, scope)=>Object.freeze([
431
+ ...prefix,
432
+ ...results.map((result)=>toHistoryEntry(result, scope))
433
+ ]);
434
+ function validateNodeOutput(output, node) {
435
+ if (void 0 === output) return;
436
+ if ('object' != typeof output || null === output || Array.isArray(output)) throw new NodeExecutionError(node, new TypeError('Node output must be an object or undefined.'));
437
+ 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.'));
438
+ if ('data' in output && void 0 !== output.data) assertJsonSerializable(output.data, 'output.data', node);
439
+ return output;
440
+ }
441
+ function createStepResultBase(step, startedAt, phase, stepIndex) {
442
+ const endedAt = new Date();
443
+ return {
444
+ node: step.node,
445
+ phase,
446
+ stepIndex,
447
+ input: step.input,
448
+ meta: step.meta,
449
+ startedAt: startedAt.toISOString(),
450
+ endedAt: endedAt.toISOString(),
451
+ durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
452
+ };
453
+ }
454
+ async function executeNode(step, execute, phase, stepIndex, options) {
455
+ validateCommonNodeInput(step.input, stepIndex);
456
+ const startedAt = new Date();
457
+ const abortController = new AbortController();
458
+ const parentSignal = options.parentSignal;
459
+ const abortFromParent = ()=>abortController.abort(parentSignal?.reason ?? new Error('Workflow execution aborted.'));
460
+ if (parentSignal?.aborted) abortFromParent();
461
+ else parentSignal?.addEventListener('abort', abortFromParent, {
462
+ once: true
463
+ });
464
+ const timeoutMs = step.meta.timeoutMs ?? options.defaultTimeoutMs;
465
+ let timeout;
466
+ let timeoutError;
467
+ const execution = Promise.resolve().then(()=>{
468
+ if (abortController.signal.aborted) throw abortController.signal.reason ?? new Error('Workflow execution aborted.');
469
+ return execute(abortController.signal);
470
+ });
471
+ const timedExecution = void 0 === timeoutMs ? execution : Promise.race([
472
+ execution,
473
+ new Promise((_, reject)=>{
474
+ timeout = setTimeout(()=>{
475
+ timeoutError = new StepTimeoutError(timeoutMs, step.node);
476
+ abortController.abort(timeoutError);
477
+ reject(timeoutError);
478
+ }, timeoutMs);
479
+ })
480
+ ]);
481
+ try {
482
+ const output = validateNodeOutput(await timedExecution, step.node);
483
+ return {
484
+ ...createStepResultBase(step, startedAt, phase, stepIndex),
485
+ status: 'success',
486
+ continuedAfterError: false,
487
+ ...void 0 === output ? {} : {
488
+ output
489
+ }
490
+ };
491
+ } catch (error) {
492
+ return {
493
+ ...createStepResultBase(step, startedAt, phase, stepIndex),
494
+ status: 'failed',
495
+ continuedAfterError: step.meta.continueOnError,
496
+ error: normalizeNodeExecutionError(timeoutError ?? error, step.node)
497
+ };
498
+ } finally{
499
+ if (void 0 !== timeout) clearTimeout(timeout);
500
+ parentSignal?.removeEventListener('abort', abortFromParent);
501
+ }
502
+ }
503
+ async function parseNodeInput(node, input) {
504
+ if (!node.inputSchema) return input;
505
+ const parsed = await node.inputSchema.safeParseAsync(input);
506
+ if (!parsed.success) throw NodeInputValidationError.fromZod(node.name, parsed.error);
507
+ return parsed.data;
508
+ }
509
+ async function executeStep(step, node, target, context, execution = {}) {
510
+ const phase = 'case' === target.scope ? target.case.phase : target.document.phase;
511
+ const stepIndex = 'case' === target.scope ? target.case.stepIndex : target.document.stepIndex;
512
+ return executeNode(step, async (signal)=>{
513
+ const input = await parseNodeInput(node, step.input);
514
+ const common = {
515
+ input,
516
+ $: step.meta,
517
+ signal,
518
+ context,
519
+ history: execution.history ?? Object.freeze([]),
520
+ onTeardown: (teardown)=>{
521
+ if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
522
+ execution.onTeardown(step.node, teardown);
523
+ }
524
+ };
525
+ return 'case' === target.scope ? node.execute({
526
+ ...common,
527
+ scope: 'case',
528
+ case: target.case
529
+ }) : node.execute({
530
+ ...common,
531
+ scope: 'document',
532
+ document: target.document
533
+ });
534
+ }, phase, stepIndex, {
535
+ parentSignal: execution.signal,
536
+ defaultTimeoutMs: execution.defaultTimeoutMs
537
+ });
538
+ }
539
+ const reportPathsFromTeardown = (result)=>{
540
+ if (void 0 === result) return [];
541
+ if ('object' != typeof result || null === result || Array.isArray(result)) throw new TypeError('Node teardown result must be an object or undefined.');
542
+ const paths = result.reportPaths;
543
+ if (void 0 === paths) return [];
544
+ if (!Array.isArray(paths)) throw new TypeError('Node teardown reportPaths must be an array.');
545
+ return paths.map((path, index)=>{
546
+ if ('string' != typeof path || 0 === path.trim().length) throw new TypeError(`Node teardown reportPaths[${index}] must be a non-empty string.`);
547
+ if (!isAbsolute(path)) throw new TypeError(`Node teardown reportPaths[${index}] must be an absolute path.`);
548
+ return path;
549
+ });
550
+ };
551
+ async function runCollectedCase(collectedCase, options) {
552
+ const phases = {
553
+ beforeEach: options.beforeEach ?? [],
554
+ steps: collectedCase.definition.steps,
555
+ afterEach: options.afterEach ?? []
556
+ };
557
+ const nodes = {
558
+ beforeEach: phases.beforeEach.map((step)=>options.resolveNode(step.node)),
559
+ steps: phases.steps.map((step)=>options.resolveNode(step.node)),
560
+ afterEach: phases.afterEach.map((step)=>options.resolveNode(step.node))
561
+ };
562
+ const runId = options.createRunId?.() ?? randomUUID();
563
+ const startedAt = new Date();
564
+ const beforeEach = [];
565
+ const steps = [];
566
+ const afterEach = [];
567
+ const completedNodes = [];
568
+ const teardownStack = [];
569
+ let acceptingTeardowns = true;
570
+ const onTeardown = (node, teardown)=>{
571
+ if (!acceptingTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a case attempt is running.', {
572
+ runId,
573
+ node
574
+ });
575
+ if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
576
+ runId,
577
+ node
578
+ });
579
+ teardownStack.push({
580
+ registrationIndex: teardownStack.length,
581
+ node,
582
+ teardown
583
+ });
584
+ };
585
+ const runPhase = async (phase, results, signal = options.signal)=>{
586
+ for (const [stepIndex, step] of phases[phase].entries()){
587
+ const caseContext = {
588
+ caseId: collectedCase.caseId,
589
+ runId,
590
+ projectName: options.projectName ?? collectedCase.projectId,
591
+ attemptIndex: options.attemptIndex ?? 0,
592
+ name: collectedCase.definition.name,
593
+ sourcePath: collectedCase.sourcePath,
594
+ caseIndex: collectedCase.caseIndex,
595
+ phase,
596
+ stepIndex,
597
+ completedSteps: Object.freeze([
598
+ ...steps
599
+ ]),
600
+ completedNodes: Object.freeze([
601
+ ...completedNodes
602
+ ])
603
+ };
604
+ const stepInfo = {
605
+ scope: 'case',
606
+ node: step.node,
607
+ stepCount: phases[phase].length,
608
+ case: caseContext
609
+ };
610
+ await options.onStepStart?.(stepInfo);
611
+ const result = await executeStep(step, nodes[phase][stepIndex], {
612
+ scope: 'case',
613
+ case: caseContext
614
+ }, options.context, {
615
+ history: createHistory(options.documentHistory ?? [], completedNodes, 'case'),
616
+ signal,
617
+ defaultTimeoutMs: options.defaultTimeoutMs,
618
+ onTeardown
619
+ });
620
+ await options.onStepResult?.(stepInfo, result);
621
+ results.push(result);
622
+ completedNodes.push(result);
623
+ if ('failed' === result.status && !result.continuedAfterError) break;
624
+ }
625
+ };
626
+ let executionError;
627
+ try {
628
+ await runPhase('beforeEach', beforeEach);
629
+ if (!beforeEach.some((step)=>'failed' === step.status)) await runPhase('steps', steps);
630
+ } catch (error) {
631
+ executionError = error;
632
+ }
633
+ try {
634
+ const cleanupSignal = options.signal?.aborted ? new AbortController().signal : options.signal;
635
+ await runPhase('afterEach', afterEach, cleanupSignal);
636
+ } catch (error) {
637
+ executionError ??= error;
638
+ }
639
+ acceptingTeardowns = false;
640
+ const teardownErrors = [];
641
+ const reportPaths = new Set();
642
+ for (const { registrationIndex, node, teardown } of teardownStack.reverse())try {
643
+ for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
644
+ } catch (error) {
645
+ teardownErrors.push(new NodeScopeTeardownError(error, {
646
+ scope: 'case',
647
+ scopeId: runId,
648
+ node,
649
+ registrationIndex
650
+ }));
651
+ }
652
+ if (executionError) throw executionError;
653
+ const endedAt = new Date();
654
+ const result = {
655
+ caseId: collectedCase.caseId,
656
+ runId,
657
+ projectName: options.projectName ?? collectedCase.projectId,
658
+ attemptIndex: options.attemptIndex ?? 0,
659
+ name: collectedCase.definition.name,
660
+ sourcePath: collectedCase.sourcePath,
661
+ caseIndex: collectedCase.caseIndex,
662
+ status: completedNodes.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
663
+ beforeEach,
664
+ steps,
665
+ afterEach,
666
+ ...teardownErrors.length > 0 ? {
667
+ teardownErrors
668
+ } : {},
669
+ ...reportPaths.size > 0 ? {
670
+ reportPaths: [
671
+ ...reportPaths
672
+ ]
673
+ } : {},
674
+ startedAt: startedAt.toISOString(),
675
+ endedAt: endedAt.toISOString(),
676
+ durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
677
+ };
678
+ await options.onResult?.(result);
679
+ return result;
680
+ }
681
+ function case_runner_define_property(obj, key, value) {
682
+ if (key in obj) Object.defineProperty(obj, key, {
683
+ value: value,
684
+ enumerable: true,
685
+ configurable: true,
686
+ writable: true
687
+ });
688
+ else obj[key] = value;
689
+ return obj;
690
+ }
691
+ class CaseRunner {
692
+ register(node) {
693
+ this.registry.register(node);
694
+ return this;
695
+ }
696
+ async run(input) {
697
+ const collectedCase = {
698
+ caseId: 'standalone-case',
699
+ projectId: 'standalone-project',
700
+ sourcePath: '',
701
+ caseIndex: 0,
702
+ definition: {
703
+ name: input.name ?? 'case',
704
+ tags: input.tags ?? [],
705
+ steps: normalizeSteps(input.steps)
706
+ }
707
+ };
708
+ const result = await runCollectedCase(collectedCase, {
709
+ resolveNode: (name)=>this.registry.require(name),
710
+ context: this.context,
711
+ createRunId: ()=>'standalone-case'
712
+ });
713
+ const fatalStepError = result.steps.find((step)=>'failed' === step.status && !step.continuedAfterError)?.error;
714
+ if (fatalStepError) throw fatalStepError;
715
+ return result;
716
+ }
717
+ constructor(options = {}){
718
+ case_runner_define_property(this, "registry", void 0);
719
+ case_runner_define_property(this, "context", void 0);
720
+ this.registry = new NodeRegistry(options.nodes);
721
+ this.context = options.context;
722
+ }
723
+ }
724
+ function createCaseRunner(options = {}) {
725
+ return new CaseRunner(options);
726
+ }
727
+ function createDocumentRuntime(document, options) {
728
+ const documentRunId = options.createDocumentRunId?.() ?? randomUUID();
729
+ const startedAt = new Date();
730
+ const beforeAll = [];
731
+ const afterAll = [];
732
+ const completedNodes = [];
733
+ const nodes = {
734
+ beforeAll: document.lifecycle.beforeAll.map((step)=>options.resolveNode(step.node)),
735
+ afterAll: document.lifecycle.afterAll.map((step)=>options.resolveNode(step.node))
736
+ };
737
+ const project = options.project ?? Object.freeze({
738
+ name: document.projectId,
739
+ platform: 'web',
740
+ tags: Object.freeze({
741
+ include: [],
742
+ exclude: []
743
+ }),
744
+ retry: 0,
745
+ variables: Object.freeze({})
746
+ });
747
+ const context = options.projectContext;
748
+ const signal = options.signal ?? new AbortController().signal;
749
+ const nodeTeardownStack = [];
750
+ let acceptingNodeTeardowns = true;
751
+ let started = false;
752
+ let finishedResult;
753
+ const reportPaths = new Set();
754
+ const createResult = (teardownErrors = [])=>{
755
+ const endedAt = new Date();
756
+ return {
757
+ documentId: document.documentId,
758
+ documentRunId,
759
+ projectId: document.projectId,
760
+ projectName: project.name,
761
+ sourcePath: document.sourcePath,
762
+ status: completedNodes.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
763
+ startedAt: startedAt.toISOString(),
764
+ endedAt: endedAt.toISOString(),
765
+ durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
766
+ beforeAll: [
767
+ ...beforeAll
768
+ ],
769
+ afterAll: [
770
+ ...afterAll
771
+ ],
772
+ ...teardownErrors.length > 0 ? {
773
+ teardownErrors
774
+ } : {},
775
+ ...reportPaths.size > 0 ? {
776
+ reportPaths: [
777
+ ...reportPaths
778
+ ]
779
+ } : {}
780
+ };
781
+ };
782
+ const runPhase = async (phase, results, executionSignal = signal)=>{
783
+ for (const [stepIndex, step] of document.lifecycle[phase].entries()){
784
+ const documentContext = {
785
+ documentId: document.documentId,
786
+ documentRunId,
787
+ projectId: document.projectId,
788
+ projectName: project.name,
789
+ sourcePath: document.sourcePath,
790
+ phase,
791
+ stepIndex,
792
+ completedNodes: Object.freeze([
793
+ ...completedNodes
794
+ ])
795
+ };
796
+ const stepInfo = {
797
+ scope: 'document',
798
+ node: step.node,
799
+ stepCount: document.lifecycle[phase].length,
800
+ document: documentContext
801
+ };
802
+ await options.onStepStart?.(stepInfo);
803
+ const result = await executeStep(step, nodes[phase][stepIndex], {
804
+ scope: 'document',
805
+ document: documentContext
806
+ }, context, {
807
+ history: createHistory([], completedNodes, 'document'),
808
+ signal: executionSignal,
809
+ defaultTimeoutMs: options.defaultTimeoutMs,
810
+ onTeardown (node, teardown) {
811
+ if (!acceptingNodeTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a workflow document is running.', {
812
+ documentId: document.documentId,
813
+ documentRunId,
814
+ node
815
+ });
816
+ if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
817
+ documentId: document.documentId,
818
+ documentRunId,
819
+ node
820
+ });
821
+ nodeTeardownStack.push({
822
+ registrationIndex: nodeTeardownStack.length,
823
+ node,
824
+ teardown
825
+ });
826
+ }
827
+ });
828
+ await options.onStepResult?.(stepInfo, result);
829
+ results.push(result);
830
+ completedNodes.push(result);
831
+ if ('failed' === result.status && !result.continuedAfterError) break;
832
+ }
833
+ };
834
+ return {
835
+ get context () {
836
+ return context;
837
+ },
838
+ get history () {
839
+ return createHistory([], completedNodes, 'document');
840
+ },
841
+ get canRunCases () {
842
+ return started && !signal.aborted && !beforeAll.some((step)=>'failed' === step.status);
843
+ },
844
+ async start () {
845
+ if (started) throw new WorkflowLifecycleError('Workflow document runtime has already started.', {
846
+ documentId: document.documentId,
847
+ documentRunId
848
+ });
849
+ started = true;
850
+ if (!signal.aborted) await runPhase('beforeAll', beforeAll);
851
+ return createResult();
852
+ },
853
+ async finish () {
854
+ if (finishedResult) return finishedResult;
855
+ if (!started) throw new WorkflowLifecycleError('Workflow document runtime must start before it can finish.', {
856
+ documentId: document.documentId,
857
+ documentRunId
858
+ });
859
+ let finishError;
860
+ try {
861
+ await runPhase('afterAll', afterAll, signal.aborted ? new AbortController().signal : signal);
862
+ } catch (error) {
863
+ finishError = error;
864
+ }
865
+ acceptingNodeTeardowns = false;
866
+ const teardownErrors = [];
867
+ for (const { registrationIndex, node, teardown } of nodeTeardownStack.reverse())try {
868
+ for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
869
+ } catch (error) {
870
+ teardownErrors.push(new NodeScopeTeardownError(error, {
871
+ scope: 'document',
872
+ scopeId: documentRunId,
873
+ node,
874
+ registrationIndex
875
+ }));
876
+ }
877
+ finishedResult = createResult(teardownErrors);
878
+ if (finishError) throw finishError;
879
+ await options.onResult?.(finishedResult);
880
+ return finishedResult;
881
+ }
882
+ };
883
+ }
884
+ const asNotRun = (collectedCase, projectName, reason)=>({
885
+ caseId: collectedCase.caseId,
886
+ projectName,
887
+ name: collectedCase.definition.name,
888
+ sourcePath: collectedCase.sourcePath,
889
+ caseIndex: collectedCase.caseIndex,
890
+ status: 'not-run',
891
+ notRunReason: reason
892
+ });
893
+ async function runWorkflowDocument(document, options) {
894
+ const createDocumentRunId = options.createDocumentRunId;
895
+ const createCaseRunId = options.createCaseRunId;
896
+ const projectName = options.project?.name ?? document.projectId;
897
+ const retry = options.retry ?? options.project?.retry ?? 0;
898
+ if (!Number.isInteger(retry) || retry < 0) throw new TypeError('Workflow document retry must be a non-negative integer.');
899
+ const runtime = createDocumentRuntime(document, {
900
+ resolveNode: options.resolveNode,
901
+ project: options.project,
902
+ projectContext: options.projectContext,
903
+ signal: options.signal,
904
+ defaultTimeoutMs: options.defaultTimeoutMs,
905
+ onStepStart: options.onStepStart,
906
+ onStepResult: options.onStepResult,
907
+ onResult: options.onDocumentResult,
908
+ createDocumentRunId: createDocumentRunId ? ()=>createDocumentRunId(document) : void 0
909
+ });
910
+ const cases = [];
911
+ let runtimeStarted = false;
912
+ let documentResult;
913
+ let executionError;
914
+ let fatalError = false;
915
+ try {
916
+ runtimeStarted = true;
917
+ await runtime.start();
918
+ if (runtime.canRunCases) for (const collectedCase of document.cases){
919
+ if (fatalError || options.shouldStop?.()) {
920
+ const outcome = asNotRun(collectedCase, projectName, fatalError ? 'fatal-error' : options.stopReason?.() ?? 'interrupted');
921
+ cases.push(outcome);
922
+ await options.onCaseOutcome?.(outcome);
923
+ continue;
924
+ }
925
+ await options.onCaseStart?.(collectedCase);
926
+ const attempts = [];
927
+ for(let attemptIndex = 0; attemptIndex <= retry; attemptIndex += 1){
928
+ const run = await runCollectedCase(collectedCase, {
929
+ resolveNode: options.resolveNode,
930
+ beforeEach: document.lifecycle.beforeEach,
931
+ afterEach: document.lifecycle.afterEach,
932
+ context: runtime.context,
933
+ projectName,
934
+ attemptIndex,
935
+ documentHistory: runtime.history,
936
+ signal: options.signal,
937
+ defaultTimeoutMs: options.defaultTimeoutMs,
938
+ onStepStart: options.onStepStart,
939
+ onStepResult: options.onStepResult,
940
+ onResult: options.onCaseResult,
941
+ createRunId: createCaseRunId ? ()=>createCaseRunId(collectedCase, attemptIndex) : void 0
942
+ });
943
+ attempts.push(run);
944
+ fatalError = options.isFatalError?.(run) ?? false;
945
+ if ('success' === run.status || fatalError) break;
946
+ }
947
+ const run = attempts.at(-1);
948
+ const outcome = {
949
+ caseId: collectedCase.caseId,
950
+ projectName,
951
+ name: collectedCase.definition.name,
952
+ sourcePath: collectedCase.sourcePath,
953
+ caseIndex: collectedCase.caseIndex,
954
+ status: run.status,
955
+ run,
956
+ attempts
957
+ };
958
+ cases.push(outcome);
959
+ await options.onCaseOutcome?.(outcome);
960
+ }
961
+ else {
962
+ const reason = options.signal?.aborted ? options.stopReason?.() ?? 'interrupted' : 'document-start-failed';
963
+ cases.push(...document.cases.map((collectedCase)=>asNotRun(collectedCase, projectName, reason)));
964
+ }
965
+ } catch (error) {
966
+ executionError = error;
967
+ } finally{
968
+ if (runtimeStarted) try {
969
+ documentResult = await runtime.finish();
970
+ } catch (error) {
971
+ executionError ??= error;
972
+ }
973
+ }
974
+ if (executionError) throw executionError;
975
+ if (!documentResult) throw new Error(`Workflow document "${document.sourcePath}" did not produce a result.`);
976
+ return {
977
+ document: documentResult,
978
+ cases
979
+ };
980
+ }
981
+ const createProjectRuntime = (options)=>{
982
+ const startedAt = new Date();
983
+ const controller = new AbortController();
984
+ const parentSignal = options.signal;
985
+ const abortFromParent = ()=>controller.abort(parentSignal?.reason);
986
+ if (parentSignal?.aborted) abortFromParent();
987
+ else parentSignal?.addEventListener('abort', abortFromParent, {
988
+ once: true
989
+ });
990
+ const teardownStack = [];
991
+ let context;
992
+ let setupError;
993
+ let started = false;
994
+ let acceptingTeardowns = void 0 !== options.setup;
995
+ let finishedResult;
996
+ const createResult = (requestedStatus, teardownErrors = [])=>{
997
+ const endedAt = new Date();
998
+ return {
999
+ projectName: options.project.name,
1000
+ platform: options.project.platform,
1001
+ status: 'failed' === requestedStatus || setupError || teardownErrors.length > 0 ? 'failed' : 'success',
1002
+ ...setupError ? {
1003
+ setupError
1004
+ } : {},
1005
+ ...teardownErrors.length > 0 ? {
1006
+ teardownErrors
1007
+ } : {},
1008
+ startedAt: startedAt.toISOString(),
1009
+ endedAt: endedAt.toISOString(),
1010
+ durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
1011
+ };
1012
+ };
1013
+ const onTeardown = (teardown)=>{
1014
+ if (!acceptingTeardowns) throw new WorkflowLifecycleError('onTeardown() can only be called while Project setup is running.', {
1015
+ projectName: options.project.name
1016
+ });
1017
+ if ('function' != typeof teardown) throw new WorkflowLifecycleError('Project onTeardown() requires a teardown function.', {
1018
+ projectName: options.project.name
1019
+ });
1020
+ teardownStack.push({
1021
+ registrationIndex: teardownStack.length,
1022
+ teardown
1023
+ });
1024
+ };
1025
+ return {
1026
+ get context () {
1027
+ return context;
1028
+ },
1029
+ get signal () {
1030
+ return controller.signal;
1031
+ },
1032
+ get canRun () {
1033
+ return started && !setupError && !controller.signal.aborted;
1034
+ },
1035
+ async start () {
1036
+ if (started) throw new WorkflowLifecycleError('Project runtime has already started.', {
1037
+ projectName: options.project.name
1038
+ });
1039
+ started = true;
1040
+ if (controller.signal.aborted) {
1041
+ acceptingTeardowns = false;
1042
+ return createResult('failed');
1043
+ }
1044
+ if (options.setup) try {
1045
+ context = await options.setup.setup({
1046
+ project: options.project,
1047
+ env: Object.freeze({
1048
+ ...process.env
1049
+ }),
1050
+ signal: controller.signal,
1051
+ onTeardown
1052
+ });
1053
+ } catch (error) {
1054
+ setupError = new ProjectSetupError(error, {
1055
+ projectName: options.project.name
1056
+ });
1057
+ } finally{
1058
+ acceptingTeardowns = false;
1059
+ }
1060
+ return createResult(setupError ? 'failed' : 'success');
1061
+ },
1062
+ async finish (requestedStatus = 'success') {
1063
+ if (finishedResult) return finishedResult;
1064
+ if (!started) throw new WorkflowLifecycleError('Project runtime must start before it can finish.', {
1065
+ projectName: options.project.name
1066
+ });
1067
+ const statusBeforeTeardown = createResult(requestedStatus).status;
1068
+ const teardownErrors = [];
1069
+ for (const { registrationIndex, teardown } of teardownStack.reverse())try {
1070
+ await teardown({
1071
+ project: options.project,
1072
+ context,
1073
+ status: statusBeforeTeardown,
1074
+ ...setupError ? {
1075
+ setupError
1076
+ } : {}
1077
+ });
1078
+ } catch (error) {
1079
+ teardownErrors.push(new ProjectTeardownError(error, {
1080
+ projectName: options.project.name,
1081
+ registrationIndex
1082
+ }));
1083
+ }
1084
+ parentSignal?.removeEventListener('abort', abortFromParent);
1085
+ finishedResult = createResult(requestedStatus, teardownErrors);
1086
+ return finishedResult;
1087
+ },
1088
+ abort (reason) {
1089
+ controller.abort(reason);
1090
+ }
1091
+ };
1092
+ };
1093
+ const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
1094
+ const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
1095
+ const exactProjectExpression = /^\$\{([^{}]+)\}$/;
1096
+ const missingVariable = (kind, name, options)=>{
1097
+ throw new WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
1098
+ ...options.location,
1099
+ variable: name,
1100
+ variableKind: kind
1101
+ });
1102
+ };
1103
+ const projectVariable = (name, options)=>{
1104
+ if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
1105
+ return options.variables[name];
1106
+ };
1107
+ const environmentVariable = (name, options)=>{
1108
+ const value = options.env?.[name];
1109
+ if (void 0 === value) return missingVariable('environment', name, options);
1110
+ return value;
1111
+ };
1112
+ const interpolationValue = (name, value, options)=>{
1113
+ if ('object' == typeof value && null !== value) throw new WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
1114
+ ...options.location,
1115
+ variable: name,
1116
+ variableKind: 'project'
1117
+ });
1118
+ return String(value);
1119
+ };
1120
+ const resolveString = (value, options)=>{
1121
+ const exactEnv = exactEnvExpression.exec(value);
1122
+ if (exactEnv) return environmentVariable(exactEnv[1], options);
1123
+ const exactProject = exactProjectExpression.exec(value);
1124
+ if (exactProject) return projectVariable(exactProject[1], options);
1125
+ return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
1126
+ };
1127
+ const resolveWorkflowVariables = (value, options)=>{
1128
+ if ('string' == typeof value) return resolveString(value, options);
1129
+ if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
1130
+ if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
1131
+ key,
1132
+ resolveWorkflowVariables(child, options)
1133
+ ]));
1134
+ return value;
1135
+ };
1136
+ const collect_isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
1137
+ const rejectUnknownKeys = (value, allowed, label)=>{
1138
+ const allowedSet = new Set(allowed);
1139
+ const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
1140
+ if (unknown.length) throw new WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
1141
+ field: unknown[0]
1142
+ });
1143
+ };
1144
+ const createCaseId = (projectId, sourcePath, caseIndex)=>createHash('sha256').update(JSON.stringify([
1145
+ projectId,
1146
+ sourcePath,
1147
+ caseIndex
1148
+ ])).digest('hex');
1149
+ const createWorkflowDocumentId = (projectId, sourcePath)=>createHash('sha256').update(JSON.stringify([
1150
+ projectId,
1151
+ sourcePath
1152
+ ])).digest('hex');
1153
+ function collectWorkflowDocument(source, options) {
1154
+ let parsed;
1155
+ try {
1156
+ parsed = load(readFileSync(source.absolutePath, 'utf8'), {
1157
+ schema: JSON_SCHEMA
1158
+ });
1159
+ } catch (error) {
1160
+ throw new WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
1161
+ sourcePath: source.sourcePath
1162
+ }, error);
1163
+ }
1164
+ if (!collect_isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
1165
+ rejectUnknownKeys(parsed, [
1166
+ 'beforeAll',
1167
+ 'beforeEach',
1168
+ 'cases',
1169
+ 'afterEach',
1170
+ 'afterAll'
1171
+ ], 'Workflow document');
1172
+ if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new WorkflowParseError('Workflow document must contain a non-empty cases array.');
1173
+ const sourcePath = source.sourcePath.split(sep).join('/');
1174
+ const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
1175
+ ...step,
1176
+ input: resolveWorkflowVariables(step.input, {
1177
+ variables: options.variables,
1178
+ env: options.env ?? process.env,
1179
+ location: {
1180
+ projectName: source.projectId,
1181
+ sourcePath,
1182
+ phase,
1183
+ stepIndex,
1184
+ ...void 0 === caseIndex ? {} : {
1185
+ caseIndex
1186
+ }
1187
+ }
1188
+ })
1189
+ });
1190
+ const normalizeLifecycle = (phase)=>{
1191
+ const value = parsed[phase];
1192
+ if (void 0 === value) return [];
1193
+ if (!Array.isArray(value)) throw new WorkflowParseError(`Workflow document ${phase} must be an array.`, {
1194
+ phase
1195
+ });
1196
+ return normalizeSteps(value).map((normalized, stepIndex)=>{
1197
+ const resolved = options.resolveNode(normalized.node);
1198
+ if (!resolved) throw new WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1199
+ phase,
1200
+ stepIndex,
1201
+ node: normalized.node
1202
+ });
1203
+ return resolveStepVariables(normalized, phase, stepIndex);
1204
+ });
1205
+ };
1206
+ const lifecycle = {
1207
+ beforeAll: normalizeLifecycle('beforeAll'),
1208
+ beforeEach: normalizeLifecycle('beforeEach'),
1209
+ afterEach: normalizeLifecycle('afterEach'),
1210
+ afterAll: normalizeLifecycle('afterAll')
1211
+ };
1212
+ const ids = new Set();
1213
+ const cases = parsed.cases.map((definition, caseIndex)=>{
1214
+ if (!collect_isMapping(definition)) throw new WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
1215
+ caseIndex
1216
+ });
1217
+ rejectUnknownKeys(definition, [
1218
+ 'name',
1219
+ 'tags',
1220
+ 'steps'
1221
+ ], `Case ${caseIndex + 1}`);
1222
+ if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
1223
+ caseIndex
1224
+ });
1225
+ if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
1226
+ caseIndex
1227
+ });
1228
+ const tags = definition.tags ?? [];
1229
+ 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.`, {
1230
+ caseIndex
1231
+ });
1232
+ const steps = normalizeSteps(definition.steps).map((normalized, stepIndex)=>{
1233
+ if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1234
+ caseIndex,
1235
+ stepIndex,
1236
+ node: normalized.node
1237
+ });
1238
+ return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
1239
+ });
1240
+ const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
1241
+ if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
1242
+ caseId
1243
+ });
1244
+ ids.add(caseId);
1245
+ return {
1246
+ caseId,
1247
+ projectId: source.projectId,
1248
+ sourcePath,
1249
+ caseIndex,
1250
+ definition: {
1251
+ name: definition.name,
1252
+ tags: tags,
1253
+ steps
1254
+ }
1255
+ };
1256
+ });
1257
+ return {
1258
+ documentId: createWorkflowDocumentId(source.projectId, sourcePath),
1259
+ projectId: source.projectId,
1260
+ sourcePath,
1261
+ lifecycle,
1262
+ cases
1263
+ };
1264
+ }
1265
+ export { CaseExecutionError, CaseRunner, DuplicateNodeError, FatalDeviceError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError, assertJsonSerializable, collectWorkflowDocument, createCaseId, createCaseRunner, createDocumentRuntime, createHistory, createProjectRuntime, createWorkflowDocumentId, defineNode, isFatalDeviceError, normalizeNodeExecutionError, normalizeStep, normalizeSteps, resolveWorkflowVariables, runWorkflowDocument, toHistoryEntry, validateCommonNodeInput, z };
1266
+
1267
+ //# sourceMappingURL=index.mjs.map