@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.
@@ -38,276 +38,8 @@ const external_node_path_namespaceObject = require("node:path");
38
38
  const external_node_url_namespaceObject = require("node:url");
39
39
  const api_namespaceObject = require("tsx/cjs/api");
40
40
  const esm_api_namespaceObject = require("tsx/esm/api");
41
- function _define_property(obj, key, value) {
42
- if (key in obj) Object.defineProperty(obj, key, {
43
- value: value,
44
- enumerable: true,
45
- configurable: true,
46
- writable: true
47
- });
48
- else obj[key] = value;
49
- return obj;
50
- }
51
- class WorkflowError extends Error {
52
- toJSON() {
53
- return {
54
- name: this.name,
55
- message: this.message,
56
- code: this.code,
57
- ...void 0 === this.details ? {} : {
58
- details: this.details
59
- }
60
- };
61
- }
62
- constructor(message, options = {}){
63
- super(message, {
64
- cause: options.cause
65
- }), _define_property(this, "code", void 0), _define_property(this, "details", void 0);
66
- this.name = new.target.name;
67
- this.code = options.code ?? 'WORKFLOW_ERROR';
68
- this.details = options.details;
69
- }
70
- }
71
- class WorkflowParseError extends WorkflowError {
72
- constructor(message, details, cause){
73
- super(message, {
74
- code: 'WORKFLOW_PARSE_ERROR',
75
- details,
76
- cause
77
- });
78
- }
79
- }
80
- class NodeDefinitionError extends WorkflowError {
81
- constructor(message, details){
82
- super(message, {
83
- code: 'NODE_DEFINITION_ERROR',
84
- details
85
- });
86
- }
87
- }
88
- class DuplicateNodeError extends WorkflowError {
89
- constructor(node){
90
- super(`Node "${node}" is already registered.`, {
91
- code: 'DUPLICATE_NODE',
92
- details: {
93
- node
94
- }
95
- }), _define_property(this, "node", void 0);
96
- this.node = node;
97
- }
98
- }
99
- class NodeNotFoundError extends WorkflowError {
100
- constructor(node){
101
- super(`Node "${node}" is not registered.`, {
102
- code: 'NODE_NOT_FOUND',
103
- details: {
104
- node
105
- }
106
- }), _define_property(this, "node", void 0);
107
- this.node = node;
108
- }
109
- }
110
- class NodeInputValidationError extends WorkflowError {
111
- static fromZod(node, error) {
112
- const issues = error.issues.map((issue)=>({
113
- code: issue.code,
114
- path: issue.path.map(String).join('.'),
115
- message: issue.message
116
- }));
117
- const firstIssue = issues[0];
118
- const path = firstIssue?.path || '<root>';
119
- const message = firstIssue?.message ?? 'invalid input';
120
- return new NodeInputValidationError(`Node "${node}" input validation failed at "${path}": ${message}`, {
121
- node,
122
- issues
123
- });
124
- }
125
- constructor(message, details){
126
- super(message, {
127
- code: 'NODE_INPUT_VALIDATION_ERROR',
128
- details
129
- });
130
- }
131
- }
132
- class StepTimeoutError extends WorkflowError {
133
- constructor(timeoutMs, node){
134
- super(node ? `Node "${node}" timed out after ${timeoutMs}ms.` : `Step timed out after ${timeoutMs}ms.`, {
135
- code: 'STEP_TIMEOUT',
136
- details: {
137
- timeoutMs,
138
- ...void 0 === node ? {} : {
139
- node
140
- }
141
- }
142
- }), _define_property(this, "timeoutMs", void 0), _define_property(this, "node", void 0);
143
- this.timeoutMs = timeoutMs;
144
- this.node = node;
145
- }
146
- }
147
- class NodeExecutionError extends WorkflowError {
148
- constructor(node, cause){
149
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
150
- super(`Node "${node}" failed: ${causeMessage}`, {
151
- code: 'NODE_EXECUTION_ERROR',
152
- details: {
153
- node
154
- },
155
- cause
156
- }), _define_property(this, "node", void 0);
157
- this.node = node;
158
- }
159
- }
160
- class WorkflowLifecycleError extends WorkflowError {
161
- constructor(message, details){
162
- super(message, {
163
- code: 'WORKFLOW_LIFECYCLE_ERROR',
164
- details
165
- });
166
- }
167
- }
168
- class ProjectSetupError extends WorkflowError {
169
- constructor(cause, details){
170
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
171
- super(`Project "${details.projectName}" setup failed: ${causeMessage}`, {
172
- code: 'PROJECT_SETUP_ERROR',
173
- details,
174
- cause
175
- });
176
- }
177
- }
178
- class ProjectTeardownError extends WorkflowError {
179
- constructor(cause, details){
180
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
181
- super(`Project "${details.projectName}" teardown failed: ${causeMessage}`, {
182
- code: 'PROJECT_TEARDOWN_ERROR',
183
- details,
184
- cause
185
- });
186
- }
187
- }
188
- class NodeScopeTeardownError extends WorkflowError {
189
- constructor(cause, details){
190
- const causeMessage = cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');
191
- super(`${'case' === details.scope ? 'Case attempt' : 'Workflow document'} node teardown failed for "${details.node}": ${causeMessage}`, {
192
- code: 'NODE_SCOPE_TEARDOWN_ERROR',
193
- details,
194
- cause
195
- });
196
- }
197
- }
198
- class FatalDeviceError extends WorkflowError {
199
- constructor(message, cause){
200
- super(message, {
201
- code: 'FATAL_DEVICE_ERROR',
202
- cause
203
- });
204
- }
205
- }
206
- const isFatalDeviceError = (error)=>{
207
- if (error instanceof FatalDeviceError) return true;
208
- if (error instanceof WorkflowError && 'FATAL_DEVICE_ERROR' === error.code) return true;
209
- if (error instanceof Error && /device offline|device not found|(?:adb|bdc|device) connection (?:was )?closed/i.test(error.message)) return true;
210
- return error instanceof Error && void 0 !== error.cause ? isFatalDeviceError(error.cause) : false;
211
- };
212
- function normalizeNodeExecutionError(error, node) {
213
- return error instanceof WorkflowError ? error : new NodeExecutionError(node, error);
214
- }
215
- const v4_namespaceObject = require("zod/v4");
216
- const validateOptionalText = (value, field, node)=>{
217
- if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
218
- node,
219
- field
220
- });
221
- };
222
- const validateInputSchema = (schema, node)=>{
223
- if (void 0 === schema) return;
224
- if (!(schema instanceof v4_namespaceObject.z.ZodObject)) throw new NodeDefinitionError(`Node "${node}" inputSchema must be a Zod object schema.`, {
225
- node,
226
- field: 'inputSchema'
227
- });
228
- if ('$' in schema.shape) throw new NodeDefinitionError(`Node "${node}" inputSchema must not declare "$" as an input property.`, {
229
- node,
230
- field: 'inputSchema.$'
231
- });
232
- };
233
- const validateDefinition = (options)=>{
234
- if (!options || 'object' != typeof options) throw new NodeDefinitionError('Node definition must be an object.');
235
- if ('string' != typeof options.name || 0 === options.name.trim().length) throw new NodeDefinitionError('Node name must be a non-empty string.');
236
- validateOptionalText(options.title, 'title', options.name);
237
- validateOptionalText(options.description, "description", options.name);
238
- 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.`, {
239
- node: options.name,
240
- field: 'stringInputKey'
241
- });
242
- validateInputSchema(options.inputSchema, options.name);
243
- if ('string' == typeof options.stringInputKey && options.inputSchema instanceof v4_namespaceObject.z.ZodObject && !(options.stringInputKey in options.inputSchema.shape)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must reference a field in inputSchema.`, {
244
- node: options.name,
245
- field: 'stringInputKey'
246
- });
247
- if ('function' != typeof options.execute) throw new NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
248
- node: options.name
249
- });
250
- };
251
- function defineNode(options) {
252
- validateDefinition(options);
253
- return options;
254
- }
255
- function _check_private_redeclaration(obj, privateCollection) {
256
- if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
257
- }
258
- function _class_apply_descriptor_get(receiver, descriptor) {
259
- if (descriptor.get) return descriptor.get.call(receiver);
260
- return descriptor.value;
261
- }
262
- function _class_extract_field_descriptor(receiver, privateMap, action) {
263
- if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
264
- return privateMap.get(receiver);
265
- }
266
- function _class_private_field_get(receiver, privateMap) {
267
- var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
268
- return _class_apply_descriptor_get(receiver, descriptor);
269
- }
270
- function _class_private_field_init(obj, privateMap, value) {
271
- _check_private_redeclaration(obj, privateMap);
272
- privateMap.set(obj, value);
273
- }
274
- var _nodes = /*#__PURE__*/ new WeakMap();
275
- class NodeRegistry {
276
- register(node) {
277
- defineNode(node);
278
- if (_class_private_field_get(this, _nodes).has(node.name)) throw new DuplicateNodeError(node.name);
279
- _class_private_field_get(this, _nodes).set(node.name, node);
280
- return this;
281
- }
282
- get(name) {
283
- return _class_private_field_get(this, _nodes).get(name);
284
- }
285
- require(name) {
286
- const node = this.get(name);
287
- if (!node) throw new NodeNotFoundError(name);
288
- return node;
289
- }
290
- has(name) {
291
- return _class_private_field_get(this, _nodes).has(name);
292
- }
293
- names() {
294
- return [
295
- ..._class_private_field_get(this, _nodes).keys()
296
- ];
297
- }
298
- definitions() {
299
- return [
300
- ..._class_private_field_get(this, _nodes).values()
301
- ];
302
- }
303
- constructor(nodes = []){
304
- _class_private_field_init(this, _nodes, {
305
- writable: true,
306
- value: new Map()
307
- });
308
- for (const node of nodes)this.register(node);
309
- }
310
- }
41
+ const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
42
+ const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
311
43
  const projectIdFromIndex = (index)=>`project-${index}`;
312
44
  const defineTestProject = (definition)=>definition;
313
45
  const defineProjectSetup = (definition)=>definition;
@@ -347,6 +79,10 @@ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = fal
347
79
  const validateTestFileSelection = (value, label = 'files')=>{
348
80
  if (void 0 === value) return;
349
81
  if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
82
+ rejectUnknownKeys(value, [
83
+ 'include',
84
+ 'exclude'
85
+ ], label);
350
86
  const include = validatePatterns(value.include, 'include', label);
351
87
  const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
352
88
  return Object.freeze({
@@ -402,7 +138,7 @@ const validateVariables = (value, label)=>{
402
138
  assertJsonValue(value, label, new Set());
403
139
  return deepFreezeJson(value);
404
140
  };
405
- const validateProjectSetup = (value, label)=>{
141
+ const validateSetup = (value, label)=>{
406
142
  if (void 0 === value) return;
407
143
  if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
408
144
  rejectUnknownKeys(value, [
@@ -425,7 +161,7 @@ const validateNonNegativeInteger = (value, fallback, label)=>{
425
161
  };
426
162
  const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
427
163
  if (void 0 === value) {
428
- const setup = validateProjectSetup(defaultSetup, 'setup');
164
+ const setup = validateSetup(defaultSetup, 'setup');
429
165
  return {
430
166
  hasExplicitProjects: false,
431
167
  projects: Object.freeze([
@@ -482,7 +218,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
482
218
  retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
483
219
  variables: validateVariables(candidate.variables, `${label}.variables`),
484
220
  ...void 0 === candidate.setup ? {} : {
485
- setup: validateProjectSetup(candidate.setup, `${label}.setup`)
221
+ setup: validateSetup(candidate.setup, `${label}.setup`)
486
222
  }
487
223
  });
488
224
  });
@@ -577,772 +313,31 @@ async function loadTestProject(configPath) {
577
313
  }
578
314
  return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
579
315
  }
580
- const external_node_crypto_namespaceObject = require("node:crypto");
581
- const external_node_events_namespaceObject = require("node:events");
582
316
  const external_node_fs_namespaceObject = require("node:fs");
583
- const report_namespaceObject = require("@midscene/core/report");
584
317
  const external_tinyglobby_namespaceObject = require("tinyglobby");
585
- const createProjectRuntime = (options)=>{
586
- const startedAt = new Date();
587
- const controller = new AbortController();
588
- const parentSignal = options.signal;
589
- const abortFromParent = ()=>controller.abort(parentSignal?.reason);
590
- if (parentSignal?.aborted) abortFromParent();
591
- else parentSignal?.addEventListener('abort', abortFromParent, {
592
- once: true
593
- });
594
- const teardownStack = [];
595
- let context;
596
- let setupError;
597
- let started = false;
598
- let acceptingTeardowns = void 0 !== options.setup;
599
- let finishedResult;
600
- const createResult = (requestedStatus, teardownErrors = [])=>{
601
- const endedAt = new Date();
602
- return {
603
- projectName: options.project.name,
604
- status: 'failed' === requestedStatus || setupError || teardownErrors.length > 0 ? 'failed' : 'success',
605
- ...setupError ? {
606
- setupError
607
- } : {},
608
- ...teardownErrors.length > 0 ? {
609
- teardownErrors
610
- } : {},
611
- startedAt: startedAt.toISOString(),
612
- endedAt: endedAt.toISOString(),
613
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
614
- };
615
- };
616
- const onTeardown = (teardown)=>{
617
- if (!acceptingTeardowns) throw new WorkflowLifecycleError('onTeardown() can only be called while Project setup is running.', {
618
- projectName: options.project.name
619
- });
620
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Project onTeardown() requires a teardown function.', {
621
- projectName: options.project.name
622
- });
623
- teardownStack.push({
624
- registrationIndex: teardownStack.length,
625
- teardown
626
- });
627
- };
628
- return {
629
- get context () {
630
- return context;
631
- },
632
- get signal () {
633
- return controller.signal;
634
- },
635
- get canRun () {
636
- return started && !setupError && !controller.signal.aborted;
637
- },
638
- async start () {
639
- if (started) throw new WorkflowLifecycleError('Project runtime has already started.', {
640
- projectName: options.project.name
641
- });
642
- started = true;
643
- if (controller.signal.aborted) {
644
- acceptingTeardowns = false;
645
- return createResult('failed');
646
- }
647
- if (options.setup) try {
648
- context = await options.setup.setup({
649
- project: options.project,
650
- env: Object.freeze({
651
- ...process.env
652
- }),
653
- signal: controller.signal,
654
- onTeardown
655
- });
656
- } catch (error) {
657
- setupError = new ProjectSetupError(error, {
658
- projectName: options.project.name
659
- });
660
- } finally{
661
- acceptingTeardowns = false;
662
- }
663
- return createResult(setupError ? 'failed' : 'success');
664
- },
665
- async finish (requestedStatus = 'success') {
666
- if (finishedResult) return finishedResult;
667
- if (!started) throw new WorkflowLifecycleError('Project runtime must start before it can finish.', {
668
- projectName: options.project.name
669
- });
670
- const statusBeforeTeardown = createResult(requestedStatus).status;
671
- const teardownErrors = [];
672
- for (const { registrationIndex, teardown } of teardownStack.reverse())try {
673
- await teardown({
674
- project: options.project,
675
- context,
676
- status: statusBeforeTeardown,
677
- ...setupError ? {
678
- setupError
679
- } : {}
680
- });
681
- } catch (error) {
682
- teardownErrors.push(new ProjectTeardownError(error, {
683
- projectName: options.project.name,
684
- registrationIndex
685
- }));
686
- }
687
- parentSignal?.removeEventListener('abort', abortFromParent);
688
- finishedResult = createResult(requestedStatus, teardownErrors);
689
- return finishedResult;
690
- },
691
- abort (reason) {
692
- controller.abort(reason);
693
- }
694
- };
695
- };
696
- const assertJsonSerializable = (value, label, node)=>{
697
- const visit = (candidate, path, seen)=>{
698
- if (null === candidate || 'string' == typeof candidate || 'boolean' == typeof candidate) return;
699
- if ('number' == typeof candidate) {
700
- if (Number.isFinite(candidate)) return;
701
- throw new TypeError(`${path} contains a non-finite number.`);
702
- }
703
- if (void 0 === candidate) throw new TypeError(`${path} contains undefined.`);
704
- if ('object' != typeof candidate) throw new TypeError(`${path} is not JSON-serializable.`);
705
- if (!Array.isArray(candidate) && Object.getPrototypeOf(candidate) !== Object.prototype && null !== Object.getPrototypeOf(candidate)) throw new TypeError(`${path} must contain only plain objects.`);
706
- if (seen.has(candidate)) throw new TypeError(`${path} contains a cycle.`);
707
- seen.add(candidate);
708
- if (Array.isArray(candidate)) candidate.forEach((child, index)=>visit(child, `${path}[${index}]`, seen));
709
- else for (const [key, child] of Object.entries(candidate))visit(child, `${path}.${key}`, seen);
710
- seen.delete(candidate);
711
- };
712
- try {
713
- visit(value, label, new Set());
714
- } catch (error) {
715
- throw new NodeExecutionError(node, error);
716
- }
717
- };
718
- function validateNodeOutput(output, node) {
719
- if (void 0 === output) return;
720
- if ('object' != typeof output || null === output || Array.isArray(output)) throw new NodeExecutionError(node, new TypeError('Node output must be an object or undefined.'));
721
- 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.'));
722
- if ('data' in output && void 0 !== output.data) assertJsonSerializable(output.data, 'output.data', node);
723
- return output;
724
- }
725
- function createStepResultBase(step, startedAt, phase, stepIndex) {
726
- const endedAt = new Date();
727
- return {
728
- node: step.node,
729
- phase,
730
- stepIndex,
731
- input: step.input,
732
- meta: step.meta,
733
- startedAt: startedAt.toISOString(),
734
- endedAt: endedAt.toISOString(),
735
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
736
- };
737
- }
738
- async function executeNode(step, execute, phase, stepIndex, options) {
739
- const startedAt = new Date();
740
- const abortController = new AbortController();
741
- const parentSignal = options.parentSignal;
742
- const abortFromParent = ()=>abortController.abort(parentSignal?.reason);
743
- if (parentSignal?.aborted) abortFromParent();
744
- else parentSignal?.addEventListener('abort', abortFromParent, {
745
- once: true
746
- });
747
- const timeoutMs = step.meta.timeoutMs ?? options.defaultTimeoutMs;
748
- let timeout;
749
- let abortGraceTimeout;
750
- const execution = Promise.resolve().then(()=>{
751
- abortController.signal.throwIfAborted();
752
- return execute(abortController.signal);
753
- });
754
- let settleOnAbort;
755
- const abortedExecution = new Promise((_, reject)=>{
756
- settleOnAbort = ()=>{
757
- abortGraceTimeout = setTimeout(()=>{
758
- reject(abortController.signal.reason);
759
- }, 0);
760
- };
761
- if (abortController.signal.aborted) settleOnAbort();
762
- else abortController.signal.addEventListener('abort', settleOnAbort, {
763
- once: true
764
- });
765
- });
766
- const timeoutExecution = void 0 === timeoutMs ? void 0 : new Promise((_, reject)=>{
767
- timeout = setTimeout(()=>{
768
- if (abortController.signal.aborted) return void reject(abortController.signal.reason);
769
- const timeoutError = new StepTimeoutError(timeoutMs, step.node);
770
- abortController.abort(timeoutError);
771
- reject(timeoutError);
772
- }, timeoutMs);
773
- });
774
- const settledExecution = Promise.race([
775
- execution,
776
- abortedExecution,
777
- ...void 0 === timeoutExecution ? [] : [
778
- timeoutExecution
779
- ]
780
- ]);
781
- try {
782
- const output = validateNodeOutput(await settledExecution, step.node);
783
- return {
784
- ...createStepResultBase(step, startedAt, phase, stepIndex),
785
- status: 'success',
786
- continuedAfterError: false,
787
- ...void 0 === output ? {} : {
788
- output
789
- }
790
- };
791
- } catch (error) {
792
- return {
793
- ...createStepResultBase(step, startedAt, phase, stepIndex),
794
- status: 'failed',
795
- continuedAfterError: step.meta.continueOnError,
796
- error: normalizeNodeExecutionError(error, step.node)
797
- };
798
- } finally{
799
- if (void 0 !== timeout) clearTimeout(timeout);
800
- if (void 0 !== abortGraceTimeout) clearTimeout(abortGraceTimeout);
801
- if (void 0 !== settleOnAbort) abortController.signal.removeEventListener('abort', settleOnAbort);
802
- parentSignal?.removeEventListener('abort', abortFromParent);
803
- }
804
- }
805
- function createNodeReportCollector(node) {
806
- const traces = new Map();
807
- let accepting = true;
808
- return {
809
- collector: Object.freeze({
810
- addTrace (trace) {
811
- if (!accepting) throw new NodeExecutionError(node, new Error('Report traces can only be added while the current Step is running.'));
812
- if (!trace || 'object' != typeof trace || 'midscene-execution' !== trace.type) throw new NodeExecutionError(node, new TypeError('Report trace type must be "midscene-execution".'));
813
- if ('string' != typeof trace.executionId || 0 === trace.executionId.trim().length) throw new NodeExecutionError(node, new TypeError('Report trace executionId must be non-empty.'));
814
- const normalized = Object.freeze({
815
- type: 'midscene-execution',
816
- executionId: trace.executionId
817
- });
818
- traces.set(`${normalized.type}:${normalized.executionId}`, normalized);
819
- }
820
- }),
821
- close () {
822
- accepting = false;
823
- return Object.freeze([
824
- ...traces.values()
825
- ]);
826
- }
827
- };
828
- }
829
- async function parseNodeInput(node, input) {
830
- if (!node.inputSchema) return input;
831
- const parsed = await node.inputSchema.safeParseAsync(input);
832
- if (!parsed.success) throw NodeInputValidationError.fromZod(node.name, parsed.error);
833
- return parsed.data;
834
- }
835
- async function executeStep(step, node, target, context, execution = {}) {
836
- const phase = 'case' === target.scope ? target.case.phase : target.document.phase;
837
- const stepIndex = 'case' === target.scope ? target.case.stepIndex : target.document.stepIndex;
838
- const reportCollector = createNodeReportCollector(step.node);
839
- let result;
840
- try {
841
- result = await executeNode(step, async (signal)=>{
842
- const input = await parseNodeInput(node, step.input);
843
- signal.throwIfAborted();
844
- const common = {
845
- input,
846
- $: step.meta,
847
- signal,
848
- context,
849
- report: reportCollector.collector,
850
- onTeardown: (teardown)=>{
851
- if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
852
- execution.onTeardown(step.node, teardown);
853
- }
854
- };
855
- return 'case' === target.scope ? node.execute({
856
- ...common,
857
- scope: 'case',
858
- case: target.case
859
- }) : node.execute({
860
- ...common,
861
- scope: 'document',
862
- document: target.document
863
- });
864
- }, phase, stepIndex, {
865
- parentSignal: execution.signal,
866
- defaultTimeoutMs: execution.defaultTimeoutMs
867
- });
868
- } finally{
869
- const traces = reportCollector.close();
870
- if (traces.length > 0 && result) result = {
871
- ...result,
872
- report: {
873
- traces
874
- }
875
- };
876
- }
877
- if (!result) throw new NodeExecutionError(step.node, new Error('Step execution completed without a result.'));
878
- return result;
879
- }
880
- const reportPathsFromTeardown = (result)=>{
881
- if (void 0 === result) return [];
882
- if ('object' != typeof result || null === result || Array.isArray(result)) throw new TypeError('Node teardown result must be an object or undefined.');
883
- const paths = result.reportPaths;
884
- if (void 0 === paths) return [];
885
- if (!Array.isArray(paths)) throw new TypeError('Node teardown reportPaths must be an array.');
886
- return paths.map((path, index)=>{
887
- if ('string' != typeof path || 0 === path.trim().length) throw new TypeError(`Node teardown reportPaths[${index}] must be a non-empty string.`);
888
- if (!(0, external_node_path_namespaceObject.isAbsolute)(path)) throw new TypeError(`Node teardown reportPaths[${index}] must be an absolute path.`);
889
- return path;
890
- });
891
- };
892
- function createDocumentRuntime(document, options) {
893
- const documentRunId = options.createDocumentRunId?.() ?? (0, external_node_crypto_namespaceObject.randomUUID)();
894
- const startedAt = new Date();
895
- const beforeAll = [];
896
- const afterAll = [];
897
- const nodes = {
898
- beforeAll: document.lifecycle.beforeAll.map((step)=>options.resolveNode(step.node)),
899
- afterAll: document.lifecycle.afterAll.map((step)=>options.resolveNode(step.node))
900
- };
901
- const project = options.project ?? Object.freeze({
902
- projectId: document.projectId,
903
- name: document.projectId,
904
- tags: Object.freeze({
905
- include: [],
906
- exclude: []
907
- }),
908
- retry: 0,
909
- variables: Object.freeze({})
910
- });
911
- const context = options.projectContext;
912
- const signal = options.signal ?? new AbortController().signal;
913
- const nodeTeardownStack = [];
914
- let acceptingNodeTeardowns = true;
915
- let started = false;
916
- let finishedResult;
917
- const reportPaths = new Set();
918
- const createResult = (teardownErrors = [])=>{
919
- const endedAt = new Date();
920
- return {
921
- documentId: document.documentId,
922
- documentRunId,
923
- projectId: document.projectId,
924
- projectName: project.name,
925
- sourcePath: document.sourcePath,
926
- status: beforeAll.some((step)=>'failed' === step.status) || afterAll.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
927
- startedAt: startedAt.toISOString(),
928
- endedAt: endedAt.toISOString(),
929
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
930
- beforeAll: [
931
- ...beforeAll
932
- ],
933
- afterAll: [
934
- ...afterAll
935
- ],
936
- ...teardownErrors.length > 0 ? {
937
- teardownErrors
938
- } : {},
939
- ...reportPaths.size > 0 ? {
940
- reportPaths: [
941
- ...reportPaths
942
- ]
943
- } : {}
944
- };
945
- };
946
- const runPhase = async (phase, results, executionSignal = signal)=>{
947
- for (const [stepIndex, step] of document.lifecycle[phase].entries()){
948
- const documentContext = {
949
- documentId: document.documentId,
950
- documentRunId,
951
- projectId: document.projectId,
952
- projectName: project.name,
953
- sourcePath: document.sourcePath,
954
- phase,
955
- stepIndex
956
- };
957
- const stepInfo = {
958
- scope: 'document',
959
- node: step.node,
960
- stepCount: document.lifecycle[phase].length,
961
- document: documentContext
962
- };
963
- await options.onStepStart?.(stepInfo);
964
- const result = await executeStep(step, nodes[phase][stepIndex], {
965
- scope: 'document',
966
- document: documentContext
967
- }, context, {
968
- signal: executionSignal,
969
- defaultTimeoutMs: options.defaultTimeoutMs,
970
- onTeardown (node, teardown) {
971
- if (!acceptingNodeTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a workflow document is running.', {
972
- documentId: document.documentId,
973
- documentRunId,
974
- node
975
- });
976
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
977
- documentId: document.documentId,
978
- documentRunId,
979
- node
980
- });
981
- nodeTeardownStack.push({
982
- registrationIndex: nodeTeardownStack.length,
983
- node,
984
- teardown
985
- });
986
- }
987
- });
988
- await options.onStepResult?.(stepInfo, result);
989
- results.push(result);
990
- if ('failed' === result.status && !result.continuedAfterError) break;
991
- }
992
- };
993
- return {
994
- get context () {
995
- return context;
996
- },
997
- get canRunCases () {
998
- return started && !signal.aborted && !beforeAll.some((step)=>'failed' === step.status);
999
- },
1000
- async start () {
1001
- if (started) throw new WorkflowLifecycleError('Workflow document runtime has already started.', {
1002
- documentId: document.documentId,
1003
- documentRunId
1004
- });
1005
- started = true;
1006
- if (!signal.aborted) await runPhase('beforeAll', beforeAll);
1007
- return createResult();
1008
- },
1009
- async finish () {
1010
- if (finishedResult) return finishedResult;
1011
- if (!started) throw new WorkflowLifecycleError('Workflow document runtime must start before it can finish.', {
1012
- documentId: document.documentId,
1013
- documentRunId
1014
- });
1015
- let finishError;
1016
- try {
1017
- await runPhase('afterAll', afterAll, signal.aborted ? new AbortController().signal : signal);
1018
- } catch (error) {
1019
- finishError = error;
1020
- }
1021
- acceptingNodeTeardowns = false;
1022
- const teardownErrors = [];
1023
- for (const { registrationIndex, node, teardown } of nodeTeardownStack.reverse())try {
1024
- for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
1025
- } catch (error) {
1026
- teardownErrors.push(new NodeScopeTeardownError(error, {
1027
- scope: 'document',
1028
- scopeId: documentRunId,
1029
- node,
1030
- registrationIndex
1031
- }));
1032
- }
1033
- finishedResult = createResult(teardownErrors);
1034
- if (finishError) throw finishError;
1035
- await options.onResult?.(finishedResult);
1036
- return finishedResult;
1037
- }
1038
- };
1039
- }
1040
- async function runCollectedCase(collectedCase, options) {
1041
- const phases = {
1042
- beforeEach: options.beforeEach ?? [],
1043
- steps: collectedCase.definition.steps,
1044
- afterEach: options.afterEach ?? []
1045
- };
1046
- const nodes = {
1047
- beforeEach: phases.beforeEach.map((step)=>options.resolveNode(step.node)),
1048
- steps: phases.steps.map((step)=>options.resolveNode(step.node)),
1049
- afterEach: phases.afterEach.map((step)=>options.resolveNode(step.node))
1050
- };
1051
- const runId = options.createRunId?.() ?? (0, external_node_crypto_namespaceObject.randomUUID)();
1052
- const startedAt = new Date();
1053
- const beforeEach = [];
1054
- const steps = [];
1055
- const afterEach = [];
1056
- const teardownStack = [];
1057
- let acceptingTeardowns = true;
1058
- const onTeardown = (node, teardown)=>{
1059
- if (!acceptingTeardowns) throw new WorkflowLifecycleError('Node teardown can only be registered while a case attempt is running.', {
1060
- runId,
1061
- node
1062
- });
1063
- if ('function' != typeof teardown) throw new WorkflowLifecycleError('Node onTeardown() requires a teardown function.', {
1064
- runId,
1065
- node
1066
- });
1067
- teardownStack.push({
1068
- registrationIndex: teardownStack.length,
1069
- node,
1070
- teardown
1071
- });
1072
- };
1073
- const runPhase = async (phase, results, signal = options.signal)=>{
1074
- for (const [stepIndex, step] of phases[phase].entries()){
1075
- const caseContext = {
1076
- caseId: collectedCase.caseId,
1077
- runId,
1078
- projectName: options.projectName ?? collectedCase.projectId,
1079
- attemptIndex: options.attemptIndex ?? 0,
1080
- name: collectedCase.definition.name,
1081
- sourcePath: collectedCase.sourcePath,
1082
- caseIndex: collectedCase.caseIndex,
1083
- phase,
1084
- stepIndex
1085
- };
1086
- const stepInfo = {
1087
- scope: 'case',
1088
- node: step.node,
1089
- stepCount: phases[phase].length,
1090
- case: caseContext
1091
- };
1092
- await options.onStepStart?.(stepInfo);
1093
- const result = await executeStep(step, nodes[phase][stepIndex], {
1094
- scope: 'case',
1095
- case: caseContext
1096
- }, options.context, {
1097
- signal,
1098
- defaultTimeoutMs: options.defaultTimeoutMs,
1099
- onTeardown
1100
- });
1101
- await options.onStepResult?.(stepInfo, result);
1102
- results.push(result);
1103
- if ('failed' === result.status && !result.continuedAfterError) break;
1104
- }
1105
- };
1106
- let executionError;
1107
- try {
1108
- await runPhase('beforeEach', beforeEach);
1109
- if (!beforeEach.some((step)=>'failed' === step.status)) await runPhase('steps', steps);
1110
- } catch (error) {
1111
- executionError = error;
1112
- }
1113
- try {
1114
- const cleanupSignal = options.signal?.aborted ? new AbortController().signal : options.signal;
1115
- await runPhase('afterEach', afterEach, cleanupSignal);
1116
- } catch (error) {
1117
- executionError ??= error;
1118
- }
1119
- acceptingTeardowns = false;
1120
- const teardownErrors = [];
1121
- const reportPaths = new Set();
1122
- for (const { registrationIndex, node, teardown } of teardownStack.reverse())try {
1123
- for (const path of reportPathsFromTeardown(await teardown()))reportPaths.add(path);
1124
- } catch (error) {
1125
- teardownErrors.push(new NodeScopeTeardownError(error, {
1126
- scope: 'case',
1127
- scopeId: runId,
1128
- node,
1129
- registrationIndex
1130
- }));
1131
- }
1132
- if (executionError) throw executionError;
1133
- const endedAt = new Date();
1134
- const result = {
1135
- caseId: collectedCase.caseId,
1136
- runId,
1137
- projectName: options.projectName ?? collectedCase.projectId,
1138
- attemptIndex: options.attemptIndex ?? 0,
1139
- name: collectedCase.definition.name,
1140
- sourcePath: collectedCase.sourcePath,
1141
- caseIndex: collectedCase.caseIndex,
1142
- status: beforeEach.some((step)=>'failed' === step.status) || steps.some((step)=>'failed' === step.status) || afterEach.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
1143
- beforeEach,
1144
- steps,
1145
- afterEach,
1146
- ...teardownErrors.length > 0 ? {
1147
- teardownErrors
1148
- } : {},
1149
- ...reportPaths.size > 0 ? {
1150
- reportPaths: [
1151
- ...reportPaths
1152
- ]
1153
- } : {},
1154
- startedAt: startedAt.toISOString(),
1155
- endedAt: endedAt.toISOString(),
1156
- durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime())
1157
- };
1158
- await options.onResult?.(result);
1159
- return result;
1160
- }
1161
- const asNotRun = (collectedCase, projectName, reason)=>({
1162
- caseId: collectedCase.caseId,
1163
- projectName,
1164
- name: collectedCase.definition.name,
1165
- sourcePath: collectedCase.sourcePath,
1166
- caseIndex: collectedCase.caseIndex,
1167
- status: 'not-run',
1168
- notRunReason: reason
1169
- });
1170
- async function runWorkflowDocument(document, options) {
1171
- const createDocumentRunId = options.createDocumentRunId;
1172
- const createCaseRunId = options.createCaseRunId;
1173
- const projectName = options.project?.name ?? document.projectId;
1174
- const retry = options.retry ?? options.project?.retry ?? 0;
1175
- if (!Number.isInteger(retry) || retry < 0) throw new TypeError('Workflow document retry must be a non-negative integer.');
1176
- const runtime = createDocumentRuntime(document, {
1177
- resolveNode: options.resolveNode,
1178
- project: options.project,
1179
- projectContext: options.projectContext,
1180
- signal: options.signal,
1181
- defaultTimeoutMs: options.defaultTimeoutMs,
1182
- onStepStart: options.onStepStart,
1183
- onStepResult: options.onStepResult,
1184
- onResult: options.onDocumentResult,
1185
- createDocumentRunId: createDocumentRunId ? ()=>createDocumentRunId(document) : void 0
1186
- });
1187
- const cases = [];
1188
- let runtimeStarted = false;
1189
- let documentResult;
1190
- let executionError;
1191
- let fatalError = false;
1192
- try {
1193
- runtimeStarted = true;
1194
- await runtime.start();
1195
- if (runtime.canRunCases) for (const collectedCase of document.cases){
1196
- if (fatalError || options.shouldStop?.()) {
1197
- const outcome = asNotRun(collectedCase, projectName, fatalError ? 'fatal-error' : options.stopReason?.() ?? 'interrupted');
1198
- cases.push(outcome);
1199
- await options.onCaseOutcome?.(outcome);
1200
- continue;
1201
- }
1202
- await options.onCaseStart?.(collectedCase);
1203
- const attempts = [];
1204
- for(let attemptIndex = 0; attemptIndex <= retry; attemptIndex += 1){
1205
- const run = await runCollectedCase(collectedCase, {
1206
- resolveNode: options.resolveNode,
1207
- beforeEach: document.lifecycle.beforeEach,
1208
- afterEach: document.lifecycle.afterEach,
1209
- context: runtime.context,
1210
- projectName,
1211
- attemptIndex,
1212
- signal: options.signal,
1213
- defaultTimeoutMs: options.defaultTimeoutMs,
1214
- onStepStart: options.onStepStart,
1215
- onStepResult: options.onStepResult,
1216
- onResult: options.onCaseResult,
1217
- createRunId: createCaseRunId ? ()=>createCaseRunId(collectedCase, attemptIndex) : void 0
1218
- });
1219
- attempts.push(run);
1220
- fatalError = options.isFatalError?.(run) ?? false;
1221
- if ('success' === run.status || fatalError) break;
1222
- }
1223
- const run = attempts.at(-1);
1224
- const outcome = {
1225
- caseId: collectedCase.caseId,
1226
- projectName,
1227
- name: collectedCase.definition.name,
1228
- sourcePath: collectedCase.sourcePath,
1229
- caseIndex: collectedCase.caseIndex,
1230
- status: run.status,
1231
- run,
1232
- attempts
1233
- };
1234
- cases.push(outcome);
1235
- await options.onCaseOutcome?.(outcome);
1236
- }
1237
- else {
1238
- const reason = options.signal?.aborted ? options.stopReason?.() ?? 'interrupted' : 'document-start-failed';
1239
- cases.push(...document.cases.map((collectedCase)=>asNotRun(collectedCase, projectName, reason)));
1240
- }
1241
- } catch (error) {
1242
- executionError = error;
1243
- } finally{
1244
- if (runtimeStarted) try {
1245
- documentResult = await runtime.finish();
1246
- } catch (error) {
1247
- executionError ??= error;
1248
- }
1249
- }
1250
- if (executionError) throw executionError;
1251
- if (!documentResult) throw new Error(`Workflow document "${document.sourcePath}" did not produce a result.`);
1252
- return {
1253
- document: documentResult,
1254
- cases
1255
- };
1256
- }
1257
318
  const external_js_yaml_namespaceObject = require("js-yaml");
1258
- const supportedMetaKeys = new Set([
1259
- 'timeout',
1260
- 'continue-on-error'
1261
- ]);
1262
- function isMapping(value) {
1263
- return 'object' == typeof value && null !== value && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || null === Object.getPrototypeOf(value));
1264
- }
1265
- function formatStep(index) {
1266
- return `step ${index + 1}`;
1267
- }
1268
- function normalizeMeta(value, index) {
1269
- if (void 0 === value) return {
1270
- continueOnError: false
1271
- };
1272
- if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} "$" must be a mapping.`, {
1273
- index
1274
- });
1275
- for (const key of Object.keys(value))if (!supportedMetaKeys.has(key)) throw new WorkflowParseError(`${formatStep(index)} has unsupported engine metadata "${key}".`, {
1276
- index,
1277
- key
1278
- });
1279
- const timeout = value.timeout;
1280
- 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.`, {
1281
- index,
1282
- timeout
1283
- });
1284
- const continueOnError = value['continue-on-error'];
1285
- if (void 0 !== continueOnError && 'boolean' != typeof continueOnError) throw new WorkflowParseError(`${formatStep(index)} "$.continue-on-error" must be a boolean.`, {
1286
- index,
1287
- continueOnError
1288
- });
1289
- return {
1290
- ...void 0 === timeout ? {} : {
1291
- timeoutMs: timeout
1292
- },
1293
- continueOnError: continueOnError ?? false
1294
- };
1295
- }
1296
- function normalizeStep(value, index = 0, resolveNode) {
1297
- if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} must be a mapping.`, {
1298
- index
1299
- });
1300
- const entries = Object.entries(value);
1301
- if (1 !== entries.length) throw new WorkflowParseError(`${formatStep(index)} must contain exactly one node.`, {
1302
- index,
1303
- nodes: Object.keys(value)
1304
- });
1305
- const [node, rawValue] = entries[0];
1306
- if (0 === node.trim().length) throw new WorkflowParseError(`${formatStep(index)} node name must not be empty.`, {
1307
- index
1308
- });
1309
- if ('string' == typeof rawValue) {
1310
- const resolvedNode = resolveNode?.(node);
1311
- const stringInputKey = resolvedNode ? resolvedNode.stringInputKey : 'prompt';
1312
- if (false === stringInputKey || void 0 === stringInputKey) throw new WorkflowParseError(`${formatStep(index)} node "${node}" does not accept string shorthand.`, {
1313
- index,
1314
- node
1315
- });
1316
- return {
1317
- node,
1318
- input: {
1319
- [stringInputKey]: rawValue
1320
- },
1321
- meta: {
1322
- continueOnError: false
1323
- }
1324
- };
1325
- }
1326
- if (!isMapping(rawValue)) throw new WorkflowParseError(`${formatStep(index)} value must be a string shorthand or a mapping.`, {
1327
- index,
1328
- node
1329
- });
1330
- const { $: rawMeta, ...input } = rawValue;
1331
- return {
1332
- node,
1333
- input,
1334
- meta: normalizeMeta(rawMeta, index)
1335
- };
1336
- }
1337
- function normalizeSteps(steps, resolveNode) {
1338
- if (!Array.isArray(steps)) throw new WorkflowParseError('Steps must be an array.');
1339
- return steps.map((step, index)=>normalizeStep(step, index, resolveNode));
1340
- }
319
+ const external_node_crypto_namespaceObject = require("node:crypto");
320
+ const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
321
+ projectId,
322
+ sourcePath,
323
+ caseIndex,
324
+ ...invocationIndex ? [
325
+ invocationIndex
326
+ ] : []
327
+ ])).digest('hex');
328
+ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
329
+ projectId,
330
+ sourcePath,
331
+ ...invocationIndex ? [
332
+ invocationIndex
333
+ ] : []
334
+ ])).digest('hex');
335
+ const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
1341
336
  const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
1342
337
  const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
1343
338
  const exactProjectExpression = /^\$\{([^{}]+)\}$/;
1344
339
  const missingVariable = (kind, name, options)=>{
1345
- throw new WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
340
+ throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
1346
341
  ...options.location,
1347
342
  variable: name,
1348
343
  variableKind: kind
@@ -1358,7 +353,7 @@ const environmentVariable = (name, options)=>{
1358
353
  return value;
1359
354
  };
1360
355
  const interpolationValue = (name, value, options)=>{
1361
- if ('object' == typeof value && null !== value) throw new WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
356
+ if ('object' == typeof value && null !== value) throw new test_runner_namespaceObject.WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
1362
357
  ...options.location,
1363
358
  variable: name,
1364
359
  variableKind: 'project'
@@ -1381,23 +376,36 @@ const resolveWorkflowVariables = (value, options)=>{
1381
376
  ]));
1382
377
  return value;
1383
378
  };
1384
- const collect_isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
379
+ function _define_property(obj, key, value) {
380
+ if (key in obj) Object.defineProperty(obj, key, {
381
+ value: value,
382
+ enumerable: true,
383
+ configurable: true,
384
+ writable: true
385
+ });
386
+ else obj[key] = value;
387
+ return obj;
388
+ }
389
+ const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
390
+ class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
391
+ constructor(actualFormat, sourcePath){
392
+ 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.`, {
393
+ sourcePath,
394
+ actualFormat,
395
+ expectedFormat: 'native'
396
+ }), _define_property(this, "actualFormat", void 0);
397
+ this.actualFormat = actualFormat;
398
+ }
399
+ }
1385
400
  const collect_rejectUnknownKeys = (value, allowed, label)=>{
1386
401
  const allowedSet = new Set(allowed);
1387
402
  const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
1388
- if (unknown.length) throw new WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
403
+ if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
1389
404
  field: unknown[0]
1390
405
  });
1391
406
  };
1392
- const createCaseId = (projectId, sourcePath, caseIndex)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
1393
- projectId,
1394
- sourcePath,
1395
- caseIndex
1396
- ])).digest('hex');
1397
- const createWorkflowDocumentId = (projectId, sourcePath)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
1398
- projectId,
1399
- sourcePath
1400
- ])).digest('hex');
407
+ const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
408
+ const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
1401
409
  function collectWorkflowDocument(source, options) {
1402
410
  let parsed;
1403
411
  try {
@@ -1405,11 +413,13 @@ function collectWorkflowDocument(source, options) {
1405
413
  schema: external_js_yaml_namespaceObject.JSON_SCHEMA
1406
414
  });
1407
415
  } catch (error) {
1408
- throw new WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
416
+ throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
1409
417
  sourcePath: source.sourcePath
1410
418
  }, error);
1411
419
  }
1412
- if (!collect_isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
420
+ if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
421
+ const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
422
+ if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
1413
423
  collect_rejectUnknownKeys(parsed, [
1414
424
  'beforeAll',
1415
425
  'beforeEach',
@@ -1417,7 +427,7 @@ function collectWorkflowDocument(source, options) {
1417
427
  'afterEach',
1418
428
  'afterAll'
1419
429
  ], 'Workflow document');
1420
- if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new WorkflowParseError('Workflow document must contain a non-empty cases array.');
430
+ if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
1421
431
  const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
1422
432
  const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
1423
433
  ...step,
@@ -1438,12 +448,12 @@ function collectWorkflowDocument(source, options) {
1438
448
  const normalizeLifecycle = (phase)=>{
1439
449
  const value = parsed[phase];
1440
450
  if (void 0 === value) return [];
1441
- if (!Array.isArray(value)) throw new WorkflowParseError(`Workflow document ${phase} must be an array.`, {
451
+ if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
1442
452
  phase
1443
453
  });
1444
454
  return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
1445
455
  const resolved = options.resolveNode(normalized.node);
1446
- if (!resolved) throw new WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
456
+ if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1447
457
  phase,
1448
458
  stepIndex,
1449
459
  node: normalized.node
@@ -1459,7 +469,7 @@ function collectWorkflowDocument(source, options) {
1459
469
  };
1460
470
  const ids = new Set();
1461
471
  const cases = parsed.cases.map((definition, caseIndex)=>{
1462
- if (!collect_isMapping(definition)) throw new WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
472
+ if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
1463
473
  caseIndex
1464
474
  });
1465
475
  collect_rejectUnknownKeys(definition, [
@@ -1467,471 +477,63 @@ function collectWorkflowDocument(source, options) {
1467
477
  'tags',
1468
478
  'steps'
1469
479
  ], `Case ${caseIndex + 1}`);
1470
- if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
480
+ if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
1471
481
  caseIndex
1472
482
  });
1473
- if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
483
+ if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
1474
484
  caseIndex
1475
485
  });
1476
486
  const tags = definition.tags ?? [];
1477
- 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.`, {
487
+ if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
1478
488
  caseIndex
1479
489
  });
1480
490
  const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
1481
- if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
491
+ if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
1482
492
  caseIndex,
1483
493
  stepIndex,
1484
494
  node: normalized.node
1485
- });
1486
- return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
1487
- });
1488
- const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
1489
- if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
1490
- caseId
1491
- });
1492
- ids.add(caseId);
1493
- return {
1494
- caseId,
1495
- projectId: source.projectId,
1496
- sourcePath,
1497
- caseIndex,
1498
- definition: {
1499
- name: definition.name,
1500
- tags: tags,
1501
- steps
1502
- }
1503
- };
1504
- });
1505
- return {
1506
- documentId: createWorkflowDocumentId(source.projectId, sourcePath),
1507
- projectId: source.projectId,
1508
- sourcePath,
1509
- lifecycle,
1510
- cases
1511
- };
1512
- }
1513
- const logger_namespaceObject = require("@midscene/shared/logger");
1514
- const MAX_DEPTH = 6;
1515
- const MAX_STRING_LENGTH = 2000;
1516
- const MAX_ARRAY_ITEMS = 50;
1517
- const MAX_OBJECT_KEYS = 50;
1518
- const MAX_SERIALIZED_BYTES = 65536;
1519
- const TRUNCATED_VALUE = '[Truncated: display value exceeded 64 KiB]';
1520
- const normalizedSecretKeys = [
1521
- 'password',
1522
- 'passwd',
1523
- 'secret',
1524
- 'token',
1525
- 'authorization',
1526
- 'cookie',
1527
- 'apikey'
1528
- ];
1529
- const normalizedKey = (key)=>key.toLowerCase().replace(/[^a-z0-9]/g, '');
1530
- const isSecretKey = (key)=>{
1531
- const normalized = normalizedKey(key);
1532
- return normalizedSecretKeys.some((secret)=>normalized.includes(secret));
1533
- };
1534
- const objectPath = (parent, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${parent}.${key}` : `${parent}[${JSON.stringify(key)}]`;
1535
- const stringByteLength = (value)=>Buffer.byteLength(value, 'utf8');
1536
- function sanitizeReportText(value, maximumLength = MAX_STRING_LENGTH) {
1537
- return value.length <= maximumLength ? value : `${value.slice(0, maximumLength)}… [truncated]`;
1538
- }
1539
- function sanitizeReportValue(value) {
1540
- const truncatedPaths = new Set();
1541
- const redactedPaths = new Set();
1542
- const ancestors = new WeakSet();
1543
- let remainingBytes = MAX_SERIALIZED_BYTES;
1544
- const consume = (path, text)=>{
1545
- remainingBytes -= stringByteLength(text);
1546
- if (remainingBytes >= 0) return true;
1547
- truncatedPaths.add(path);
1548
- return false;
1549
- };
1550
- const visit = (current, path, depth)=>{
1551
- if (remainingBytes <= 0) {
1552
- truncatedPaths.add(path);
1553
- return '[Truncated: size limit]';
1554
- }
1555
- if (null === current || 'boolean' == typeof current) {
1556
- consume(path, String(current));
1557
- return current;
1558
- }
1559
- if ('string' == typeof current) {
1560
- if (current.length > MAX_STRING_LENGTH) {
1561
- truncatedPaths.add(path);
1562
- const shortened = `${current.slice(0, MAX_STRING_LENGTH)}… [truncated]`;
1563
- consume(path, shortened);
1564
- return shortened;
1565
- }
1566
- consume(path, current);
1567
- return current;
1568
- }
1569
- if ('number' == typeof current) {
1570
- if (Number.isFinite(current)) {
1571
- consume(path, String(current));
1572
- return current;
1573
- }
1574
- truncatedPaths.add(path);
1575
- return `[Unsupported number: ${String(current)}]`;
1576
- }
1577
- if ('bigint' == typeof current) {
1578
- truncatedPaths.add(path);
1579
- return `[Unsupported bigint: ${String(current)}]`;
1580
- }
1581
- if (void 0 === current || 'function' == typeof current || 'symbol' == typeof current) {
1582
- truncatedPaths.add(path);
1583
- return `[Unsupported ${typeof current}]`;
1584
- }
1585
- if (depth >= MAX_DEPTH) {
1586
- truncatedPaths.add(path);
1587
- return '[Truncated: maximum depth]';
1588
- }
1589
- if (current instanceof Date) {
1590
- const date = Number.isNaN(current.getTime()) ? '[Invalid Date]' : current.toISOString();
1591
- consume(path, date);
1592
- return date;
1593
- }
1594
- if (ancestors.has(current)) {
1595
- truncatedPaths.add(path);
1596
- return '[Unsupported circular value]';
1597
- }
1598
- if (Array.isArray(current)) {
1599
- ancestors.add(current);
1600
- const output = [];
1601
- const itemCount = Math.min(current.length, MAX_ARRAY_ITEMS);
1602
- for(let index = 0; index < itemCount; index += 1){
1603
- if (remainingBytes <= 0) break;
1604
- output.push(visit(current[index], `${path}[${index}]`, depth + 1));
1605
- }
1606
- if (current.length > output.length) truncatedPaths.add(path);
1607
- ancestors.delete(current);
1608
- return output;
1609
- }
1610
- const prototype = Object.getPrototypeOf(current);
1611
- if (prototype !== Object.prototype && null !== prototype) {
1612
- truncatedPaths.add(path);
1613
- const constructorName = current.constructor?.name ?? 'object';
1614
- return `[Unsupported object: ${constructorName}]`;
1615
- }
1616
- ancestors.add(current);
1617
- const output = {};
1618
- const keys = Object.keys(current);
1619
- for (const key of keys.slice(0, MAX_OBJECT_KEYS)){
1620
- const childPath = objectPath(path, key);
1621
- if (!consume(childPath, key)) break;
1622
- if (isSecretKey(key)) {
1623
- output[key] = '[REDACTED]';
1624
- redactedPaths.add(childPath);
1625
- } else output[key] = visit(current[key], childPath, depth + 1);
1626
- if (remainingBytes <= 0) break;
1627
- }
1628
- if (keys.length > Object.keys(output).length) truncatedPaths.add(path);
1629
- ancestors.delete(current);
1630
- return output;
1631
- };
1632
- const sanitized = visit(value, '$', 0);
1633
- const serialized = JSON.stringify(sanitized);
1634
- if (void 0 === serialized || stringByteLength(serialized) > MAX_SERIALIZED_BYTES) return {
1635
- value: TRUNCATED_VALUE,
1636
- truncatedPaths: [
1637
- '$'
1638
- ]
1639
- };
1640
- return {
1641
- value: sanitized,
1642
- ...truncatedPaths.size > 0 ? {
1643
- truncatedPaths: [
1644
- ...truncatedPaths
1645
- ]
1646
- } : {},
1647
- ...redactedPaths.size > 0 ? {
1648
- redactedPaths: [
1649
- ...redactedPaths
1650
- ]
1651
- } : {}
1652
- };
1653
- }
1654
- function sanitizeReportError(error) {
1655
- if (error instanceof Error) {
1656
- const workflowLike = error;
1657
- return {
1658
- name: sanitizeReportText(error.name || 'Error', 200),
1659
- message: sanitizeReportText(error.message || String(error)),
1660
- ...'string' == typeof workflowLike.code ? {
1661
- code: sanitizeReportText(workflowLike.code, 200)
1662
- } : {},
1663
- ...void 0 === workflowLike.details ? {} : {
1664
- details: sanitizeReportValue(workflowLike.details)
1665
- }
1666
- };
1667
- }
1668
- return {
1669
- name: 'Error',
1670
- message: sanitizeReportText(String(error))
1671
- };
1672
- }
1673
- const warnReport = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
1674
- console: true
1675
- });
1676
- const exactMidsceneDetailNodes = new Set([
1677
- 'aiAct',
1678
- 'aiAssert',
1679
- 'recordToReport'
1680
- ]);
1681
- const scopedExecutionKey = (scopeId, executionId)=>`${scopeId}\u0000${executionId}`;
1682
- const reportSourcesForScope = (context, scopeId)=>context.sourcesByScope.get(scopeId) ?? [];
1683
- const addDiagnostic = (context, diagnostic)=>{
1684
- context.diagnostics.push(diagnostic);
1685
- const key = JSON.stringify([
1686
- diagnostic.code,
1687
- diagnostic.scopeId,
1688
- diagnostic.executionId
1689
- ]);
1690
- if (!context.warnedDiagnostics.has(key)) {
1691
- context.warnedDiagnostics.add(key);
1692
- warnReport(diagnostic.message);
1693
- }
1694
- };
1695
- const buildStep = (step, scopeId, context)=>{
1696
- const details = [];
1697
- const unresolvedExecutionIds = [];
1698
- for (const trace of step.report?.traces ?? []){
1699
- const previousScope = context.tracedScopeByExecution.get(trace.executionId);
1700
- if (previousScope && previousScope !== scopeId) throw new Error(`Agent execution ${trace.executionId} was traced by multiple test scopes (${previousScope}, ${scopeId}).`);
1701
- context.tracedScopeByExecution.set(trace.executionId, scopeId);
1702
- const detail = context.detailsByScopeAndExecution.get(scopedExecutionKey(scopeId, trace.executionId));
1703
- if (detail) details.push(detail);
1704
- else unresolvedExecutionIds.push(trace.executionId);
1705
- }
1706
- let agentDetailDiagnostic;
1707
- if (unresolvedExecutionIds.length > 0) {
1708
- agentDetailDiagnostic = `Agent detail could not be resolved for ${unresolvedExecutionIds.join(', ')}.`;
1709
- const sources = reportSourcesForScope(context, scopeId);
1710
- for (const executionId of unresolvedExecutionIds)addDiagnostic(context, {
1711
- level: 'warning',
1712
- code: 'agent-detail-unresolved',
1713
- message: `Agent detail could not be resolved for execution ${executionId} in test scope ${scopeId}.`,
1714
- scopeId,
1715
- executionId,
1716
- ...sources.length > 0 ? {
1717
- sourcePaths: sources.map((source)=>source.sourcePath)
1718
- } : {}
1719
- });
1720
- } else if (0 === details.length && exactMidsceneDetailNodes.has(step.node)) {
1721
- agentDetailDiagnostic = 'This Agent did not expose a stable execution reference for the Step.';
1722
- addDiagnostic(context, {
1723
- level: 'warning',
1724
- code: 'agent-detail-unavailable',
1725
- message: `No stable Agent execution reference was captured for ${step.node} in test scope ${scopeId}.`,
1726
- scopeId,
1727
- ...reportSourcesForScope(context, scopeId).length > 0 ? {
1728
- sourcePaths: reportSourcesForScope(context, scopeId).map((source)=>source.sourcePath)
1729
- } : {}
1730
- });
1731
- }
1732
- const prompt = 'string' == typeof step.input?.prompt ? step.input.prompt : void 0;
1733
- return {
1734
- id: `${scopeId}:${step.phase}:${step.stepIndex}`,
1735
- phase: step.phase,
1736
- stepIndex: step.stepIndex,
1737
- node: step.node,
1738
- ...prompt ? {
1739
- title: sanitizeReportText(prompt, 240)
1740
- } : {},
1741
- status: step.status,
1742
- continuedAfterError: step.continuedAfterError,
1743
- startedAt: step.startedAt,
1744
- endedAt: step.endedAt,
1745
- durationMs: step.durationMs,
1746
- input: sanitizeReportValue(step.input),
1747
- ...step.output ? {
1748
- output: {
1749
- ...void 0 === step.output.summary ? {} : {
1750
- summary: sanitizeReportText(step.output.summary)
1751
- },
1752
- ...void 0 === step.output.data ? {} : {
1753
- data: sanitizeReportValue(step.output.data)
1754
- }
1755
- }
1756
- } : {},
1757
- ...step.error ? {
1758
- error: sanitizeReportError(step.error)
1759
- } : {},
1760
- ...details.length > 0 ? {
1761
- agentDetails: details
1762
- } : {},
1763
- ...agentDetailDiagnostic ? {
1764
- agentDetailDiagnostic
1765
- } : {}
1766
- };
1767
- };
1768
- const buildAttempt = (attempt, context)=>({
1769
- attemptId: attempt.runId,
1770
- attemptIndex: attempt.attemptIndex,
1771
- status: attempt.status,
1772
- startedAt: attempt.startedAt,
1773
- endedAt: attempt.endedAt,
1774
- durationMs: attempt.durationMs,
1775
- beforeEach: attempt.beforeEach.map((step)=>buildStep(step, attempt.runId, context)),
1776
- steps: attempt.steps.map((step)=>buildStep(step, attempt.runId, context)),
1777
- afterEach: attempt.afterEach.map((step)=>buildStep(step, attempt.runId, context)),
1778
- ...attempt.teardownErrors?.length ? {
1779
- teardownErrors: attempt.teardownErrors.map(sanitizeReportError)
1780
- } : {},
1781
- ...reportSourcesForScope(context, attempt.runId).length > 0 ? {
1782
- scopeReportIds: reportSourcesForScope(context, attempt.runId).map((source)=>source.reportId)
1783
- } : {}
1784
- });
1785
- const buildCase = (outcome, context)=>({
1786
- caseId: outcome.caseId,
1787
- name: outcome.name,
1788
- caseIndex: outcome.caseIndex,
1789
- status: outcome.status,
1790
- ...outcome.notRunReason ? {
1791
- notRunReason: outcome.notRunReason
1792
- } : {},
1793
- attempts: (outcome.attempts ?? (outcome.run ? [
1794
- outcome.run
1795
- ] : [])).map((attempt)=>buildAttempt(attempt, context))
1796
- });
1797
- const buildDocument = (documentId, document, cases, context)=>{
1798
- const sourcePath = document?.sourcePath ?? cases[0]?.sourcePath ?? '<unknown>';
1799
- const status = document?.status ?? (cases.some((item)=>'success' !== item.status) ? 'failed' : 'success');
1800
- return {
1801
- documentId,
1802
- sourcePath,
1803
- status,
1804
- ...document ? {
1805
- startedAt: document.startedAt,
1806
- endedAt: document.endedAt,
1807
- durationMs: document.durationMs
1808
- } : {},
1809
- beforeAll: (document?.beforeAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1810
- cases: cases.map((outcome)=>buildCase(outcome, context)),
1811
- afterAll: (document?.afterAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1812
- ...document?.teardownErrors?.length ? {
1813
- teardownErrors: document.teardownErrors.map(sanitizeReportError)
1814
- } : {},
1815
- ...document && reportSourcesForScope(context, document.documentRunId).length > 0 ? {
1816
- scopeReportIds: reportSourcesForScope(context, document.documentRunId).map((source)=>source.reportId)
1817
- } : {}
1818
- };
1819
- };
1820
- const buildProject = (project, context)=>{
1821
- const documentById = new Map(project.documents.map((document)=>[
1822
- document.documentId,
1823
- document
1824
- ]));
1825
- const casesByDocumentId = new Map();
1826
- for (const outcome of project.cases){
1827
- const current = casesByDocumentId.get(outcome.documentId) ?? [];
1828
- current.push(outcome);
1829
- casesByDocumentId.set(outcome.documentId, current);
1830
- }
1831
- const documentIds = new Set([
1832
- ...project.documents.map((document)=>document.documentId),
1833
- ...project.cases.map((outcome)=>outcome.documentId)
1834
- ]);
1835
- return {
1836
- projectId: project.projectId,
1837
- name: project.name,
1838
- status: project.status,
1839
- retry: project.retry,
1840
- ...project.lifecycle ? {
1841
- lifecycle: {
1842
- status: project.lifecycle.status,
1843
- startedAt: project.lifecycle.startedAt,
1844
- endedAt: project.lifecycle.endedAt,
1845
- durationMs: project.lifecycle.durationMs,
1846
- ...project.lifecycle.setupError ? {
1847
- setupError: sanitizeReportError(project.lifecycle.setupError)
1848
- } : {},
1849
- ...project.lifecycle.teardownErrors?.length ? {
1850
- teardownErrors: project.lifecycle.teardownErrors.map(sanitizeReportError)
1851
- } : {}
1852
- }
1853
- } : {},
1854
- documents: [
1855
- ...documentIds
1856
- ].map((documentId)=>buildDocument(documentId, documentById.get(documentId), casesByDocumentId.get(documentId) ?? [], context)),
1857
- collectionErrors: project.collectionErrors.map((collectionError)=>({
1858
- sourcePath: collectionError.sourcePath,
1859
- error: sanitizeReportError(collectionError.error)
1860
- }))
1861
- };
1862
- };
1863
- function collectTestRunReportSources(result) {
1864
- const sources = [];
1865
- for (const project of result.projects){
1866
- for (const document of project.documents)for (const sourcePath of document.reportPaths ?? [])sources.push({
1867
- scopeId: document.documentRunId,
1868
- sourcePath
1869
- });
1870
- for (const outcome of project.cases)for (const attempt of outcome.attempts ?? (outcome.run ? [
1871
- outcome.run
1872
- ] : []))for (const sourcePath of attempt.reportPaths ?? [])sources.push({
1873
- scopeId: attempt.runId,
1874
- sourcePath
495
+ });
496
+ return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
1875
497
  });
1876
- }
1877
- return sources;
1878
- }
1879
- function buildTestRunReportDump(result, index) {
1880
- const sourcesByScope = new Map();
1881
- const detailsByScopeAndExecution = new Map();
1882
- for (const source of index.sources){
1883
- const scopeSources = sourcesByScope.get(source.scopeId) ?? [];
1884
- scopeSources.push(source);
1885
- sourcesByScope.set(source.scopeId, scopeSources);
1886
- for (const executionId of source.executionIds)detailsByScopeAndExecution.set(scopedExecutionKey(source.scopeId, executionId), {
1887
- reportId: source.reportId,
1888
- executionId
498
+ const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
499
+ if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
500
+ caseId
1889
501
  });
1890
- }
1891
- const context = {
1892
- diagnostics: [],
1893
- sourcesByScope,
1894
- detailsByScopeAndExecution,
1895
- tracedScopeByExecution: new Map(),
1896
- warnedDiagnostics: new Set()
1897
- };
1898
- for (const source of index.sources)if (0 === source.executionIds.length) addDiagnostic(context, {
1899
- level: 'warning',
1900
- code: 'source-without-executions',
1901
- message: `Agent report contains no executions: ${source.sourcePath}`,
1902
- scopeId: source.scopeId,
1903
- sourcePaths: [
1904
- source.sourcePath
1905
- ]
502
+ ids.add(caseId);
503
+ return {
504
+ caseId,
505
+ projectId: source.projectId,
506
+ sourcePath,
507
+ caseIndex,
508
+ definition: {
509
+ name: definition.name,
510
+ tags: tags,
511
+ steps
512
+ }
513
+ };
1906
514
  });
1907
- const projects = result.projects.map((project)=>buildProject(project, context));
1908
515
  return {
1909
- schemaVersion: 1,
1910
- kind: 'test-runner',
1911
- runId: result.runId,
1912
- status: result.status,
1913
- startedAt: result.startedAt,
1914
- endedAt: result.endedAt,
1915
- durationMs: result.durationMs,
1916
- summary: {
1917
- ...result.summary
1918
- },
1919
- metrics: index.metrics,
1920
- projects,
1921
- ...context.diagnostics.length > 0 ? {
1922
- diagnostics: context.diagnostics
1923
- } : {}
516
+ documentId: createWorkflowDocumentId(source.projectId, sourcePath),
517
+ projectId: source.projectId,
518
+ sourcePath,
519
+ lifecycle,
520
+ cases
1924
521
  };
1925
522
  }
1926
- const writeJson = (path, value)=>{
1927
- (0, external_node_fs_namespaceObject.mkdirSync)((0, external_node_path_namespaceObject.dirname)(path), {
1928
- recursive: true
1929
- });
1930
- (0, external_node_fs_namespaceObject.writeFileSync)(path, JSON.stringify(value, null, 2));
523
+ const promises_namespaceObject = require("node:fs/promises");
524
+ const writeJson = async (path, value)=>{
525
+ try {
526
+ await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(path), {
527
+ recursive: true
528
+ });
529
+ await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
530
+ } catch (error) {
531
+ throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
532
+ }
1931
533
  };
1932
534
  const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
1933
535
  const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
1934
- const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, 'document.json'));
536
+ const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
1935
537
  const collectionErrorPath = (projectId, sourcePath)=>{
1936
538
  const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
1937
539
  projectId,
@@ -1941,21 +543,19 @@ const collectionErrorPath = (projectId, sourcePath)=>{
1941
543
  };
1942
544
  const writeWorkflowDocumentResult = (runDir, result)=>{
1943
545
  const { documentRunId: _documentRunId, ...persistedResult } = result;
1944
- writeJson((0, external_node_path_namespaceObject.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
546
+ return writeJson((0, external_node_path_namespaceObject.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
1945
547
  };
1946
548
  const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
1947
549
  const { runId: attemptId, ...persistedResult } = result;
1948
- writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
550
+ return writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
1949
551
  ...persistedResult,
1950
552
  attemptId
1951
553
  });
1952
554
  };
1953
- const writeCollectionError = (runDir, collectionError)=>{
1954
- writeJson((0, external_node_path_namespaceObject.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
555
+ const writeCollectionError = (runDir, collectionError)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
1955
556
  kind: 'collection-error',
1956
557
  ...collectionError
1957
558
  });
1958
- };
1959
559
  const relativeToSummary = (summaryPath, target)=>{
1960
560
  const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
1961
561
  return toPosix(value || '.');
@@ -1964,7 +564,7 @@ const errorJson = (error)=>error instanceof Error && 'toJSON' in error ? error.t
1964
564
  const writeTestProjectRunResult = (options)=>{
1965
565
  const { result } = options;
1966
566
  const fact = (path)=>toPosix(path);
1967
- writeJson(result.summaryPath, {
567
+ return writeJson(result.summaryPath, {
1968
568
  schemaVersion: result.schemaVersion,
1969
569
  runId: result.runId,
1970
570
  startedAt: result.startedAt,
@@ -1972,6 +572,9 @@ const writeTestProjectRunResult = (options)=>{
1972
572
  durationMs: result.durationMs,
1973
573
  status: result.status,
1974
574
  exitCode: result.exitCode,
575
+ ...result.errors?.length ? {
576
+ errors: result.errors.map(errorJson)
577
+ } : {},
1975
578
  projectRoot: options.projectRoot,
1976
579
  ...options.configPath ? {
1977
580
  configPath: options.configPath
@@ -2007,6 +610,9 @@ const writeTestProjectRunResult = (options)=>{
2007
610
  }
2008
611
  } : {},
2009
612
  cases: project.cases.map((outcome)=>({
613
+ ...outcome.documentRunId ? {
614
+ documentRunId: outcome.documentRunId
615
+ } : {},
2010
616
  documentId: outcome.documentId,
2011
617
  caseId: outcome.caseId,
2012
618
  sourcePath: outcome.sourcePath,
@@ -2030,6 +636,10 @@ const writeTestProjectRunResult = (options)=>{
2030
636
  })),
2031
637
  documents: project.documents.map((document)=>({
2032
638
  documentId: document.documentId,
639
+ ...void 0 === document.attemptIndex ? {} : {
640
+ attemptIndex: document.attemptIndex,
641
+ documentRunId: document.documentRunId
642
+ },
2033
643
  sourcePath: document.sourcePath,
2034
644
  status: document.status,
2035
645
  resultFile: fact(workflowDocumentResultPath(document)),
@@ -2064,32 +674,36 @@ const DEFAULT_TEST_FILE_SELECTION = {
2064
674
  '**/*.{yaml,yml}'
2065
675
  ]
2066
676
  };
2067
- const test_project_runner_toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
2068
- const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)=>{
677
+ const project_preparation_toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
678
+ const discoverResolvedTestFiles = (projectRoot, selection)=>{
2069
679
  const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
2070
- const normalized = validateTestFileSelection(selection);
2071
- if (!normalized) throw new TypeError('Test file selection is required.');
2072
- const files = (0, external_tinyglobby_namespaceObject.globSync)(normalized.include, {
2073
- absolute: true,
2074
- caseSensitiveMatch: false,
2075
- cwd: root,
2076
- dot: true,
2077
- expandDirectories: false,
2078
- followSymbolicLinks: false,
2079
- ignore: [
2080
- ...ALWAYS_IGNORED_PATTERNS,
2081
- ...normalized.exclude ?? []
2082
- ],
2083
- onlyFiles: true
2084
- }).filter((file)=>/\.ya?ml$/i.test(file));
680
+ const match = (patterns)=>(0, external_tinyglobby_namespaceObject.globSync)(patterns, {
681
+ absolute: true,
682
+ caseSensitiveMatch: false,
683
+ cwd: root,
684
+ dot: true,
685
+ expandDirectories: false,
686
+ followSymbolicLinks: false,
687
+ ignore: [
688
+ ...ALWAYS_IGNORED_PATTERNS,
689
+ ...selection.exclude ?? []
690
+ ],
691
+ onlyFiles: true
692
+ }).filter((file)=>/\.ya?ml$/i.test(file));
693
+ const files = match(selection.include);
2085
694
  return [
2086
695
  ...new Set(files.map((file)=>(0, external_node_path_namespaceObject.resolve)(file)))
2087
696
  ].sort((a, b)=>{
2088
- const relativeA = test_project_runner_toPosix((0, external_node_path_namespaceObject.relative)(root, a));
2089
- const relativeB = test_project_runner_toPosix((0, external_node_path_namespaceObject.relative)(root, b));
697
+ const relativeA = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, a));
698
+ const relativeB = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, b));
2090
699
  return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
2091
700
  });
2092
701
  };
702
+ const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)=>{
703
+ const normalized = validateTestFileSelection(selection);
704
+ if (!normalized) throw new TypeError('Test file selection is required.');
705
+ return discoverResolvedTestFiles(projectRoot, normalized);
706
+ };
2093
707
  const discoverTestConfig = (projectRoot)=>{
2094
708
  const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
2095
709
  const candidates = (0, external_node_fs_namespaceObject.readdirSync)(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
@@ -2107,27 +721,11 @@ const discoverTestConfig = (projectRoot)=>{
2107
721
  ].join('\n'));
2108
722
  return supported[0] ? (0, external_node_path_namespaceObject.join)(root, supported[0]) : void 0;
2109
723
  };
2110
- const defaultResultDir = (projectRoot)=>(0, external_node_path_namespaceObject.join)(projectRoot, '.midscene', 'test-results');
2111
- const padDatePart = (value)=>String(value).padStart(2, '0');
2112
- const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
2113
- const timestamp = [
2114
- date.getFullYear(),
2115
- padDatePart(date.getMonth() + 1),
2116
- padDatePart(date.getDate()),
2117
- padDatePart(date.getHours()),
2118
- padDatePart(date.getMinutes()),
2119
- padDatePart(date.getSeconds())
2120
- ].join('');
2121
- return `${timestamp}-${uuid.slice(0, 8)}`;
2122
- };
2123
- const assertDirectory = (path, label)=>{
2124
- if (!(0, external_node_fs_namespaceObject.existsSync)(path) || !(0, external_node_fs_namespaceObject.statSync)(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
2125
- };
2126
724
  const asCollectionError = (projectId, projectName, sourcePath, error)=>({
2127
725
  projectId,
2128
726
  projectName,
2129
727
  sourcePath,
2130
- error: error instanceof WorkflowError ? error : new WorkflowParseError(`Failed to collect workflow document "${sourcePath}": ${error instanceof Error ? error.message : String(error)}`, {
728
+ error: error instanceof test_runner_namespaceObject.WorkflowError ? error : new test_runner_namespaceObject.WorkflowParseError(`Failed to collect workflow document "${sourcePath}": ${error instanceof Error ? error.message : String(error)}`, {
2131
729
  projectName,
2132
730
  sourcePath
2133
731
  }, error)
@@ -2149,52 +747,6 @@ const filterDocumentCases = (document, project)=>{
2149
747
  filtered
2150
748
  };
2151
749
  };
2152
- const test_project_runner_asNotRun = (documentId, collectedCase, projectName, reason)=>({
2153
- documentId,
2154
- caseId: collectedCase.caseId,
2155
- projectName,
2156
- name: collectedCase.definition.name,
2157
- sourcePath: collectedCase.sourcePath,
2158
- caseIndex: collectedCase.caseIndex,
2159
- status: 'not-run',
2160
- notRunReason: reason
2161
- });
2162
- const summarize = (projects)=>{
2163
- const cases = projects.flatMap((project)=>project.cases);
2164
- const documents = projects.flatMap((project)=>project.documents);
2165
- return {
2166
- total: cases.length,
2167
- passed: cases.filter((item)=>'success' === item.status).length,
2168
- failed: cases.filter((item)=>'failed' === item.status).length,
2169
- notRun: cases.filter((item)=>'not-run' === item.status).length,
2170
- filtered: projects.reduce((total, project)=>total + project.filteredCaseCount, 0),
2171
- collectionErrors: projects.reduce((total, project)=>total + project.collectionErrors.length, 0),
2172
- documentFailures: documents.filter((document)=>'failed' === document.status).length,
2173
- projectFailures: projects.filter((project)=>'failed' === project.status).length
2174
- };
2175
- };
2176
- const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
2177
- const test_project_runner_formatStep = (info)=>{
2178
- const position = stepPosition(info);
2179
- const phase = 'steps' === position.phase ? 'step' : position.phase;
2180
- return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
2181
- };
2182
- const formatStepResult = (info, result)=>{
2183
- const indent = 'case' === info.scope ? ' ' : ' ';
2184
- const symbol = 'success' === result.status ? '✓' : '✗';
2185
- const error = result.error ? ` — ${result.error.message}` : '';
2186
- const continuation = result.continuedAfterError ? '; continuing' : '';
2187
- return `${indent}${symbol} ${test_project_runner_formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
2188
- };
2189
- const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
2190
- ...attempt.beforeEach,
2191
- ...attempt.steps,
2192
- ...attempt.afterEach
2193
- ].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError));
2194
- const documentHasFatalError = (result)=>[
2195
- ...result.beforeAll,
2196
- ...result.afterAll
2197
- ].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError);
2198
750
  const selectProjects = (projects, names)=>{
2199
751
  if (!names || 0 === names.length) return projects;
2200
752
  const requested = new Set(names);
@@ -2204,74 +756,316 @@ const selectProjects = (projects, names)=>{
2204
756
  if (unknown) throw new Error(`Unknown Midscene project: ${unknown}`);
2205
757
  return projects.filter((project)=>requested.has(project.name));
2206
758
  };
2207
- const prepareProject = (project, projectRoot, runDir)=>{
759
+ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, options = {}) {
2208
760
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
2209
- const files = discoverTestFiles(projectRoot, fileSelection);
2210
- const sources = files.map((absolutePath)=>({
761
+ const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_namespaceObject.resolve)(options.prerequisiteFile) : void 0;
762
+ const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
763
+ const files = [
764
+ ...prerequisiteFile ? [
765
+ prerequisiteFile
766
+ ] : [],
767
+ ...mainFiles.filter((file)=>file !== prerequisiteFile)
768
+ ];
769
+ const occurrences = new Map();
770
+ const sources = files.map((absolutePath)=>{
771
+ const invocationIndex = occurrences.get(absolutePath) ?? 0;
772
+ occurrences.set(absolutePath, invocationIndex + 1);
773
+ return {
2211
774
  projectId: project.projectId,
2212
775
  projectName: project.name,
2213
- sourcePath: test_project_runner_toPosix((0, external_node_path_namespaceObject.relative)(projectRoot, absolutePath)),
2214
- absolutePath
2215
- }));
776
+ sourcePath: project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(projectRoot, absolutePath)),
777
+ absolutePath,
778
+ invocationIndex
779
+ };
780
+ });
2216
781
  const collectionErrors = [];
2217
- const documents = [];
782
+ const invocations = [];
2218
783
  let filteredCaseCount = 0;
2219
784
  if (0 === sources.length) {
2220
- const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
785
+ const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
2221
786
  projectName: project.name,
2222
787
  projectRoot
2223
788
  }));
2224
789
  collectionErrors.push(error);
2225
- writeCollectionError(runDir, error);
790
+ await writeCollectionError(runDir, error);
2226
791
  }
2227
792
  for (const source of sources)try {
2228
- const collected = collectWorkflowDocument(source, {
793
+ const collected = await options.collectDocument?.(source, project);
794
+ if (collected) {
795
+ filteredCaseCount += collected.filteredCaseCount;
796
+ if (collected.invocation) invocations.push(collected.invocation);
797
+ continue;
798
+ }
799
+ const document = collectWorkflowDocument(source, {
2229
800
  resolveNode: project.nodes.get.bind(project.nodes),
2230
801
  variables: project.variables,
2231
802
  env: process.env
2232
803
  });
2233
- const filtered = filterDocumentCases(collected, project);
804
+ const filtered = source.absolutePath === prerequisiteFile ? {
805
+ document,
806
+ filtered: 0
807
+ } : filterDocumentCases(document, project);
2234
808
  filteredCaseCount += filtered.filtered;
2235
- if (filtered.document) documents.push(filtered.document);
809
+ if (filtered.document) invocations.push({
810
+ document: filtered.document,
811
+ retry: {
812
+ scope: 'case',
813
+ count: project.retry
814
+ },
815
+ defaultTimeoutMs,
816
+ reportEnabled: true,
817
+ bindings: {
818
+ resolveNode: project.nodes.require.bind(project.nodes)
819
+ }
820
+ });
2236
821
  } catch (error) {
2237
822
  const collectionError = asCollectionError(project.projectId, project.name, source.sourcePath, error);
2238
823
  collectionErrors.push(collectionError);
2239
- writeCollectionError(runDir, collectionError);
824
+ await writeCollectionError(runDir, collectionError);
2240
825
  }
826
+ const documents = invocations.map((invocation)=>invocation.document);
827
+ const prerequisiteSource = sources.find((source)=>source.absolutePath === prerequisiteFile);
828
+ const prerequisiteDocument = documents.find((document)=>document.sourcePath === prerequisiteSource?.sourcePath);
2241
829
  return {
2242
830
  project,
831
+ platform: options.platform ?? 'test',
832
+ documentConcurrency: options.documentConcurrency ?? 1,
833
+ ...prerequisiteDocument ? {
834
+ prerequisiteDocumentId: prerequisiteDocument.documentId
835
+ } : {},
2243
836
  fileSelection,
2244
837
  sources,
2245
838
  documents,
839
+ invocations,
2246
840
  collectionErrors,
2247
841
  selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
2248
842
  filteredCaseCount
2249
843
  };
844
+ }
845
+ const padDatePart = (value)=>String(value).padStart(2, '0');
846
+ const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
847
+ const timestamp = [
848
+ date.getFullYear(),
849
+ padDatePart(date.getMonth() + 1),
850
+ padDatePart(date.getDate()),
851
+ padDatePart(date.getHours()),
852
+ padDatePart(date.getMinutes()),
853
+ padDatePart(date.getSeconds())
854
+ ].join('');
855
+ return `${timestamp}-${uuid.slice(0, 8)}`;
2250
856
  };
2251
- const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>test_project_runner_asNotRun(document.documentId, item, prepared.project.name, reason)));
2252
- async function runTestProject(options = {}) {
2253
- const startedAt = new Date();
2254
- const runId = createTestRunId(startedAt);
857
+ const assertDirectory = (path, label)=>{
858
+ if (!(0, external_node_fs_namespaceObject.existsSync)(path) || !(0, external_node_fs_namespaceObject.statSync)(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
859
+ };
860
+ const resolveRunInput = (options)=>{
2255
861
  const cwd = (0, external_node_path_namespaceObject.resolve)(options.cwd ?? process.cwd());
2256
862
  assertDirectory(cwd, 'Test working directory');
2257
- const cliProjectRoot = options.projectRoot ? (0, external_node_path_namespaceObject.resolve)(cwd, options.projectRoot) : void 0;
863
+ const inputPath = options.projectRoot ? (0, external_node_path_namespaceObject.resolve)(cwd, options.projectRoot) : void 0;
864
+ const singleFile = inputPath && (0, external_node_fs_namespaceObject.existsSync)(inputPath) && (0, external_node_fs_namespaceObject.statSync)(inputPath).isFile() ? inputPath : void 0;
865
+ if (singleFile && !/\.ya?ml$/i.test(singleFile)) throw new Error(`Test input must be a YAML file or project directory: ${singleFile}`);
866
+ const cliProjectRoot = singleFile ? (0, external_node_path_namespaceObject.dirname)(singleFile) : inputPath;
2258
867
  if (cliProjectRoot) assertDirectory(cliProjectRoot, 'Test project directory');
2259
- const configSearchRoot = cliProjectRoot ?? cwd;
2260
- const configPath = options.configPath ? (0, external_node_path_namespaceObject.resolve)(configSearchRoot, options.configPath) : discoverTestConfig(configSearchRoot);
2261
- if (options.configPath && (!configPath || !(0, external_node_fs_namespaceObject.existsSync)(configPath))) throw new Error(`Midscene config does not exist: ${configPath}`);
2262
- const definition = await loadTestProject(configPath);
2263
868
  const projectRoot = cliProjectRoot ?? cwd;
2264
- const resultDir = options.resultDir ? (0, external_node_path_namespaceObject.resolve)(cwd, options.resultDir) : defaultResultDir(projectRoot);
869
+ return {
870
+ cwd,
871
+ projectRoot,
872
+ configSearchRoot: projectRoot,
873
+ ...singleFile ? {
874
+ singleFile
875
+ } : {}
876
+ };
877
+ };
878
+ async function prepareTestRun(options) {
879
+ const startedAt = new Date();
880
+ const runId = createTestRunId(startedAt);
881
+ const input = resolveRunInput(options);
882
+ const configPath = options.configPath ? (0, external_node_path_namespaceObject.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
883
+ if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test --config only accepts a native TypeScript or JavaScript project config: ${configPath}. Run legacy batch YAML with \`midscene --config ${options.configPath}\`.`);
884
+ if (options.configPath && !(0, external_node_fs_namespaceObject.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
885
+ const definition = await loadTestProject(configPath);
886
+ const resultDir = options.resultDir ? (0, external_node_path_namespaceObject.resolve)(input.cwd, options.resultDir) : (0, external_node_path_namespaceObject.join)(input.projectRoot, '.midscene', 'test-results');
2265
887
  const runDir = (0, external_node_path_namespaceObject.join)(resultDir, runId);
2266
888
  const summaryPath = (0, external_node_path_namespaceObject.join)(runDir, 'summary.json');
2267
- const reportDir = (0, external_node_path_namespaceObject.resolve)(projectRoot, definition.output.reportDir);
889
+ const reportDir = (0, external_node_path_namespaceObject.resolve)(input.projectRoot, definition.output.reportDir);
2268
890
  (0, external_node_fs_namespaceObject.mkdirSync)(resultDir, {
2269
891
  recursive: true
2270
892
  });
2271
893
  (0, external_node_fs_namespaceObject.mkdirSync)(runDir);
2272
- const selectedProjects = selectProjects(definition.projects, options.projectNames);
2273
- const preparedProjects = selectedProjects.map((project)=>prepareProject(project, projectRoot, runDir));
2274
- const progress = options.onProgress ?? (()=>{});
894
+ const projects = [];
895
+ for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
896
+ files: [
897
+ input.singleFile
898
+ ]
899
+ } : void 0));
900
+ const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
901
+ return {
902
+ startedAt,
903
+ runId,
904
+ projectRoot: input.projectRoot,
905
+ ...configPath ? {
906
+ configPath
907
+ } : {},
908
+ resultDir,
909
+ runDir,
910
+ summaryPath,
911
+ reportDir,
912
+ definition,
913
+ projects,
914
+ preflightScope: hasFormatMismatch ? 'run' : 'project',
915
+ reportEnabled: true,
916
+ publications: [],
917
+ onProgress: options.onProgress ?? (()=>{})
918
+ };
919
+ }
920
+ const external_node_events_namespaceObject = require("node:events");
921
+ const report_namespaceObject = require("@midscene/core/report");
922
+ const logger_namespaceObject = require("@midscene/shared/logger");
923
+ const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
924
+ const formatStep = (info)=>{
925
+ const position = stepPosition(info);
926
+ const phase = 'steps' === position.phase ? 'step' : position.phase;
927
+ return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
928
+ };
929
+ const formatStepResult = (info, result)=>{
930
+ const indent = 'case' === info.scope ? ' ' : ' ';
931
+ const symbol = 'success' === result.status ? '✓' : '✗';
932
+ const error = result.error ? ` — ${result.error.message}` : '';
933
+ const continuation = result.continuedAfterError ? '; continuing' : '';
934
+ return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
935
+ };
936
+ const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
937
+ ...attempt.beforeEach,
938
+ ...attempt.steps,
939
+ ...attempt.afterEach
940
+ ].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (attempt.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError));
941
+ const documentHasFatalError = (result)=>[
942
+ ...result.beforeAll,
943
+ ...result.afterAll
944
+ ].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (result.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError);
945
+ async function executeDocumentInvocation(options) {
946
+ const { invocation, project, sinks } = options;
947
+ const { document } = invocation;
948
+ const documentRetry = 'document' === invocation.retry.scope;
949
+ const attempts = [];
950
+ await (0, test_runner_namespaceObject.runDocumentAttempts)({
951
+ retry: documentRetry ? invocation.retry.count : 0,
952
+ signal: options.signal,
953
+ shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
954
+ }, async (documentAttemptIndex)=>{
955
+ if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
956
+ const execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(document, {
957
+ documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
958
+ ...invocation.bindings,
959
+ project,
960
+ projectContext: options.projectContext,
961
+ retry: documentRetry ? 0 : invocation.retry.count,
962
+ signal: options.signal,
963
+ defaultTimeoutMs: invocation.defaultTimeoutMs,
964
+ shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
965
+ stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
966
+ isFatalError: (run)=>[
967
+ ...run.beforeEach,
968
+ ...run.steps,
969
+ ...run.afterEach
970
+ ].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (run.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError),
971
+ onCaseStart: (collectedCase)=>{
972
+ options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
973
+ },
974
+ onStepStart: (info)=>{
975
+ const indent = 'case' === info.scope ? ' ' : ' ';
976
+ options.onProgress(`${indent}→ ${formatStep(info)}`);
977
+ },
978
+ onStepResult: async (info, result)=>{
979
+ await invocation.bindings.onStepResult?.(info, result);
980
+ options.onProgress(formatStepResult(info, result));
981
+ },
982
+ onCaseResult: async (attempt)=>{
983
+ await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
984
+ options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
985
+ },
986
+ onCaseOutcome: (outcome)=>{
987
+ if (caseHasFatalError(outcome)) options.markProjectFatal();
988
+ if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
989
+ },
990
+ onDocumentResult: options.onDocumentResult
991
+ }).catch(async (error)=>{
992
+ if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) {
993
+ const partial = error.result;
994
+ attempts.push(partial);
995
+ sinks.documents.push(partial.document);
996
+ sinks.cases.push(...partial.cases.map((outcome)=>({
997
+ ...outcome,
998
+ documentId: document.documentId,
999
+ documentRunId: partial.document.documentRunId
1000
+ })));
1001
+ try {
1002
+ await writeWorkflowDocumentResult(options.runDir, partial.document);
1003
+ } catch (publicationError) {
1004
+ throw new test_runner_namespaceObject.WorkflowExecutionFailure(partial, [
1005
+ ...error.errors,
1006
+ publicationError
1007
+ ]);
1008
+ }
1009
+ }
1010
+ throw error;
1011
+ });
1012
+ attempts.push(execution);
1013
+ sinks.cases.push(...execution.cases.map((outcome)=>({
1014
+ ...outcome,
1015
+ documentId: document.documentId,
1016
+ documentRunId: execution.document.documentRunId
1017
+ })));
1018
+ sinks.documents.push(execution.document);
1019
+ await writeWorkflowDocumentResult(options.runDir, execution.document);
1020
+ if (documentHasFatalError(execution.document)) options.markProjectFatal();
1021
+ const cleanupErrors = [
1022
+ ...execution.document.teardownErrors ?? [],
1023
+ ...execution.cases.flatMap((outcome)=>(outcome.attempts ?? []).flatMap((attempt)=>attempt.teardownErrors ?? []))
1024
+ ];
1025
+ if (cleanupErrors.length) throw new test_runner_namespaceObject.WorkflowExecutionFailure(execution, cleanupErrors);
1026
+ const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
1027
+ return failed ? 'failed' : 'success';
1028
+ });
1029
+ const final = attempts.at(-1);
1030
+ if (!final) return;
1031
+ const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
1032
+ if (documentRetry && failedCases) options.addFailedCases(failedCases);
1033
+ else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
1034
+ }
1035
+ const asNotRun = (documentId, collectedCase, projectName, reason)=>({
1036
+ documentId,
1037
+ caseId: collectedCase.caseId,
1038
+ projectName,
1039
+ name: collectedCase.definition.name,
1040
+ sourcePath: collectedCase.sourcePath,
1041
+ caseIndex: collectedCase.caseIndex,
1042
+ status: 'not-run',
1043
+ notRunReason: reason
1044
+ });
1045
+ const summarize = (projects)=>{
1046
+ const cases = projects.flatMap((project)=>latestById(project.cases, (item)=>item.caseId));
1047
+ const documents = projects.flatMap((project)=>latestById(project.documents, (item)=>item.documentId));
1048
+ return {
1049
+ total: cases.length,
1050
+ passed: cases.filter((item)=>'success' === item.status).length,
1051
+ failed: cases.filter((item)=>'failed' === item.status).length,
1052
+ notRun: cases.filter((item)=>'not-run' === item.status).length,
1053
+ filtered: projects.reduce((total, project)=>total + project.filteredCaseCount, 0),
1054
+ collectionErrors: projects.reduce((total, project)=>total + project.collectionErrors.length, 0),
1055
+ documentFailures: documents.filter((document)=>'failed' === document.status).length,
1056
+ projectFailures: projects.filter((project)=>'failed' === project.status).length
1057
+ };
1058
+ };
1059
+ const latestById = (items, id)=>[
1060
+ ...new Map(items.map((item)=>[
1061
+ id(item),
1062
+ item
1063
+ ])).values()
1064
+ ];
1065
+ const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
1066
+ async function runPreparedTestProject(plan) {
1067
+ const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
1068
+ const progress = plan.onProgress;
2275
1069
  const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
2276
1070
  const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
2277
1071
  const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
@@ -2289,11 +1083,13 @@ async function runTestProject(options = {}) {
2289
1083
  let failedCaseCount = 0;
2290
1084
  const bailReached = ()=>definition.test.bail > 0 && failedCaseCount >= definition.test.bail;
2291
1085
  let hasInfrastructureError = false;
2292
- let firstInfrastructureError;
1086
+ const infrastructureErrors = [];
2293
1087
  const recordInfrastructureError = (error)=>{
2294
- if (hasInfrastructureError) return;
1088
+ const errors = error instanceof test_runner_namespaceObject.WorkflowExecutionFailure ? error.errors : [
1089
+ error
1090
+ ];
1091
+ for (const item of errors)if (!infrastructureErrors.includes(item)) infrastructureErrors.push(item);
2295
1092
  hasInfrastructureError = true;
2296
- firstInfrastructureError = error;
2297
1093
  rootController.abort(error);
2298
1094
  };
2299
1095
  const runInfrastructureCallback = (callback)=>{
@@ -2310,10 +1106,11 @@ async function runTestProject(options = {}) {
2310
1106
  };
2311
1107
  const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
2312
1108
  const { project } = prepared;
2313
- const projectFailed = prepared.collectionErrors.length > 0 || cases.some((item)=>'success' !== item.status) || documents.some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
1109
+ const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
2314
1110
  return {
2315
1111
  projectId: project.projectId,
2316
1112
  name: project.name,
1113
+ platform: prepared.platform,
2317
1114
  status: projectFailed ? 'failed' : 'success',
2318
1115
  retry: project.retry,
2319
1116
  fileSelection: prepared.fileSelection,
@@ -2341,106 +1138,111 @@ async function runTestProject(options = {}) {
2341
1138
  const documents = [];
2342
1139
  let lifecycle;
2343
1140
  let projectFatal = false;
2344
- if (prepared.collectionErrors.length > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
1141
+ if (prepared.collectionErrors.length > 0 || 'run' === plan.preflightScope && totalErrors > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
2345
1142
  else if (rootController.signal.aborted) cases.push(...notRunSuite(prepared, 'interrupted'));
2346
1143
  else if (bailReached()) cases.push(...notRunSuite(prepared, 'bail'));
2347
1144
  else {
2348
- const runtime = createProjectRuntime({
1145
+ const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
2349
1146
  project,
2350
1147
  setup: project.setup,
2351
1148
  signal: rootController.signal
2352
1149
  });
2353
1150
  let hasProjectExecutionError = false;
2354
- let projectExecutionError;
2355
1151
  try {
2356
1152
  await runtime.start();
2357
- if (runtime.canRun) for (const [documentIndex, document] of prepared.documents.entries()){
2358
- if (rootController.signal.aborted || bailReached() || projectFatal) {
2359
- const reason = rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
2360
- cases.push(...document.cases.map((item)=>test_project_runner_asNotRun(document.documentId, item, project.name, reason)));
2361
- continue;
1153
+ if (runtime.canRun) {
1154
+ const runDocument = async (invocation, documentIndex)=>{
1155
+ const { document } = invocation;
1156
+ if (rootController.signal.aborted || bailReached() || projectFatal) {
1157
+ const reason = rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
1158
+ cases.push(...document.cases.map((item)=>asNotRun(document.documentId, item, project.name, reason)));
1159
+ return;
1160
+ }
1161
+ projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
1162
+ await executeDocumentInvocation({
1163
+ invocation,
1164
+ project,
1165
+ projectContext: runtime.context,
1166
+ runDir,
1167
+ signal: runtime.signal,
1168
+ rootSignal: rootController.signal,
1169
+ shouldBail: bailReached,
1170
+ isProjectFatal: ()=>projectFatal,
1171
+ markProjectFatal: ()=>{
1172
+ projectFatal = true;
1173
+ },
1174
+ addFailedCases: (count)=>{
1175
+ failedCaseCount += count;
1176
+ },
1177
+ onProgress: projectProgress,
1178
+ sinks: {
1179
+ cases,
1180
+ documents
1181
+ }
1182
+ });
1183
+ };
1184
+ let prerequisiteFailed = false;
1185
+ const prerequisite = prepared.invocations.find((invocation)=>invocation.document.documentId === prepared.prerequisiteDocumentId);
1186
+ const prerequisiteCount = prerequisite ? 1 : 0;
1187
+ if (prerequisite) {
1188
+ await runDocument(prerequisite, 0);
1189
+ prerequisiteFailed = documents.at(-1)?.status === 'failed' || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status);
2362
1190
  }
2363
- projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
2364
- const execution = await runWorkflowDocument(document, {
2365
- resolveNode: project.nodes.require.bind(project.nodes),
2366
- project,
2367
- projectContext: runtime.context,
2368
- retry: project.retry,
2369
- signal: runtime.signal,
2370
- defaultTimeoutMs: definition.test.testTimeout,
2371
- shouldStop: ()=>rootController.signal.aborted || bailReached() || projectFatal,
2372
- stopReason: ()=>rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail',
2373
- isFatalError: (run)=>[
2374
- ...run.beforeEach,
2375
- ...run.steps,
2376
- ...run.afterEach
2377
- ].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
2378
- onCaseStart: (collectedCase)=>{
2379
- projectProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
2380
- },
2381
- onStepStart: (info)=>{
2382
- const indent = 'case' === info.scope ? ' ' : ' ';
2383
- projectProgress(`${indent}→ ${test_project_runner_formatStep(info)}`);
2384
- },
2385
- onStepResult: (info, result)=>projectProgress(formatStepResult(info, result)),
2386
- onCaseResult: (attempt)=>{
2387
- runInfrastructureCallback(()=>writeCaseAttemptResult(runDir, project.projectId, document.documentId, attempt));
2388
- projectProgress(` ${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
2389
- },
2390
- onCaseOutcome: (outcome)=>{
2391
- if ('failed' === outcome.status) failedCaseCount += 1;
2392
- if (caseHasFatalError(outcome)) projectFatal = true;
2393
- },
2394
- onDocumentResult: (documentResult)=>runInfrastructureCallback(()=>writeWorkflowDocumentResult(runDir, documentResult))
1191
+ if (!prerequisiteFailed) await (0, test_runner_namespaceObject.runConcurrentJobs)(prepared.invocations.filter((invocation)=>invocation !== prerequisite), {
1192
+ concurrency: prepared.documentConcurrency,
1193
+ shouldStop: ()=>rootController.signal.aborted || bailReached() || projectFatal
1194
+ }, async (invocation, index)=>{
1195
+ try {
1196
+ await runDocument(invocation, index + prerequisiteCount);
1197
+ } catch (error) {
1198
+ recordInfrastructureError(error);
1199
+ throw error;
1200
+ }
2395
1201
  });
2396
- cases.push(...execution.cases.map((outcome)=>({
2397
- ...outcome,
2398
- documentId: document.documentId
2399
- })));
2400
- if (documentHasFatalError(execution.document)) projectFatal = true;
2401
- documents.push(execution.document);
2402
- }
2403
- else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
1202
+ const completed = new Set(cases.map((item)=>item.caseId));
1203
+ const reason = prerequisiteFailed ? 'project-setup-failed' : rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
1204
+ cases.push(...notRunSuite(prepared, reason).filter((item)=>!completed.has(item.caseId)));
1205
+ } else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
2404
1206
  } catch (error) {
2405
1207
  hasProjectExecutionError = true;
2406
- projectExecutionError = error;
2407
1208
  recordInfrastructureError(error);
2408
1209
  } finally{
2409
- const hasFailure = hasProjectExecutionError || cases.some((item)=>'success' !== item.status) || documents.some((item)=>'failed' === item.status) || projectFatal || rootController.signal.aborted;
1210
+ const hasFailure = hasProjectExecutionError || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || projectFatal || rootController.signal.aborted;
2410
1211
  lifecycle = await runtime.finish(hasFailure ? 'failed' : 'success');
2411
1212
  }
2412
- if (hasProjectExecutionError) throw projectExecutionError;
1213
+ if (hasProjectExecutionError) {
1214
+ const completed = new Set(cases.map((item)=>item.caseId));
1215
+ cases.push(...notRunSuite(prepared, 'interrupted').filter((item)=>!completed.has(item.caseId)));
1216
+ }
2413
1217
  }
1218
+ const order = new Map(prepared.documents.map((document, index)=>[
1219
+ document.documentId,
1220
+ index
1221
+ ]));
1222
+ const documentOrder = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
1223
+ cases.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || a.caseIndex - b.caseIndex);
1224
+ documents.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || (a.attemptIndex ?? 0) - (b.attemptIndex ?? 0));
2414
1225
  return buildProjectResult(prepared, cases, documents, lifecycle);
2415
1226
  };
2416
1227
  const projectResults = new Array(preparedProjects.length);
2417
- let nextProjectIndex = 0;
2418
- const claimNextProject = ()=>{
2419
- if (hasInfrastructureError || rootController.signal.aborted || bailReached() || nextProjectIndex >= preparedProjects.length) return;
2420
- const projectIndex = nextProjectIndex;
2421
- nextProjectIndex += 1;
2422
- return projectIndex;
2423
- };
2424
- const worker = async ()=>{
2425
- try {
2426
- while(true){
2427
- const projectIndex = claimNextProject();
2428
- if (void 0 === projectIndex) return;
2429
- projectResults[projectIndex] = await runPreparedProject(preparedProjects[projectIndex], projectIndex);
2430
- }
2431
- } catch (error) {
2432
- recordInfrastructureError(error);
2433
- }
2434
- };
2435
1228
  try {
2436
- await Promise.allSettled(Array.from({
2437
- length: effectiveConcurrency
2438
- }, ()=>worker()));
1229
+ await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
1230
+ concurrency: effectiveConcurrency,
1231
+ shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
1232
+ }, async (prepared, index)=>{
1233
+ try {
1234
+ projectResults[index] = await runPreparedProject(prepared, index);
1235
+ } catch (error) {
1236
+ recordInfrastructureError(error);
1237
+ throw error;
1238
+ }
1239
+ });
1240
+ } catch (error) {
1241
+ recordInfrastructureError(error);
2439
1242
  } finally{
2440
1243
  process.off('SIGINT', sigint);
2441
1244
  process.off('SIGTERM', sigterm);
2442
1245
  }
2443
- if (hasInfrastructureError) throw firstInfrastructureError;
2444
1246
  const completedProjectResults = preparedProjects.map((prepared, projectIndex)=>{
2445
1247
  const result = projectResults[projectIndex];
2446
1248
  if (result) return result;
@@ -2449,7 +1251,7 @@ async function runTestProject(options = {}) {
2449
1251
  return buildSkippedProjectResult(prepared, projectIndex, reason);
2450
1252
  });
2451
1253
  const summary = summarize(completedProjectResults);
2452
- const failed = rootController.signal.aborted || summary.failed > 0 || summary.notRun > 0 || summary.collectionErrors > 0 || summary.documentFailures > 0 || summary.projectFailures > 0;
1254
+ const failed = hasInfrastructureError || rootController.signal.aborted || summary.failed > 0 || summary.notRun > 0 || summary.collectionErrors > 0 || summary.documentFailures > 0 || summary.projectFailures > 0;
2453
1255
  const endedAt = new Date();
2454
1256
  const result = {
2455
1257
  schemaVersion: 3,
@@ -2466,34 +1268,66 @@ async function runTestProject(options = {}) {
2466
1268
  projects: completedProjectResults,
2467
1269
  cases: completedProjectResults.flatMap((project)=>project.cases),
2468
1270
  documents: completedProjectResults.flatMap((project)=>project.documents),
2469
- collectionErrors: completedProjectResults.flatMap((project)=>project.collectionErrors)
1271
+ collectionErrors: completedProjectResults.flatMap((project)=>project.collectionErrors),
1272
+ ...hasInfrastructureError ? {
1273
+ errors: infrastructureErrors.map(test_runner_namespaceObject.asExecutionError)
1274
+ } : {}
2470
1275
  };
2471
- writeTestProjectRunResult({
2472
- projectRoot,
2473
- ...configPath ? {
2474
- configPath
2475
- } : {},
2476
- result
2477
- });
2478
- const reportPath = new report_namespaceObject.TestRunReportAssembler().assemble({
2479
- outputDir: reportDir,
2480
- reportFileName: `test-run-${runId}`,
2481
- sources: collectTestRunReportSources(result),
2482
- buildRunnerDump: (index)=>buildTestRunReportDump(result, index)
2483
- });
2484
- const completedResult = {
2485
- ...result,
2486
- reportPath
1276
+ let completedResult = result;
1277
+ const publish = async (operation, path, callback)=>{
1278
+ try {
1279
+ await callback();
1280
+ return true;
1281
+ } catch (error) {
1282
+ recordInfrastructureError(error instanceof test_runner_namespaceObject.WorkflowPublicationError || error instanceof test_runner_namespaceObject.WorkflowExecutionFailure ? error : new test_runner_namespaceObject.WorkflowPublicationError(operation, path, error));
1283
+ completedResult = {
1284
+ ...completedResult,
1285
+ status: 'failed',
1286
+ exitCode: 1,
1287
+ errors: infrastructureErrors.map(test_runner_namespaceObject.asExecutionError)
1288
+ };
1289
+ return false;
1290
+ }
2487
1291
  };
2488
- writeTestProjectRunResult({
2489
- projectRoot,
2490
- ...configPath ? {
2491
- configPath
2492
- } : {},
2493
- result: completedResult
1292
+ const writeSummary = ()=>publish('write-result', summaryPath, ()=>writeTestProjectRunResult({
1293
+ projectRoot,
1294
+ ...configPath ? {
1295
+ configPath
1296
+ } : {},
1297
+ result: completedResult
1298
+ }));
1299
+ const writeReport = ()=>publish('write-report', reportDir, async ()=>{
1300
+ if (!plan.reportEnabled) return;
1301
+ const reportPath = await new report_namespaceObject.TestRunReportAssembler().assembleAsync({
1302
+ outputDir: reportDir,
1303
+ reportFileName: `midscene-e2e-${runId}`,
1304
+ overwrite: false,
1305
+ sources: (0, test_runner_namespaceObject.collectTestRunReportSources)(completedResult),
1306
+ buildRunnerDump: (index)=>{
1307
+ const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, index);
1308
+ const warn = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
1309
+ console: true
1310
+ });
1311
+ for (const diagnostic of dump.diagnostics ?? [])warn(diagnostic.message);
1312
+ return dump;
1313
+ }
1314
+ });
1315
+ completedResult = {
1316
+ ...completedResult,
1317
+ reportPath
1318
+ };
1319
+ });
1320
+ await writeReport();
1321
+ for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
1322
+ await publication.publish(completedResult);
2494
1323
  });
1324
+ await writeSummary();
1325
+ if (hasInfrastructureError) throw new test_runner_namespaceObject.WorkflowExecutionFailure(completedResult, infrastructureErrors);
2495
1326
  return completedResult;
2496
1327
  }
1328
+ async function runTestProject(options = {}) {
1329
+ return runPreparedTestProject(await prepareTestRun(options));
1330
+ }
2497
1331
  exports.DEFAULT_TEST_FILE_SELECTION = __webpack_exports__.DEFAULT_TEST_FILE_SELECTION;
2498
1332
  exports.createTestRunId = __webpack_exports__.createTestRunId;
2499
1333
  exports.defineProjectSetup = __webpack_exports__.defineProjectSetup;