@midscene/test 1.15.4-beta-20261013083552.0 → 1.15.4

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.
Files changed (68) hide show
  1. package/dist/es/{1~214.mjs → 1~4.mjs} +96 -174
  2. package/dist/es/1~4.mjs.map +1 -0
  3. package/dist/es/cli/index.mjs +741 -1899
  4. package/dist/es/cli/index.mjs.map +1 -1
  5. package/dist/es/index.mjs +1 -20
  6. package/dist/es/index.mjs.map +1 -1
  7. package/dist/es/midscene/index.mjs +2 -2
  8. package/dist/es/midscene/index.mjs.map +1 -1
  9. package/dist/es/test-cli.mjs +70 -1254
  10. package/dist/es/test-cli.mjs.map +1 -1
  11. package/dist/lib/{0~160.js → 0~593.js} +95 -173
  12. package/dist/lib/0~593.js.map +1 -0
  13. package/dist/lib/cli/index.js +740 -1913
  14. package/dist/lib/cli/index.js.map +1 -1
  15. package/dist/lib/index.js +1 -20
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/midscene/index.js +0 -3
  18. package/dist/lib/midscene/index.js.map +1 -1
  19. package/dist/lib/test-cli.js +70 -1254
  20. package/dist/lib/test-cli.js.map +1 -1
  21. package/dist/templates/manifest.json +60 -0
  22. package/dist/templates/test/android/.env.example +15 -0
  23. package/dist/templates/test/android/README.md +19 -0
  24. package/dist/templates/test/android/README.zh.md +11 -0
  25. package/dist/templates/test/android/cases/todo.yaml +26 -0
  26. package/dist/templates/test/android/gitignore +4 -0
  27. package/dist/templates/test/android/midscene.config.ts +69 -0
  28. package/dist/templates/test/android/package.json +20 -0
  29. package/dist/templates/test/android/tsconfig.json +12 -0
  30. package/dist/templates/test/computer/.env.example +17 -0
  31. package/dist/templates/test/computer/README.md +19 -0
  32. package/dist/templates/test/computer/README.zh.md +11 -0
  33. package/dist/templates/test/computer/cases/todo.yaml +31 -0
  34. package/dist/templates/test/computer/gitignore +4 -0
  35. package/dist/templates/test/computer/midscene.config.ts +74 -0
  36. package/dist/templates/test/computer/package.json +20 -0
  37. package/dist/templates/test/computer/tsconfig.json +12 -0
  38. package/dist/templates/test/harmony/.env.example +15 -0
  39. package/dist/templates/test/harmony/README.md +19 -0
  40. package/dist/templates/test/harmony/README.zh.md +11 -0
  41. package/dist/templates/test/harmony/cases/todo.yaml +26 -0
  42. package/dist/templates/test/harmony/gitignore +4 -0
  43. package/dist/templates/test/harmony/midscene.config.ts +69 -0
  44. package/dist/templates/test/harmony/package.json +20 -0
  45. package/dist/templates/test/harmony/tsconfig.json +12 -0
  46. package/dist/templates/test/ios/.env.example +18 -0
  47. package/dist/templates/test/ios/README.md +19 -0
  48. package/dist/templates/test/ios/README.zh.md +11 -0
  49. package/dist/templates/test/ios/cases/todo.yaml +26 -0
  50. package/dist/templates/test/ios/gitignore +4 -0
  51. package/dist/templates/test/ios/midscene.config.ts +95 -0
  52. package/dist/templates/test/ios/package.json +20 -0
  53. package/dist/templates/test/ios/tsconfig.json +12 -0
  54. package/dist/templates/test/web/.env.example +14 -0
  55. package/dist/templates/test/web/README.md +19 -0
  56. package/dist/templates/test/web/README.zh.md +13 -0
  57. package/dist/templates/test/web/cases/todo.yaml +62 -0
  58. package/dist/templates/test/web/gitignore +4 -0
  59. package/dist/templates/test/web/midscene.config.ts +56 -0
  60. package/dist/templates/test/web/nodes/todo.ts +215 -0
  61. package/dist/templates/test/web/package.json +21 -0
  62. package/dist/templates/test/web/tsconfig.json +12 -0
  63. package/dist/types/cli/index.d.ts +0 -550
  64. package/dist/types/index.d.ts +0 -20
  65. package/dist/types/midscene/index.d.ts +2 -6
  66. package/package.json +16 -15
  67. package/dist/es/1~214.mjs.map +0 -1
  68. package/dist/lib/0~160.js.map +0 -1
@@ -25,1281 +25,751 @@ var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  DEFAULT_TEST_FILE_SELECTION: ()=>DEFAULT_TEST_FILE_SELECTION,
28
- TestExecutorError: ()=>TestExecutorError,
29
28
  createTestRunId: ()=>createTestRunId,
30
- validateTestFileSelection: ()=>validateTestFileSelection,
31
- defineTestExecutor: ()=>defineTestExecutor,
32
- discoverTestConfig: ()=>discoverTestConfig,
33
29
  runTestProject: ()=>runTestProject,
34
- localTestExecutor: ()=>localTestExecutor,
35
- assertTestCaseTaskRunResult: ()=>assertTestCaseTaskRunResult,
30
+ discoverTestConfig: ()=>discoverTestConfig,
31
+ validateTestFileSelection: ()=>validateTestFileSelection,
36
32
  defineProjectSetup: ()=>defineProjectSetup,
37
- discoverSelectedTestFiles: ()=>discoverSelectedTestFiles,
38
33
  discoverTestFiles: ()=>discoverTestFiles,
39
- defineTestProject: ()=>defineTestProject,
40
- loadTestProject: ()=>loadTestProject
41
- });
42
- const v4_namespaceObject = require("zod/v4");
43
- const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
44
- const json = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
45
- v4_namespaceObject.z["null"](),
46
- v4_namespaceObject.z.string(),
47
- v4_namespaceObject.z.number().finite(),
48
- v4_namespaceObject.z.boolean(),
49
- v4_namespaceObject.z.array(json),
50
- v4_namespaceObject.z.record(v4_namespaceObject.z.string(), json)
51
- ]));
52
- const time = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)));
53
- const executor_runtime_schema_count = v4_namespaceObject.z.number().int().nonnegative();
54
- const executor_runtime_schema_value = v4_namespaceObject.z.strictObject({
55
- value: json,
56
- truncatedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional(),
57
- redactedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional()
58
- });
59
- const reportErrorSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.strictObject({
60
- format: v4_namespaceObject.z.literal('midscene-error').optional(),
61
- name: v4_namespaceObject.z.string(),
62
- message: v4_namespaceObject.z.string(),
63
- code: v4_namespaceObject.z.string().optional(),
64
- details: executor_runtime_schema_value.optional(),
65
- cause: reportErrorSchema.optional(),
66
- errors: v4_namespaceObject.z.array(reportErrorSchema).optional(),
67
- truncated: v4_namespaceObject.z.boolean().optional()
68
- }));
69
- const executor_runtime_schema_operation = v4_namespaceObject.z.strictObject({
70
- operationId: v4_namespaceObject.z.string().min(1),
71
- scopeId: v4_namespaceObject.z.string().optional(),
72
- stepId: v4_namespaceObject.z.string().optional(),
73
- node: v4_namespaceObject.z.string().optional()
74
- });
75
- const cleanupSchema = v4_namespaceObject.z.strictObject({
76
- status: v4_namespaceObject.z["enum"]([
77
- 'not-required',
78
- 'completed',
79
- 'pending',
80
- 'failed'
81
- ]),
82
- capturedAt: time,
83
- operationId: v4_namespaceObject.z.string().optional(),
84
- blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional(),
85
- error: reportErrorSchema.optional()
86
- });
87
- const pendingOperationsSchema = v4_namespaceObject.z.array(executor_runtime_schema_operation);
88
- const collectionStatus = v4_namespaceObject.z["enum"]([
89
- 'complete',
90
- 'partial',
91
- 'unknown'
92
- ]);
93
- const metricsSchema = v4_namespaceObject.z.strictObject({
94
- modelCallCount: executor_runtime_schema_count,
95
- modelTimeMs: v4_namespaceObject.z.number().finite().nonnegative(),
96
- promptTokens: executor_runtime_schema_count,
97
- cachedInputTokens: executor_runtime_schema_count,
98
- completionTokens: executor_runtime_schema_count,
99
- totalTokens: executor_runtime_schema_count,
100
- collection: v4_namespaceObject.z.strictObject({
101
- status: collectionStatus,
102
- expectedExecutionCount: executor_runtime_schema_count,
103
- capturedExecutionCount: executor_runtime_schema_count,
104
- reasons: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
105
- fields: v4_namespaceObject.z.strictObject({
106
- modelCallCount: collectionStatus,
107
- modelTimeMs: collectionStatus,
108
- totalTokens: collectionStatus
109
- }).optional()
110
- }).optional()
111
- });
112
- const operationsSchema = v4_namespaceObject.z.strictObject({
113
- capturedAt: time,
114
- events: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
115
- operationId: v4_namespaceObject.z.string().min(1),
116
- operationKind: v4_namespaceObject.z["enum"]([
117
- 'node',
118
- 'resource',
119
- 'acquisition',
120
- 'cleanup',
121
- 'readiness'
122
- ]),
123
- kind: v4_namespaceObject.z["enum"]([
124
- 'started',
125
- 'cancel-requested',
126
- 'settled',
127
- 'runner-returned',
128
- 'cleanup-deferred'
129
- ]),
130
- at: time,
131
- stepId: v4_namespaceObject.z.string().optional(),
132
- scopeId: v4_namespaceObject.z.string().optional(),
133
- node: v4_namespaceObject.z.string().optional(),
134
- status: v4_namespaceObject.z["enum"]([
135
- 'success',
136
- 'failed',
137
- 'not-run'
138
- ]).optional(),
139
- pending: v4_namespaceObject.z.boolean().optional(),
140
- error: reportErrorSchema.optional(),
141
- blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional()
142
- }))
143
- });
144
- const runtimeResultFields = {
145
- executionStatus: v4_namespaceObject.z["enum"]([
146
- 'success',
147
- 'failed',
148
- 'not-run'
149
- ]).optional(),
150
- phases: v4_namespaceObject.z.partialRecord(v4_namespaceObject.z["enum"]([
151
- 'beforeAll',
152
- 'beforeEach',
153
- 'steps',
154
- 'afterEach',
155
- 'afterAll'
156
- ]), v4_namespaceObject.z.strictObject({
157
- status: v4_namespaceObject.z["enum"]([
158
- 'success',
159
- 'failed',
160
- 'not-run',
161
- 'not-required'
162
- ]),
163
- plannedStepCount: executor_runtime_schema_count,
164
- executedStepCount: executor_runtime_schema_count
165
- })).optional(),
166
- cleanup: cleanupSchema.optional(),
167
- pendingOperations: pendingOperationsSchema.optional(),
168
- retryDecision: v4_namespaceObject.z.strictObject({
169
- scope: v4_namespaceObject.z["enum"]([
170
- 'case',
171
- 'document'
172
- ]),
173
- retryCount: executor_runtime_schema_count,
174
- attemptIndex: executor_runtime_schema_count,
175
- decision: v4_namespaceObject.z["enum"]([
176
- 'retry',
177
- 'stop'
178
- ]),
179
- reason: v4_namespaceObject.z["enum"]([
180
- 'passed',
181
- 'attempt-failed',
182
- 'budget-exhausted',
183
- 'cleanup-pending',
184
- 'cleanup-failed',
185
- 'operation-pending',
186
- 'fatal-error',
187
- 'interrupted',
188
- 'bail',
189
- 'infrastructure-error'
190
- ])
191
- }).optional()
192
- };
193
- const plannedStep = v4_namespaceObject.z.strictObject({
194
- id: v4_namespaceObject.z.string(),
195
- phase: v4_namespaceObject.z["enum"]([
196
- 'beforeAll',
197
- 'beforeEach',
198
- 'steps',
199
- 'afterEach',
200
- 'afterAll'
201
- ]),
202
- stepIndex: executor_runtime_schema_count,
203
- node: v4_namespaceObject.z.string(),
204
- title: v4_namespaceObject.z.string().optional(),
205
- input: executor_runtime_schema_value,
206
- inputMode: v4_namespaceObject.z["enum"]([
207
- 'template',
208
- 'resolved'
209
- ]),
210
- timeoutMs: v4_namespaceObject.z.number().positive().optional(),
211
- continueOnError: v4_namespaceObject.z.boolean()
212
- });
213
- const documentPlanSchema = v4_namespaceObject.z.strictObject({
214
- documentId: v4_namespaceObject.z.string(),
215
- sourcePath: v4_namespaceObject.z.string(),
216
- sourceYaml: executor_runtime_schema_value.optional(),
217
- retry: v4_namespaceObject.z.strictObject({
218
- scope: v4_namespaceObject.z["enum"]([
219
- 'case',
220
- 'document'
221
- ]),
222
- count: executor_runtime_schema_count
223
- }),
224
- beforeAll: v4_namespaceObject.z.array(plannedStep),
225
- afterAll: v4_namespaceObject.z.array(plannedStep),
226
- cases: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
227
- caseId: v4_namespaceObject.z.string(),
228
- name: v4_namespaceObject.z.string(),
229
- caseIndex: executor_runtime_schema_count,
230
- selected: v4_namespaceObject.z.boolean(),
231
- tags: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
232
- onFailure: v4_namespaceObject.z["enum"]([
233
- 'continue',
234
- 'stop-document'
235
- ]).optional(),
236
- beforeEach: v4_namespaceObject.z.array(plannedStep),
237
- steps: v4_namespaceObject.z.array(plannedStep),
238
- afterEach: v4_namespaceObject.z.array(plannedStep)
239
- }))
240
- });
241
- function _define_property(obj, key, value) {
242
- if (key in obj) Object.defineProperty(obj, key, {
243
- value: value,
244
- enumerable: true,
245
- configurable: true,
246
- writable: true
247
- });
248
- else obj[key] = value;
249
- return obj;
250
- }
251
- class TestExecutorError extends Error {
252
- constructor(message, options = {}){
253
- super(message, {
254
- cause: options.cause
255
- }), _define_property(this, "kind", void 0), _define_property(this, "retryable", void 0);
256
- this.name = 'TestExecutorError';
257
- this.kind = options.kind ?? 'unknown';
258
- this.retryable = options.retryable ?? false;
259
- }
260
- }
261
- const defineTestExecutor = (executor)=>executor;
262
- const localTestExecutor = Object.freeze({
263
- name: 'local',
264
- execute: (_task, context)=>context.runLocal()
265
- });
266
- const jsonValueSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
267
- v4_namespaceObject.z["null"](),
268
- v4_namespaceObject.z.string(),
269
- v4_namespaceObject.z.number().finite(),
270
- v4_namespaceObject.z.boolean(),
271
- v4_namespaceObject.z.array(jsonValueSchema),
272
- v4_namespaceObject.z.record(v4_namespaceObject.z.string(), jsonValueSchema)
273
- ]));
274
- const timestampSchema = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
275
- message: 'Expected an ISO-compatible timestamp.'
276
- });
277
- const errorSchema = v4_namespaceObject.z.strictObject({
278
- format: v4_namespaceObject.z.literal('midscene-error').optional(),
279
- cause: reportErrorSchema.optional(),
280
- name: v4_namespaceObject.z.string().min(1),
281
- message: v4_namespaceObject.z.string(),
282
- code: v4_namespaceObject.z.string().min(1),
283
- details: jsonValueSchema.optional()
284
- });
285
- const stepBaseSchema = v4_namespaceObject.z.strictObject({
286
- phase: v4_namespaceObject.z["enum"]([
287
- 'beforeAll',
288
- 'beforeEach',
289
- 'steps',
290
- 'afterEach',
291
- 'afterAll'
292
- ]),
293
- stepIndex: v4_namespaceObject.z.number().int().nonnegative(),
294
- node: v4_namespaceObject.z.string().min(1),
295
- input: jsonValueSchema,
296
- meta: v4_namespaceObject.z.strictObject({
297
- timeoutMs: v4_namespaceObject.z.number().positive().finite().optional(),
298
- continueOnError: v4_namespaceObject.z.boolean()
299
- }),
300
- continuedAfterError: v4_namespaceObject.z.boolean(),
301
- waitForPreviousOperationMs: v4_namespaceObject.z.number().nonnegative().finite().optional(),
302
- operations: operationsSchema.optional(),
303
- output: v4_namespaceObject.z.strictObject({
304
- summary: v4_namespaceObject.z.string().optional(),
305
- data: jsonValueSchema.optional()
306
- }).optional(),
307
- error: errorSchema.optional(),
308
- report: v4_namespaceObject.z.strictObject({
309
- traces: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
310
- type: v4_namespaceObject.z.literal('midscene-execution'),
311
- executionId: v4_namespaceObject.z.string().min(1),
312
- metrics: metricsSchema.optional()
313
- })),
314
- collectionErrors: v4_namespaceObject.z.array(errorSchema).optional()
315
- }).optional()
316
- });
317
- const stepSchema = v4_namespaceObject.z.discriminatedUnion('status', [
318
- stepBaseSchema.extend({
319
- status: v4_namespaceObject.z["enum"]([
320
- 'success',
321
- 'failed'
322
- ]),
323
- startedAt: timestampSchema,
324
- endedAt: timestampSchema,
325
- durationMs: v4_namespaceObject.z.number().nonnegative().finite()
326
- }),
327
- stepBaseSchema.extend({
328
- status: v4_namespaceObject.z.literal('not-run'),
329
- notRunReason: v4_namespaceObject.z.string().min(1),
330
- blockedBy: v4_namespaceObject.z.string().optional()
331
- })
332
- ]);
333
- const validatedStepSchema = stepSchema.superRefine((step, context)=>{
334
- if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
335
- code: 'custom',
336
- message: 'Successful steps cannot contain failure evidence.'
337
- });
338
- if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
339
- code: 'custom',
340
- message: 'Continuation requires continueOnError.'
341
- });
342
- });
343
- const attemptSchema = v4_namespaceObject.z.strictObject({
344
- ...runtimeResultFields,
345
- caseId: v4_namespaceObject.z.string().min(1),
346
- runId: v4_namespaceObject.z.string().min(1),
347
- projectName: v4_namespaceObject.z.string().min(1),
348
- attemptIndex: v4_namespaceObject.z.number().int().nonnegative(),
349
- name: v4_namespaceObject.z.string(),
350
- sourcePath: v4_namespaceObject.z.string().min(1),
351
- caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
352
- status: v4_namespaceObject.z["enum"]([
353
- 'success',
354
- 'failed'
355
- ]),
356
- beforeEach: v4_namespaceObject.z.array(validatedStepSchema),
357
- steps: v4_namespaceObject.z.array(validatedStepSchema),
358
- afterEach: v4_namespaceObject.z.array(validatedStepSchema),
359
- executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
360
- teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
361
- reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional(),
362
- startedAt: timestampSchema,
363
- endedAt: timestampSchema,
364
- durationMs: v4_namespaceObject.z.number().nonnegative().finite()
34
+ loadTestProject: ()=>loadTestProject,
35
+ defineTestProject: ()=>defineTestProject
365
36
  });
366
- const notRunReasonSchema = v4_namespaceObject.z["enum"]([
367
- 'document-start-failed',
368
- 'project-preflight-failed',
369
- 'project-setup-failed',
370
- 'interrupted',
371
- 'bail',
372
- 'fatal-error',
373
- 'executor-failed',
374
- 'cleanup-pending',
375
- 'cleanup-failed',
376
- 'operation-pending'
377
- ]);
378
- const resultSchema = v4_namespaceObject.z.strictObject({
379
- case: v4_namespaceObject.z.strictObject({
380
- documentId: v4_namespaceObject.z.string().min(1),
381
- caseId: v4_namespaceObject.z.string().min(1),
382
- projectName: v4_namespaceObject.z.string().min(1),
383
- name: v4_namespaceObject.z.string(),
384
- sourcePath: v4_namespaceObject.z.string().min(1),
385
- caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
386
- status: v4_namespaceObject.z["enum"]([
387
- 'success',
388
- 'failed',
389
- 'not-run'
390
- ]),
391
- run: attemptSchema.optional(),
392
- attempts: v4_namespaceObject.z.array(attemptSchema).optional(),
393
- notRunReason: notRunReasonSchema.optional()
394
- }),
395
- document: v4_namespaceObject.z.strictObject({
396
- ...runtimeResultFields,
397
- plan: documentPlanSchema.optional(),
398
- attemptIndex: v4_namespaceObject.z.number().int().nonnegative().optional(),
399
- hostErrors: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
400
- phase: v4_namespaceObject.z["enum"]([
401
- 'setup',
402
- 'execution',
403
- 'cleanup',
404
- 'report',
405
- 'publication',
406
- 'observer'
407
- ]),
408
- error: reportErrorSchema
409
- })).optional(),
410
- documentId: v4_namespaceObject.z.string().min(1),
411
- documentRunId: v4_namespaceObject.z.string().min(1),
412
- projectId: v4_namespaceObject.z.string().min(1),
413
- projectName: v4_namespaceObject.z.string().min(1),
414
- sourcePath: v4_namespaceObject.z.string().min(1),
415
- status: v4_namespaceObject.z["enum"]([
416
- 'success',
417
- 'failed'
418
- ]),
419
- startedAt: timestampSchema,
420
- endedAt: timestampSchema,
421
- durationMs: v4_namespaceObject.z.number().nonnegative().finite(),
422
- beforeAll: v4_namespaceObject.z.array(validatedStepSchema),
423
- afterAll: v4_namespaceObject.z.array(validatedStepSchema),
424
- executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
425
- teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
426
- reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional()
427
- }).optional(),
428
- lifecycle: v4_namespaceObject.z.strictObject({
429
- cleanup: cleanupSchema.optional(),
430
- pendingOperations: pendingOperationsSchema.optional(),
431
- projectName: v4_namespaceObject.z.string().min(1),
432
- status: v4_namespaceObject.z["enum"]([
433
- 'success',
434
- 'failed'
435
- ]),
436
- setupError: errorSchema.optional(),
437
- teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
438
- startedAt: timestampSchema,
439
- endedAt: timestampSchema,
440
- durationMs: v4_namespaceObject.z.number().nonnegative().finite()
441
- }).optional(),
442
- artifacts: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
443
- name: v4_namespaceObject.z.string().min(1),
444
- uri: v4_namespaceObject.z.string().min(1),
445
- mediaType: v4_namespaceObject.z.string().min(1).optional()
446
- })).optional(),
447
- metadata: v4_namespaceObject.z.record(v4_namespaceObject.z.string(), v4_namespaceObject.z.union([
448
- v4_namespaceObject.z.string(),
449
- v4_namespaceObject.z.number().finite(),
450
- v4_namespaceObject.z.boolean(),
451
- v4_namespaceObject.z["null"]()
452
- ])).optional()
453
- });
454
- const attemptMatchesTask = (task, attempt)=>attempt.caseId === task.caseId && attempt.projectName === task.projectName && attempt.name === task.caseName && attempt.sourcePath === task.sourcePath && attempt.caseIndex === task.caseIndex && isSafeResultPathSegment(attempt.runId);
455
- const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
456
- 'completed',
457
- 'not-required'
458
- ].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
459
- function assertTestCaseTaskRunResult(task, value) {
460
- const parsed = resultSchema.safeParse(value);
461
- if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${v4_namespaceObject.z.prettifyError(parsed.error)}`);
462
- const outcome = parsed.data.case;
463
- if (outcome.caseId !== task.caseId || outcome.documentId !== task.documentId || outcome.projectName !== task.projectName || outcome.name !== task.caseName || outcome.sourcePath !== task.sourcePath || outcome.caseIndex !== task.caseIndex) throw new Error(`Executor returned a result that does not match task ${task.caseId}.`);
464
- const attempts = outcome.attempts ?? [];
465
- for (const attempt of attempts){
466
- const steps = [
467
- ...attempt.beforeEach,
468
- ...attempt.steps,
469
- ...attempt.afterEach
470
- ];
471
- if ('success' === attempt.status && (0 === attempt.steps.length || (attempt.executionErrors?.length ?? 0) > 0 || steps.some((step)=>'success' !== step.status) || hasUnfinishedWork(attempt) || (attempt.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful attempt for ${task.caseId}.`);
472
- }
473
- if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
474
- if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
475
- if (attempts.some((attempt, index)=>attempt.attemptIndex !== index) || attempts.length > task.retry + 1) throw new TypeError(`Executor returned a non-sequential attempt history for ${task.caseId}.`);
476
- const finalAttempt = attempts.at(-1);
477
- if ('not-run' === outcome.status) {
478
- if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
479
- } else if (void 0 !== outcome.notRunReason || !outcome.run || !finalAttempt || finalAttempt.status !== outcome.status || outcome.run.runId !== finalAttempt.runId || JSON.stringify(outcome.run) !== JSON.stringify(finalAttempt) || attempts.slice(0, -1).some((attempt)=>'failed' !== attempt.status)) throw new TypeError(`Executor returned an inconsistent ${outcome.status} result for ${task.caseId}.`);
480
- const reportRefs = [
481
- ...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
482
- ...parsed.data.document?.reportRefs ?? []
483
- ];
484
- if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
485
- const document = parsed.data.document;
486
- if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
487
- ...document.beforeAll,
488
- ...document.afterAll
489
- ].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
490
- const lifecycle = parsed.data.lifecycle;
491
- if (lifecycle?.status === 'success' && (hasUnfinishedWork(lifecycle) || lifecycle.setupError || (lifecycle.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful lifecycle for ${task.caseId}.`);
492
- if (document && (document.documentId !== task.documentId || document.projectId !== task.projectId || document.projectName !== task.projectName || document.sourcePath !== task.sourcePath || !isSafeResultPathSegment(document.documentRunId))) throw new Error(`Executor returned a document that does not match task ${task.caseId}.`);
493
- if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
494
- }
495
- const external_node_fs_namespaceObject = require("node:fs");
496
37
  const external_node_path_namespaceObject = require("node:path");
38
+ const external_node_url_namespaceObject = require("node:url");
39
+ const api_namespaceObject = require("tsx/cjs/api");
497
40
  const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
498
- const external_tinyglobby_namespaceObject = require("tinyglobby");
499
- const external_node_crypto_namespaceObject = require("node:crypto");
500
- const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
501
- projectId,
502
- sourcePath,
503
- caseIndex,
504
- ...invocationIndex ? [
505
- invocationIndex
506
- ] : []
507
- ])).digest('hex');
508
- const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
509
- projectId,
510
- sourcePath,
511
- ...invocationIndex ? [
512
- invocationIndex
513
- ] : []
514
- ])).digest('hex');
515
- const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
516
- const external_js_yaml_namespaceObject = require("js-yaml");
517
- function parseWorkflowSource(source) {
518
- const stack = [];
519
- const nodes = [];
520
- const sensitiveNodes = new Set();
521
- const sensitiveValues = new Set();
522
- const redactedPaths = new Set();
523
- const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
524
- const parsed = (0, external_js_yaml_namespaceObject.load)(source, {
525
- schema: external_js_yaml_namespaceObject.JSON_SCHEMA,
526
- listener (event, state) {
527
- if ('open' === event) return void stack.push({
528
- start: state.position + sourceOffset,
529
- end: 0,
530
- value: null,
531
- children: []
532
- });
533
- const node = stack.pop();
534
- node.end = state.position + sourceOffset;
535
- node.value = state.result;
536
- stack.at(-1)?.children.push(node);
537
- nodes.push(node);
538
- if ('mapping' !== state.kind) return;
539
- for(let index = 0; index < node.children.length; index += 2){
540
- const key = node.children[index].value;
541
- const value = node.children[index + 1];
542
- if ('string' == typeof key && (0, test_runner_namespaceObject.isReportSecretKey)(key) && value) {
543
- sensitiveNodes.add(value);
544
- if (null !== value.value) sensitiveValues.add(value.value);
545
- redactedPaths.add(`$..[${JSON.stringify(key)}]`);
546
- }
547
- }
41
+ const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
42
+ const projectIdFromIndex = (index)=>`project-${index}`;
43
+ const defineTestProject = (definition)=>definition;
44
+ const defineProjectSetup = (definition)=>definition;
45
+ const getDefaultExport = (loaded, absolutePath)=>{
46
+ if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
47
+ const defaultExport = loaded.default;
48
+ if ('object' == typeof defaultExport && null !== defaultExport && '__esModule' in defaultExport) {
49
+ if ('default' in defaultExport) return defaultExport.default;
50
+ throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
548
51
  }
549
- });
550
- const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
551
- let offset = 0;
552
- let redacted = '';
553
- for (const range of ranges){
554
- if (range.start < offset) continue;
555
- const text = source.slice(range.start, range.end);
556
- const leading = text.match(/^\s*/)?.[0] ?? '';
557
- const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
558
- redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
559
- offset = range.end;
52
+ return defaultExport;
560
53
  }
561
- redacted += source.slice(offset);
562
- const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
563
- return {
564
- parsed,
565
- sourceYaml: {
566
- value,
567
- ...redactedPaths.size ? {
568
- redactedPaths: [
569
- ...redactedPaths
570
- ]
571
- } : {},
572
- ...value !== redacted ? {
573
- truncatedPaths: [
574
- '$'
575
- ]
576
- } : {}
577
- }
578
- };
579
- }
580
- const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
581
- const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
582
- const exactProjectExpression = /^\$\{([^{}]+)\}$/;
583
- const missingVariable = (kind, name, options)=>{
584
- throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
585
- ...options.location,
586
- variable: name,
587
- variableKind: kind
588
- });
589
- };
590
- const projectVariable = (name, options)=>{
591
- if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
592
- return options.variables[name];
54
+ throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
593
55
  };
594
- const environmentVariable = (name, options)=>{
595
- const value = options.env?.[name];
596
- if (void 0 === value) return missingVariable('environment', name, options);
597
- return value;
56
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
57
+ const rejectUnknownKeys = (value, allowedKeys, label)=>{
58
+ const allowed = new Set(allowedKeys);
59
+ const unknown = Object.keys(value).find((key)=>!allowed.has(key));
60
+ if (unknown) throw new TypeError(`Midscene config ${label}.${unknown} is not supported.`);
598
61
  };
599
- const interpolationValue = (name, value, options)=>{
600
- if ('object' == typeof value && null !== value) throw new test_runner_namespaceObject.WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
601
- ...options.location,
602
- variable: name,
603
- variableKind: 'project'
62
+ const isAbsolutePattern = (pattern)=>(0, external_node_path_namespaceObject.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith('\\\\');
63
+ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = false)=>{
64
+ if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
65
+ if ('include' === field && 0 === value.length && !allowEmptyInclude) throw new TypeError(`Midscene config ${label}.include must be a non-empty array.`);
66
+ return value.map((pattern, index)=>{
67
+ if ('string' != typeof pattern || 0 === pattern.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
68
+ if (isAbsolutePattern(pattern)) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be relative to the project root.`);
69
+ if (pattern.split(/[\\/]/).includes('..')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must not contain a ".." path segment.`);
70
+ if (pattern.includes('\\')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must use POSIX path separators (/).`);
71
+ if (pattern.startsWith('!')) {
72
+ if ('include' === field) throw new TypeError(`Midscene config ${label}.include[${index}] must not be a negated pattern. Use files.exclude instead.`);
73
+ throw new TypeError(`Midscene config ${label}.exclude[${index}] must not be a negated pattern.`);
74
+ }
75
+ return pattern;
604
76
  });
605
- return String(value);
606
77
  };
607
- const resolveString = (value, options)=>{
608
- const exactEnv = exactEnvExpression.exec(value);
609
- if (exactEnv) return environmentVariable(exactEnv[1], options);
610
- const exactProject = exactProjectExpression.exec(value);
611
- if (exactProject) return projectVariable(exactProject[1], options);
612
- return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
78
+ const validateTestFileSelection = (value, label = 'files')=>{
79
+ if (void 0 === value) return;
80
+ if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
81
+ rejectUnknownKeys(value, [
82
+ 'include',
83
+ 'exclude'
84
+ ], label);
85
+ const include = validatePatterns(value.include, 'include', label);
86
+ const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
87
+ return Object.freeze({
88
+ include: Object.freeze(include),
89
+ ...exclude ? {
90
+ exclude: Object.freeze(exclude)
91
+ } : {}
92
+ });
613
93
  };
614
- const resolveWorkflowVariables = (value, options)=>{
615
- if ('string' == typeof value) return resolveString(value, options);
616
- if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
617
- if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
618
- key,
619
- resolveWorkflowVariables(child, options)
620
- ]));
621
- return value;
94
+ const validateTagList = (value, field, label)=>{
95
+ if (void 0 === value) return [];
96
+ if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
97
+ return value.map((tag, index)=>{
98
+ if ('string' != typeof tag || 0 === tag.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
99
+ return tag;
100
+ });
622
101
  };
623
- function collect_define_property(obj, key, value) {
624
- if (key in obj) Object.defineProperty(obj, key, {
625
- value: value,
626
- enumerable: true,
627
- configurable: true,
628
- writable: true
102
+ const validateTagSelection = (value, label)=>{
103
+ if (void 0 === value) return Object.freeze({
104
+ include: [],
105
+ exclude: []
629
106
  });
630
- else obj[key] = value;
631
- return obj;
632
- }
633
- const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
634
- class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
635
- constructor(actualFormat, sourcePath){
636
- 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.`, {
637
- sourcePath,
638
- actualFormat,
639
- expectedFormat: 'native'
640
- }), collect_define_property(this, "actualFormat", void 0);
641
- this.actualFormat = actualFormat;
642
- }
643
- }
644
- const rejectUnknownKeys = (value, allowed, label)=>{
645
- const allowedSet = new Set(allowed);
646
- const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
647
- if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
648
- field: unknown[0]
107
+ if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
108
+ return Object.freeze({
109
+ include: Object.freeze(validateTagList(value.include, 'include', label)),
110
+ exclude: Object.freeze(validateTagList(value.exclude, 'exclude', label))
649
111
  });
650
112
  };
651
- const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
652
- const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
653
- function collectWorkflowDocument(source, options) {
654
- let snapshot;
655
- try {
656
- snapshot = parseWorkflowSource((0, external_node_fs_namespaceObject.readFileSync)(source.absolutePath, 'utf8'));
657
- } catch (error) {
658
- throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
659
- sourcePath: source.sourcePath
660
- }, error);
113
+ function assertJsonValue(value, path, seen) {
114
+ if (null === value || 'string' == typeof value || 'boolean' == typeof value) return;
115
+ if ('number' == typeof value) {
116
+ if (Number.isFinite(value)) return;
117
+ throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
661
118
  }
662
- const { parsed, sourceYaml } = snapshot;
663
- if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
664
- const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
665
- if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
666
- rejectUnknownKeys(parsed, [
667
- 'beforeAll',
668
- 'beforeEach',
669
- 'cases',
670
- 'afterEach',
671
- 'afterAll'
672
- ], 'Workflow document');
673
- if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
674
- const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
675
- const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
676
- ...step,
677
- templateInput: step.input,
678
- input: resolveWorkflowVariables(step.input, {
679
- variables: options.variables,
680
- env: options.env ?? process.env,
681
- location: {
682
- projectName: source.projectName ?? source.projectId,
683
- sourcePath,
684
- phase,
685
- stepIndex,
686
- ...void 0 === caseIndex ? {} : {
687
- caseIndex
688
- }
689
- }
690
- })
691
- });
692
- const normalizeLifecycle = (phase)=>{
693
- const value = parsed[phase];
694
- if (void 0 === value) return [];
695
- if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
696
- phase
697
- });
698
- return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
699
- const resolved = options.resolveNode(normalized.node);
700
- if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
701
- phase,
702
- stepIndex,
703
- node: normalized.node
704
- });
705
- return resolveStepVariables(normalized, phase, stepIndex);
706
- });
707
- };
708
- const lifecycle = {
709
- beforeAll: normalizeLifecycle('beforeAll'),
710
- beforeEach: normalizeLifecycle('beforeEach'),
711
- afterEach: normalizeLifecycle('afterEach'),
712
- afterAll: normalizeLifecycle('afterAll')
713
- };
714
- const ids = new Set();
715
- const cases = parsed.cases.map((definition, caseIndex)=>{
716
- if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
717
- caseIndex
718
- });
719
- rejectUnknownKeys(definition, [
720
- 'id',
119
+ if ('object' != typeof value || void 0 === value) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
120
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
121
+ if (seen.has(value)) throw new TypeError(`Midscene config ${path} must not contain cycles.`);
122
+ seen.add(value);
123
+ if (Array.isArray(value)) value.forEach((item, index)=>assertJsonValue(item, `${path}[${index}]`, seen));
124
+ else for (const [key, child] of Object.entries(value))assertJsonValue(child, `${path}.${key}`, seen);
125
+ seen.delete(value);
126
+ }
127
+ const deepFreezeJson = (value)=>{
128
+ if ('object' == typeof value && null !== value) {
129
+ for (const child of Object.values(value))deepFreezeJson(child);
130
+ Object.freeze(value);
131
+ }
132
+ return value;
133
+ };
134
+ const validateVariables = (value, label)=>{
135
+ if (void 0 === value) return Object.freeze({});
136
+ if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
137
+ assertJsonValue(value, label, new Set());
138
+ return deepFreezeJson(value);
139
+ };
140
+ const validateSetup = (value, label)=>{
141
+ if (void 0 === value) return;
142
+ if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
143
+ rejectUnknownKeys(value, [
144
+ 'name',
145
+ 'setup'
146
+ ], label);
147
+ if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
148
+ if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
149
+ return value;
150
+ };
151
+ const validatePositiveInteger = (value, fallback, label)=>{
152
+ if (void 0 === value) return fallback;
153
+ if (!Number.isInteger(value) || value <= 0) throw new TypeError(`Midscene config ${label} must be a positive integer.`);
154
+ return value;
155
+ };
156
+ const validateNonNegativeInteger = (value, fallback, label)=>{
157
+ if (void 0 === value) return fallback;
158
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
159
+ return value;
160
+ };
161
+ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
162
+ if (void 0 === value) {
163
+ const setup = validateSetup(defaultSetup, 'setup');
164
+ return {
165
+ hasExplicitProjects: false,
166
+ projects: Object.freeze([
167
+ Object.freeze({
168
+ projectId: projectIdFromIndex(0),
169
+ name: 'default',
170
+ ...setup ? {
171
+ setup
172
+ } : {},
173
+ tags: Object.freeze({
174
+ include: [],
175
+ exclude: []
176
+ }),
177
+ retry: 0,
178
+ variables: Object.freeze({}),
179
+ nodes: new NodeRegistry(globalNodes.definitions())
180
+ })
181
+ ])
182
+ };
183
+ }
184
+ if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
185
+ if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
186
+ const names = new Set();
187
+ const projects = value.map((candidate, index)=>{
188
+ const label = `projects[${index}]`;
189
+ if (!isRecord(candidate)) throw new TypeError(`Midscene config ${label} must be an object.`);
190
+ rejectUnknownKeys(candidate, [
721
191
  'name',
192
+ 'setup',
193
+ 'nodes',
194
+ 'files',
722
195
  'tags',
723
- 'resources',
724
- 'steps'
725
- ], `Case ${caseIndex + 1}`);
726
- if (void 0 !== definition.id && !isSafeResultPathSegment(definition.id)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} id must start with an ASCII letter or number, contain only letters, numbers, dots, underscores, or hyphens, and be at most 128 characters.`, {
727
- caseIndex
728
- });
729
- 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.`, {
730
- caseIndex
731
- });
732
- 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.`, {
733
- caseIndex
734
- });
735
- const tags = definition.tags ?? [];
736
- 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.`, {
737
- caseIndex
738
- });
739
- const resources = definition.resources ?? [];
740
- if (!Array.isArray(resources) || resources.some((resource)=>'string' != typeof resource || 0 === resource.trim().length)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} resources must be an array of non-empty strings.`, {
741
- caseIndex
742
- });
743
- if (new Set(resources).size !== resources.length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} resources must not contain duplicates.`, {
744
- caseIndex
745
- });
746
- const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
747
- if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
748
- caseIndex,
749
- stepIndex,
750
- node: normalized.node
751
- });
752
- return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
753
- });
754
- const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
755
- if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
756
- caseId
757
- });
758
- ids.add(caseId);
759
- return {
760
- caseId,
761
- projectId: source.projectId,
762
- sourcePath,
763
- caseIndex,
764
- definition: {
765
- ...'string' == typeof definition.id ? {
766
- id: definition.id
767
- } : {},
768
- name: definition.name,
769
- tags: tags,
770
- ...resources.length > 0 ? {
771
- resources: resources
772
- } : {},
773
- steps
196
+ 'retry',
197
+ 'variables'
198
+ ], label);
199
+ if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
200
+ if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
201
+ names.add(candidate.name);
202
+ const files = validateTestFileSelection(candidate.files, `${label}.files`);
203
+ if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
204
+ const localNodes = new NodeRegistry(candidate.nodes);
205
+ const nodes = new NodeRegistry([
206
+ ...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
207
+ ...localNodes.definitions()
208
+ ]);
209
+ return Object.freeze({
210
+ projectId: projectIdFromIndex(index),
211
+ name: candidate.name,
212
+ nodes,
213
+ ...files ? {
214
+ files
215
+ } : {},
216
+ tags: validateTagSelection(candidate.tags, `${label}.tags`),
217
+ retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
218
+ variables: validateVariables(candidate.variables, `${label}.variables`),
219
+ ...void 0 === candidate.setup ? {} : {
220
+ setup: validateSetup(candidate.setup, `${label}.setup`)
774
221
  }
775
- };
222
+ });
776
223
  });
777
224
  return {
778
- documentId: createWorkflowDocumentId(source.projectId, sourcePath),
779
- projectId: source.projectId,
780
- sourcePath,
781
- sourceYaml,
782
- lifecycle,
783
- cases
225
+ projects: Object.freeze(projects),
226
+ hasExplicitProjects: true
784
227
  };
785
- }
786
- const promises_namespaceObject = require("node:fs/promises");
787
- const writeJson = async (path, value)=>{
788
- try {
789
- await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(path), {
790
- recursive: true
791
- });
792
- await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
793
- } catch (error) {
794
- throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
795
- }
796
228
  };
797
- const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, 'plan.json'), {
798
- schemaVersion: 1,
799
- createdAt: new Date().toISOString(),
800
- projects: projects.map((item)=>({
801
- projectId: item.project.projectId,
802
- name: item.project.name,
803
- documents: item.plans ?? []
804
- }))
229
+ const validateTestOptions = (value)=>{
230
+ if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
231
+ const candidate = value ?? {};
232
+ return Object.freeze({
233
+ maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
234
+ bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
235
+ testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
805
236
  });
806
- const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
807
- const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
808
- const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
809
- const collectionErrorPath = (projectId, sourcePath)=>{
810
- const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
811
- projectId,
812
- sourcePath
813
- ])).digest('hex');
814
- return toPosix((0, external_node_path_namespaceObject.join)(projectId, 'collection-errors', `${id}.json`));
815
237
  };
816
- const writeWorkflowDocumentResult = (runDir, result)=>{
817
- const { documentRunId: _documentRunId, ...persistedResult } = result;
818
- return writeJson((0, external_node_path_namespaceObject.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
819
- };
820
- const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
821
- const { runId: attemptId, ...persistedResult } = result;
822
- return writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
823
- ...persistedResult,
824
- attemptId
825
- });
238
+ const validateOutputPath = (value, fallback, label)=>{
239
+ if (void 0 === value) return fallback;
240
+ if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
241
+ return value;
826
242
  };
827
- const writeCollectionError = (runDir, collectionError)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
828
- kind: 'collection-error',
829
- ...collectionError
243
+ const validateOutput = (value)=>{
244
+ if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config output must be an object.');
245
+ const candidate = value ?? {};
246
+ rejectUnknownKeys(candidate, [
247
+ 'reportDir'
248
+ ], 'output');
249
+ return Object.freeze({
250
+ reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
830
251
  });
831
- const relativeToSummary = (summaryPath, target)=>{
832
- const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
833
- return toPosix(value || '.');
834
252
  };
835
- const errorJson = test_runner_namespaceObject.sanitizeReportError;
836
- const writeTestProjectRunResult = (options)=>{
837
- const { result } = options;
838
- const fact = (path)=>toPosix(path);
839
- return writeJson(result.summaryPath, {
840
- schemaVersion: result.schemaVersion,
841
- runId: result.runId,
842
- startedAt: result.startedAt,
843
- endedAt: result.endedAt,
844
- durationMs: result.durationMs,
845
- status: result.status,
846
- exitCode: result.exitCode,
847
- ...result.errors?.length ? {
848
- errors: result.errors.map(errorJson)
849
- } : {},
850
- projectRoot: options.projectRoot,
851
- ...options.configPath ? {
852
- configPath: options.configPath
853
- } : {},
854
- factsRoot: '.',
855
- plan: 'plan.json',
856
- ...result.operationLog ? {
857
- operationLog: {
858
- ...result.operationLog,
859
- path: relativeToSummary(result.summaryPath, result.operationLog.path)
860
- }
861
- } : {},
862
- reportDir: relativeToSummary(result.summaryPath, result.reportDir),
863
- ...result.reportPath ? {
864
- report: relativeToSummary(result.summaryPath, result.reportPath)
865
- } : {},
866
- ...result.markdownPath ? {
867
- markdown: relativeToSummary(result.summaryPath, result.markdownPath)
868
- } : {},
869
- summary: result.summary,
870
- projects: result.projects.map((project)=>({
871
- projectId: project.projectId,
872
- name: project.name,
873
- status: project.status,
874
- retry: project.retry,
875
- fileSelection: project.fileSelection,
876
- tagSelection: project.tagSelection,
877
- sourceCount: project.sourceCount,
878
- selectedCaseCount: project.selectedCaseCount,
879
- filteredCaseCount: project.filteredCaseCount,
880
- ...project.lifecycle ? {
881
- lifecycle: {
882
- status: project.lifecycle.status,
883
- startedAt: project.lifecycle.startedAt,
884
- endedAt: project.lifecycle.endedAt,
885
- durationMs: project.lifecycle.durationMs,
886
- ...project.lifecycle.cleanup ? {
887
- cleanup: project.lifecycle.cleanup
888
- } : {},
889
- ...project.lifecycle.pendingOperations ? {
890
- pendingOperations: project.lifecycle.pendingOperations
891
- } : {},
892
- ...project.lifecycle.setupError ? {
893
- setupError: errorJson(project.lifecycle.setupError)
894
- } : {},
895
- ...project.lifecycle.teardownErrors ? {
896
- teardownErrors: project.lifecycle.teardownErrors.map(errorJson)
897
- } : {}
898
- }
899
- } : {},
900
- cases: project.cases.map((outcome)=>({
901
- ...outcome.documentRunId ? {
902
- documentRunId: outcome.documentRunId
903
- } : {},
904
- documentId: outcome.documentId,
905
- caseId: outcome.caseId,
906
- sourcePath: outcome.sourcePath,
907
- caseIndex: outcome.caseIndex,
908
- name: outcome.name,
909
- status: outcome.status,
910
- ...outcome.notRunReason ? {
911
- notRunReason: outcome.notRunReason
912
- } : {},
913
- ...outcome.execution ? {
914
- execution: {
915
- executor: outcome.execution.executor,
916
- resources: outcome.execution.resources,
917
- ...void 0 === outcome.execution.attempts ? {} : {
918
- attempts: outcome.execution.attempts
919
- },
920
- ...outcome.execution.failure ? {
921
- failure: outcome.execution.failure
922
- } : {},
923
- ...outcome.execution.artifacts ? {
924
- artifacts: outcome.execution.artifacts
925
- } : {},
926
- ...outcome.execution.metadata ? {
927
- metadata: outcome.execution.metadata
928
- } : {},
929
- ...outcome.execution.lifecycle ? {
930
- lifecycle: {
931
- status: outcome.execution.lifecycle.status,
932
- startedAt: outcome.execution.lifecycle.startedAt,
933
- endedAt: outcome.execution.lifecycle.endedAt,
934
- durationMs: outcome.execution.lifecycle.durationMs,
935
- ...outcome.execution.lifecycle.setupError ? {
936
- setupError: errorJson(outcome.execution.lifecycle.setupError)
937
- } : {},
938
- ...outcome.execution.lifecycle.teardownErrors ? {
939
- teardownErrors: outcome.execution.lifecycle.teardownErrors.map(errorJson)
940
- } : {}
941
- }
942
- } : {}
943
- }
944
- } : {},
945
- ...outcome.attempts ? {
946
- attempts: outcome.attempts.map((attempt)=>({
947
- attemptId: attempt.runId,
948
- attemptIndex: attempt.attemptIndex,
949
- status: attempt.status,
950
- ...attempt.cleanup ? {
951
- cleanup: attempt.cleanup
952
- } : {},
953
- ...attempt.pendingOperations ? {
954
- pendingOperations: attempt.pendingOperations
955
- } : {},
956
- ...attempt.executionStatus ? {
957
- executionStatus: attempt.executionStatus
958
- } : {},
959
- ...attempt.retryDecision ? {
960
- retryDecision: attempt.retryDecision
961
- } : {},
962
- resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
963
- ...attempt.reportPaths?.length ? {
964
- reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
965
- } : {}
966
- }))
967
- } : {}
968
- })),
969
- documents: project.documents.map((document)=>({
970
- documentId: document.documentId,
971
- ...void 0 === document.attemptIndex ? {} : {
972
- attemptIndex: document.attemptIndex,
973
- documentRunId: document.documentRunId
974
- },
975
- sourcePath: document.sourcePath,
976
- status: document.status,
977
- ...document.cleanup ? {
978
- cleanup: document.cleanup
979
- } : {},
980
- ...document.pendingOperations ? {
981
- pendingOperations: document.pendingOperations
982
- } : {},
983
- ...document.executionStatus ? {
984
- executionStatus: document.executionStatus
985
- } : {},
986
- ...document.retryDecision ? {
987
- retryDecision: document.retryDecision
988
- } : {},
989
- resultFile: fact(workflowDocumentResultPath(document)),
990
- ...document.reportPaths?.length ? {
991
- reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
992
- } : {}
993
- })),
994
- collectionErrors: project.collectionErrors.map((error)=>({
995
- sourcePath: error.sourcePath,
996
- errorFile: fact(collectionErrorPath(error.projectId, error.sourcePath))
997
- }))
998
- }))
999
- });
253
+ const validateTestProjectDefinition = (definition)=>{
254
+ if (!isRecord(definition)) throw new TypeError('Midscene config must default export an object.');
255
+ if (void 0 !== definition.nodes && !Array.isArray(definition.nodes)) throw new TypeError('Midscene config nodes must be an array.');
256
+ if ('setupWorkflow' in definition) throw new TypeError('Midscene config setupWorkflow is no longer supported. Use setup or projects[].setup instead.');
257
+ if ('setupDocument' in definition) throw new TypeError('Midscene config setupDocument is not supported. Use setup or projects[].setup for resources, and beforeAll/afterAll for document lifecycle steps.');
258
+ if ('root' in definition) throw new TypeError('Midscene config root is not supported. Pass the Test Project directory to midscene-test instead.');
259
+ if ('files' in definition) throw new TypeError('Midscene config files is not supported at the root. Move it to projects[].files.');
260
+ if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
261
+ rejectUnknownKeys(definition, [
262
+ 'setup',
263
+ 'projects',
264
+ 'test',
265
+ 'output',
266
+ 'nodes'
267
+ ], 'root');
268
+ const nodes = new NodeRegistry(definition.nodes);
269
+ const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
270
+ return {
271
+ ...resolvedProjects,
272
+ test: validateTestOptions(definition.test),
273
+ output: validateOutput(definition.output),
274
+ nodes,
275
+ resolveNode: (name)=>nodes.get(name)
276
+ };
1000
277
  };
1001
- const external_node_url_namespaceObject = require("node:url");
1002
- const api_namespaceObject = require("tsx/cjs/api");
1003
- const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
1004
- const projectIdFromIndex = (index)=>`project-${index}`;
1005
- const defineTestProject = (definition)=>definition;
1006
- const defineProjectSetup = (definition)=>definition;
1007
- const getDefaultExport = (loaded, absolutePath)=>{
1008
- if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
1009
- const defaultExport = loaded.default;
1010
- if ('object' == typeof defaultExport && null !== defaultExport && '__esModule' in defaultExport) {
1011
- if ('default' in defaultExport) return defaultExport.default;
1012
- throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
1013
- }
1014
- return defaultExport;
278
+ const assertSupportedConfig = (absolutePath)=>{
279
+ if (!absolutePath.endsWith('.ts') && !absolutePath.endsWith('.mjs')) {
280
+ const extension = absolutePath.match(/(\.[^./\\]+)$/)?.[1] ?? '(none)';
281
+ throw new TypeError(`Unsupported Midscene config extension: ${extension}. Supported extensions: .ts, .mjs.`);
1015
282
  }
1016
- throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
283
+ return absolutePath.endsWith('.mjs') ? '.mjs' : '.ts';
1017
284
  };
1018
- const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
1019
- const test_project_rejectUnknownKeys = (value, allowedKeys, label)=>{
1020
- const allowed = new Set(allowedKeys);
1021
- const unknown = Object.keys(value).find((key)=>!allowed.has(key));
1022
- if (unknown) throw new TypeError(`Midscene config ${label}.${unknown} is not supported.`);
285
+ const canRetryWithCjsLoader = (error)=>{
286
+ if (!(error instanceof Error)) return false;
287
+ const errorCode = error.code;
288
+ return error instanceof SyntaxError && /^Unexpected (?:identifier|reserved word|token)/.test(error.message) || 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' === errorCode || 'ERR_UNKNOWN_FILE_EXTENSION' === errorCode && /^Unknown file extension\b/.test(error.message);
1023
289
  };
1024
- const isAbsolutePattern = (pattern)=>(0, external_node_path_namespaceObject.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith('\\\\');
1025
- const validatePatterns = (value, field, label = 'files', allowEmptyInclude = false)=>{
1026
- if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
1027
- if ('include' === field && 0 === value.length && !allowEmptyInclude) throw new TypeError(`Midscene config ${label}.include must be a non-empty array.`);
1028
- return value.map((pattern, index)=>{
1029
- if ('string' != typeof pattern || 0 === pattern.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
1030
- if (isAbsolutePattern(pattern)) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be relative to the project root.`);
1031
- if (pattern.split(/[\\/]/).includes('..')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must not contain a ".." path segment.`);
1032
- if (pattern.includes('\\')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must use POSIX path separators (/).`);
1033
- if (pattern.startsWith('!')) {
1034
- if ('include' === field) throw new TypeError(`Midscene config ${label}.include[${index}] must not be a negated pattern. Use files.exclude instead.`);
1035
- throw new TypeError(`Midscene config ${label}.exclude[${index}] must not be a negated pattern.`);
1036
- }
1037
- return pattern;
290
+ async function loadTestProject(configPath) {
291
+ if (!configPath) return validateTestProjectDefinition({
292
+ nodes: []
1038
293
  });
1039
- };
1040
- const validateTestFileSelection = (value, label = 'files')=>{
1041
- if (void 0 === value) return;
1042
- if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
1043
- test_project_rejectUnknownKeys(value, [
1044
- 'include',
1045
- 'exclude'
1046
- ], label);
1047
- const include = validatePatterns(value.include, 'include', label);
1048
- const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
1049
- return Object.freeze({
1050
- include: Object.freeze(include),
1051
- ...exclude ? {
1052
- exclude: Object.freeze(exclude)
1053
- } : {}
294
+ const absolutePath = (0, external_node_path_namespaceObject.resolve)(configPath);
295
+ const extension = assertSupportedConfig(absolutePath);
296
+ let loaded;
297
+ try {
298
+ if ('.mjs' === extension) loaded = await import((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href);
299
+ else {
300
+ const { tsImport } = await import("tsx/esm/api");
301
+ loaded = await tsImport((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href, {
302
+ parentURL: (0, external_node_url_namespaceObject.pathToFileURL)(`${(0, external_node_path_namespaceObject.dirname)(absolutePath)}${external_node_path_namespaceObject.sep}`).href,
303
+ tsconfig: false
304
+ });
305
+ }
306
+ } catch (error) {
307
+ try {
308
+ if ('.mjs' === extension || !canRetryWithCjsLoader(error)) throw error;
309
+ loaded = (0, api_namespaceObject.require)(absolutePath, (0, external_node_url_namespaceObject.pathToFileURL)(absolutePath));
310
+ } catch (fallbackError) {
311
+ const message = fallbackError instanceof Error ? `: ${fallbackError.message}` : '';
312
+ throw new Error(`Failed to load Midscene config "${absolutePath}"${message}`, {
313
+ cause: fallbackError
314
+ });
315
+ }
316
+ }
317
+ return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
318
+ }
319
+ const external_node_fs_namespaceObject = require("node:fs");
320
+ const external_tinyglobby_namespaceObject = require("tinyglobby");
321
+ const external_node_crypto_namespaceObject = require("node:crypto");
322
+ const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
323
+ projectId,
324
+ sourcePath,
325
+ caseIndex,
326
+ ...invocationIndex ? [
327
+ invocationIndex
328
+ ] : []
329
+ ])).digest('hex');
330
+ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
331
+ projectId,
332
+ sourcePath,
333
+ ...invocationIndex ? [
334
+ invocationIndex
335
+ ] : []
336
+ ])).digest('hex');
337
+ const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
338
+ const external_js_yaml_namespaceObject = require("js-yaml");
339
+ function parseWorkflowSource(source) {
340
+ const stack = [];
341
+ const nodes = [];
342
+ const sensitiveNodes = new Set();
343
+ const sensitiveValues = new Set();
344
+ const redactedPaths = new Set();
345
+ const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
346
+ const parsed = (0, external_js_yaml_namespaceObject.load)(source, {
347
+ schema: external_js_yaml_namespaceObject.JSON_SCHEMA,
348
+ listener (event, state) {
349
+ if ('open' === event) return void stack.push({
350
+ start: state.position + sourceOffset,
351
+ end: 0,
352
+ value: null,
353
+ children: []
354
+ });
355
+ const node = stack.pop();
356
+ node.end = state.position + sourceOffset;
357
+ node.value = state.result;
358
+ stack.at(-1)?.children.push(node);
359
+ nodes.push(node);
360
+ if ('mapping' !== state.kind) return;
361
+ for(let index = 0; index < node.children.length; index += 2){
362
+ const key = node.children[index].value;
363
+ const value = node.children[index + 1];
364
+ if ('string' == typeof key && (0, test_runner_namespaceObject.isReportSecretKey)(key) && value) {
365
+ sensitiveNodes.add(value);
366
+ if (null !== value.value) sensitiveValues.add(value.value);
367
+ redactedPaths.add(`$..[${JSON.stringify(key)}]`);
368
+ }
369
+ }
370
+ }
1054
371
  });
1055
- };
1056
- const validateTagList = (value, field, label)=>{
1057
- if (void 0 === value) return [];
1058
- if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
1059
- return value.map((tag, index)=>{
1060
- if ('string' != typeof tag || 0 === tag.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
1061
- return tag;
372
+ const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
373
+ let offset = 0;
374
+ let redacted = '';
375
+ for (const range of ranges){
376
+ if (range.start < offset) continue;
377
+ const text = source.slice(range.start, range.end);
378
+ const leading = text.match(/^\s*/)?.[0] ?? '';
379
+ const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
380
+ redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
381
+ offset = range.end;
382
+ }
383
+ redacted += source.slice(offset);
384
+ const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
385
+ return {
386
+ parsed,
387
+ sourceYaml: {
388
+ value,
389
+ ...redactedPaths.size ? {
390
+ redactedPaths: [
391
+ ...redactedPaths
392
+ ]
393
+ } : {},
394
+ ...value !== redacted ? {
395
+ truncatedPaths: [
396
+ '$'
397
+ ]
398
+ } : {}
399
+ }
400
+ };
401
+ }
402
+ const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
403
+ const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
404
+ const exactProjectExpression = /^\$\{([^{}]+)\}$/;
405
+ const missingVariable = (kind, name, options)=>{
406
+ throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
407
+ ...options.location,
408
+ variable: name,
409
+ variableKind: kind
1062
410
  });
1063
411
  };
1064
- const validateTagSelection = (value, label)=>{
1065
- if (void 0 === value) return Object.freeze({
1066
- include: [],
1067
- exclude: []
1068
- });
1069
- if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
1070
- return Object.freeze({
1071
- include: Object.freeze(validateTagList(value.include, 'include', label)),
1072
- exclude: Object.freeze(validateTagList(value.exclude, 'exclude', label))
1073
- });
412
+ const projectVariable = (name, options)=>{
413
+ if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
414
+ return options.variables[name];
1074
415
  };
1075
- function assertJsonValue(value, path, seen) {
1076
- if (null === value || 'string' == typeof value || 'boolean' == typeof value) return;
1077
- if ('number' == typeof value) {
1078
- if (Number.isFinite(value)) return;
1079
- throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
1080
- }
1081
- if ('object' != typeof value || void 0 === value) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
1082
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
1083
- if (seen.has(value)) throw new TypeError(`Midscene config ${path} must not contain cycles.`);
1084
- seen.add(value);
1085
- if (Array.isArray(value)) value.forEach((item, index)=>assertJsonValue(item, `${path}[${index}]`, seen));
1086
- else for (const [key, child] of Object.entries(value))assertJsonValue(child, `${path}.${key}`, seen);
1087
- seen.delete(value);
1088
- }
1089
- const deepFreezeJson = (value)=>{
1090
- if ('object' == typeof value && null !== value) {
1091
- for (const child of Object.values(value))deepFreezeJson(child);
1092
- Object.freeze(value);
1093
- }
416
+ const environmentVariable = (name, options)=>{
417
+ const value = options.env?.[name];
418
+ if (void 0 === value) return missingVariable('environment', name, options);
1094
419
  return value;
1095
420
  };
1096
- const validateVariables = (value, label)=>{
1097
- if (void 0 === value) return Object.freeze({});
1098
- if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
1099
- assertJsonValue(value, label, new Set());
1100
- return deepFreezeJson(value);
421
+ const interpolationValue = (name, value, options)=>{
422
+ if ('object' == typeof value && null !== value) throw new test_runner_namespaceObject.WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
423
+ ...options.location,
424
+ variable: name,
425
+ variableKind: 'project'
426
+ });
427
+ return String(value);
1101
428
  };
1102
- const validateSetup = (value, label)=>{
1103
- if (void 0 === value) return;
1104
- if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
1105
- test_project_rejectUnknownKeys(value, [
1106
- 'name',
1107
- 'setup'
1108
- ], label);
1109
- if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
1110
- if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
1111
- return value;
429
+ const resolveString = (value, options)=>{
430
+ const exactEnv = exactEnvExpression.exec(value);
431
+ if (exactEnv) return environmentVariable(exactEnv[1], options);
432
+ const exactProject = exactProjectExpression.exec(value);
433
+ if (exactProject) return projectVariable(exactProject[1], options);
434
+ return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
1112
435
  };
1113
- const validatePositiveInteger = (value, fallback, label)=>{
1114
- if (void 0 === value) return fallback;
1115
- if (!Number.isInteger(value) || value <= 0) throw new TypeError(`Midscene config ${label} must be a positive integer.`);
436
+ const resolveWorkflowVariables = (value, options)=>{
437
+ if ('string' == typeof value) return resolveString(value, options);
438
+ if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
439
+ if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
440
+ key,
441
+ resolveWorkflowVariables(child, options)
442
+ ]));
1116
443
  return value;
1117
444
  };
1118
- const validateNonNegativeInteger = (value, fallback, label)=>{
1119
- if (void 0 === value) return fallback;
1120
- if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
1121
- return value;
445
+ function _define_property(obj, key, value) {
446
+ if (key in obj) Object.defineProperty(obj, key, {
447
+ value: value,
448
+ enumerable: true,
449
+ configurable: true,
450
+ writable: true
451
+ });
452
+ else obj[key] = value;
453
+ return obj;
454
+ }
455
+ const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
456
+ class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
457
+ constructor(actualFormat, sourcePath){
458
+ 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.`, {
459
+ sourcePath,
460
+ actualFormat,
461
+ expectedFormat: 'native'
462
+ }), _define_property(this, "actualFormat", void 0);
463
+ this.actualFormat = actualFormat;
464
+ }
465
+ }
466
+ const collect_rejectUnknownKeys = (value, allowed, label)=>{
467
+ const allowedSet = new Set(allowed);
468
+ const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
469
+ if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
470
+ field: unknown[0]
471
+ });
1122
472
  };
1123
- const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
1124
- if (void 0 === value) {
1125
- const setup = validateSetup(defaultSetup, 'setup');
1126
- return {
1127
- hasExplicitProjects: false,
1128
- projects: Object.freeze([
1129
- Object.freeze({
1130
- projectId: projectIdFromIndex(0),
1131
- name: 'default',
1132
- ...setup ? {
1133
- setup
1134
- } : {},
1135
- tags: Object.freeze({
1136
- include: [],
1137
- exclude: []
1138
- }),
1139
- retry: 0,
1140
- variables: Object.freeze({}),
1141
- nodes: new NodeRegistry(globalNodes.definitions())
1142
- })
1143
- ])
1144
- };
473
+ const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
474
+ const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
475
+ function collectWorkflowDocument(source, options) {
476
+ let snapshot;
477
+ try {
478
+ snapshot = parseWorkflowSource((0, external_node_fs_namespaceObject.readFileSync)(source.absolutePath, 'utf8'));
479
+ } catch (error) {
480
+ throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
481
+ sourcePath: source.sourcePath
482
+ }, error);
1145
483
  }
1146
- if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
1147
- if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
1148
- const names = new Set();
1149
- const projects = value.map((candidate, index)=>{
1150
- const label = `projects[${index}]`;
1151
- if (!isRecord(candidate)) throw new TypeError(`Midscene config ${label} must be an object.`);
1152
- test_project_rejectUnknownKeys(candidate, [
484
+ const { parsed, sourceYaml } = snapshot;
485
+ if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
486
+ const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
487
+ if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
488
+ collect_rejectUnknownKeys(parsed, [
489
+ 'beforeAll',
490
+ 'beforeEach',
491
+ 'cases',
492
+ 'afterEach',
493
+ 'afterAll'
494
+ ], 'Workflow document');
495
+ if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
496
+ const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
497
+ const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
498
+ ...step,
499
+ templateInput: step.input,
500
+ input: resolveWorkflowVariables(step.input, {
501
+ variables: options.variables,
502
+ env: options.env ?? process.env,
503
+ location: {
504
+ projectName: source.projectName ?? source.projectId,
505
+ sourcePath,
506
+ phase,
507
+ stepIndex,
508
+ ...void 0 === caseIndex ? {} : {
509
+ caseIndex
510
+ }
511
+ }
512
+ })
513
+ });
514
+ const normalizeLifecycle = (phase)=>{
515
+ const value = parsed[phase];
516
+ if (void 0 === value) return [];
517
+ if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
518
+ phase
519
+ });
520
+ return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
521
+ const resolved = options.resolveNode(normalized.node);
522
+ if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
523
+ phase,
524
+ stepIndex,
525
+ node: normalized.node
526
+ });
527
+ return resolveStepVariables(normalized, phase, stepIndex);
528
+ });
529
+ };
530
+ const lifecycle = {
531
+ beforeAll: normalizeLifecycle('beforeAll'),
532
+ beforeEach: normalizeLifecycle('beforeEach'),
533
+ afterEach: normalizeLifecycle('afterEach'),
534
+ afterAll: normalizeLifecycle('afterAll')
535
+ };
536
+ const ids = new Set();
537
+ const cases = parsed.cases.map((definition, caseIndex)=>{
538
+ if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
539
+ caseIndex
540
+ });
541
+ collect_rejectUnknownKeys(definition, [
1153
542
  'name',
1154
- 'setup',
1155
- 'nodes',
1156
- 'files',
1157
543
  'tags',
1158
- 'retry',
1159
- 'variables'
1160
- ], label);
1161
- if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
1162
- if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
1163
- names.add(candidate.name);
1164
- const files = validateTestFileSelection(candidate.files, `${label}.files`);
1165
- if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
1166
- const localNodes = new NodeRegistry(candidate.nodes);
1167
- const nodes = new NodeRegistry([
1168
- ...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
1169
- ...localNodes.definitions()
1170
- ]);
1171
- return Object.freeze({
1172
- projectId: projectIdFromIndex(index),
1173
- name: candidate.name,
1174
- nodes,
1175
- ...files ? {
1176
- files
1177
- } : {},
1178
- tags: validateTagSelection(candidate.tags, `${label}.tags`),
1179
- retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
1180
- variables: validateVariables(candidate.variables, `${label}.variables`),
1181
- ...void 0 === candidate.setup ? {} : {
1182
- setup: validateSetup(candidate.setup, `${label}.setup`)
1183
- }
544
+ 'steps'
545
+ ], `Case ${caseIndex + 1}`);
546
+ 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.`, {
547
+ caseIndex
548
+ });
549
+ 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.`, {
550
+ caseIndex
551
+ });
552
+ const tags = definition.tags ?? [];
553
+ 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.`, {
554
+ caseIndex
555
+ });
556
+ const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
557
+ if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
558
+ caseIndex,
559
+ stepIndex,
560
+ node: normalized.node
561
+ });
562
+ return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
563
+ });
564
+ const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
565
+ if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
566
+ caseId
1184
567
  });
568
+ ids.add(caseId);
569
+ return {
570
+ caseId,
571
+ projectId: source.projectId,
572
+ sourcePath,
573
+ caseIndex,
574
+ definition: {
575
+ name: definition.name,
576
+ tags: tags,
577
+ steps
578
+ }
579
+ };
1185
580
  });
1186
581
  return {
1187
- projects: Object.freeze(projects),
1188
- hasExplicitProjects: true
582
+ documentId: createWorkflowDocumentId(source.projectId, sourcePath),
583
+ projectId: source.projectId,
584
+ sourcePath,
585
+ sourceYaml,
586
+ lifecycle,
587
+ cases
1189
588
  };
589
+ }
590
+ const promises_namespaceObject = require("node:fs/promises");
591
+ const writeJson = async (path, value)=>{
592
+ try {
593
+ await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(path), {
594
+ recursive: true
595
+ });
596
+ await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
597
+ } catch (error) {
598
+ throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
599
+ }
1190
600
  };
1191
- const validateTestOptions = (value)=>{
1192
- if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
1193
- const candidate = value ?? {};
1194
- const executionUnit = candidate.executionUnit ?? 'project';
1195
- if ('project' !== executionUnit && 'case' !== executionUnit) throw new TypeError('Midscene config test.executionUnit must be "project" or "case".');
1196
- return Object.freeze({
1197
- maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
1198
- bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
1199
- testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout'),
1200
- executionUnit,
1201
- executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
601
+ const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, 'plan.json'), {
602
+ schemaVersion: 1,
603
+ createdAt: new Date().toISOString(),
604
+ projects: projects.map((item)=>({
605
+ projectId: item.project.projectId,
606
+ name: item.project.name,
607
+ documents: item.plans ?? []
608
+ }))
1202
609
  });
610
+ const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
611
+ const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
612
+ const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
613
+ const collectionErrorPath = (projectId, sourcePath)=>{
614
+ const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
615
+ projectId,
616
+ sourcePath
617
+ ])).digest('hex');
618
+ return toPosix((0, external_node_path_namespaceObject.join)(projectId, 'collection-errors', `${id}.json`));
1203
619
  };
1204
- const validateTestExecutor = (value)=>{
1205
- if (void 0 === value) return;
1206
- if (!isRecord(value)) throw new TypeError('Midscene config executor must be an object.');
1207
- test_project_rejectUnknownKeys(value, [
1208
- 'name',
1209
- 'execute'
1210
- ], 'executor');
1211
- if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError('Midscene config executor.name must be non-empty.');
1212
- if ('function' != typeof value.execute) throw new TypeError('Midscene config executor.execute must be a function.');
1213
- return Object.freeze(value);
620
+ const writeWorkflowDocumentResult = (runDir, result)=>{
621
+ const { documentRunId: _documentRunId, ...persistedResult } = result;
622
+ return writeJson((0, external_node_path_namespaceObject.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
1214
623
  };
1215
- const validateOutputPath = (value, fallback, label)=>{
1216
- if (void 0 === value) return fallback;
1217
- if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
1218
- return value;
624
+ const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
625
+ const { runId: attemptId, ...persistedResult } = result;
626
+ return writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
627
+ ...persistedResult,
628
+ attemptId
629
+ });
1219
630
  };
1220
- const validateOutput = (value)=>{
1221
- if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config output must be an object.');
1222
- const candidate = value ?? {};
1223
- test_project_rejectUnknownKeys(candidate, [
1224
- 'reportDir'
1225
- ], 'output');
1226
- return Object.freeze({
1227
- reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
631
+ const writeCollectionError = (runDir, collectionError)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
632
+ kind: 'collection-error',
633
+ ...collectionError
1228
634
  });
635
+ const relativeToSummary = (summaryPath, target)=>{
636
+ const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
637
+ return toPosix(value || '.');
1229
638
  };
1230
- const validateTestProjectDefinition = (definition)=>{
1231
- if (!isRecord(definition)) throw new TypeError('Midscene config must default export an object.');
1232
- if (void 0 !== definition.nodes && !Array.isArray(definition.nodes)) throw new TypeError('Midscene config nodes must be an array.');
1233
- if ('setupWorkflow' in definition) throw new TypeError('Midscene config setupWorkflow is no longer supported. Use setup or projects[].setup instead.');
1234
- if ('setupDocument' in definition) throw new TypeError('Midscene config setupDocument is not supported. Use setup or projects[].setup for resources, and beforeAll/afterAll for document lifecycle steps.');
1235
- if ('root' in definition) throw new TypeError('Midscene config root is not supported. Pass the Test Project directory to midscene-test instead.');
1236
- if ('files' in definition) throw new TypeError('Midscene config files is not supported at the root. Move it to projects[].files.');
1237
- if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
1238
- test_project_rejectUnknownKeys(definition, [
1239
- 'setup',
1240
- 'projects',
1241
- 'test',
1242
- 'output',
1243
- 'nodes',
1244
- 'executor'
1245
- ], 'root');
1246
- const nodes = new NodeRegistry(definition.nodes);
1247
- const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
1248
- const test = validateTestOptions(definition.test);
1249
- const executor = validateTestExecutor(definition.executor);
1250
- if (executor && 'case' !== test.executionUnit) throw new TypeError('Midscene config executor requires test.executionUnit to be "case".');
1251
- return {
1252
- ...resolvedProjects,
1253
- test,
1254
- output: validateOutput(definition.output),
1255
- nodes,
1256
- ...executor ? {
1257
- executor
639
+ const errorJson = test_runner_namespaceObject.sanitizeReportError;
640
+ const writeTestProjectRunResult = (options)=>{
641
+ const { result } = options;
642
+ const fact = (path)=>toPosix(path);
643
+ return writeJson(result.summaryPath, {
644
+ schemaVersion: result.schemaVersion,
645
+ runId: result.runId,
646
+ startedAt: result.startedAt,
647
+ endedAt: result.endedAt,
648
+ durationMs: result.durationMs,
649
+ status: result.status,
650
+ exitCode: result.exitCode,
651
+ ...result.errors?.length ? {
652
+ errors: result.errors.map(errorJson)
1258
653
  } : {},
1259
- resolveNode: (name)=>nodes.get(name)
1260
- };
1261
- };
1262
- const assertSupportedConfig = (absolutePath)=>{
1263
- if (!absolutePath.endsWith('.ts') && !absolutePath.endsWith('.mjs')) {
1264
- const extension = absolutePath.match(/(\.[^./\\]+)$/)?.[1] ?? '(none)';
1265
- throw new TypeError(`Unsupported Midscene config extension: ${extension}. Supported extensions: .ts, .mjs.`);
1266
- }
1267
- return absolutePath.endsWith('.mjs') ? '.mjs' : '.ts';
1268
- };
1269
- const canRetryWithCjsLoader = (error)=>{
1270
- if (!(error instanceof Error)) return false;
1271
- const errorCode = error.code;
1272
- return error instanceof SyntaxError && /^Unexpected (?:identifier|reserved word|token)/.test(error.message) || 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' === errorCode || 'ERR_UNKNOWN_FILE_EXTENSION' === errorCode && /^Unknown file extension\b/.test(error.message);
1273
- };
1274
- async function loadTestProject(configPath) {
1275
- if (!configPath) return validateTestProjectDefinition({
1276
- nodes: []
654
+ projectRoot: options.projectRoot,
655
+ ...options.configPath ? {
656
+ configPath: options.configPath
657
+ } : {},
658
+ factsRoot: '.',
659
+ plan: 'plan.json',
660
+ ...result.operationLog ? {
661
+ operationLog: {
662
+ ...result.operationLog,
663
+ path: relativeToSummary(result.summaryPath, result.operationLog.path)
664
+ }
665
+ } : {},
666
+ reportDir: relativeToSummary(result.summaryPath, result.reportDir),
667
+ ...result.reportPath ? {
668
+ report: relativeToSummary(result.summaryPath, result.reportPath)
669
+ } : {},
670
+ ...result.markdownPath ? {
671
+ markdown: relativeToSummary(result.summaryPath, result.markdownPath)
672
+ } : {},
673
+ summary: result.summary,
674
+ projects: result.projects.map((project)=>({
675
+ projectId: project.projectId,
676
+ name: project.name,
677
+ status: project.status,
678
+ retry: project.retry,
679
+ fileSelection: project.fileSelection,
680
+ tagSelection: project.tagSelection,
681
+ sourceCount: project.sourceCount,
682
+ selectedCaseCount: project.selectedCaseCount,
683
+ filteredCaseCount: project.filteredCaseCount,
684
+ ...project.lifecycle ? {
685
+ lifecycle: {
686
+ status: project.lifecycle.status,
687
+ startedAt: project.lifecycle.startedAt,
688
+ endedAt: project.lifecycle.endedAt,
689
+ durationMs: project.lifecycle.durationMs,
690
+ ...project.lifecycle.cleanup ? {
691
+ cleanup: project.lifecycle.cleanup
692
+ } : {},
693
+ ...project.lifecycle.pendingOperations ? {
694
+ pendingOperations: project.lifecycle.pendingOperations
695
+ } : {},
696
+ ...project.lifecycle.setupError ? {
697
+ setupError: errorJson(project.lifecycle.setupError)
698
+ } : {},
699
+ ...project.lifecycle.teardownErrors ? {
700
+ teardownErrors: project.lifecycle.teardownErrors.map(errorJson)
701
+ } : {}
702
+ }
703
+ } : {},
704
+ cases: project.cases.map((outcome)=>({
705
+ ...outcome.documentRunId ? {
706
+ documentRunId: outcome.documentRunId
707
+ } : {},
708
+ documentId: outcome.documentId,
709
+ caseId: outcome.caseId,
710
+ sourcePath: outcome.sourcePath,
711
+ caseIndex: outcome.caseIndex,
712
+ name: outcome.name,
713
+ status: outcome.status,
714
+ ...outcome.notRunReason ? {
715
+ notRunReason: outcome.notRunReason
716
+ } : {},
717
+ ...outcome.attempts ? {
718
+ attempts: outcome.attempts.map((attempt)=>({
719
+ attemptId: attempt.runId,
720
+ attemptIndex: attempt.attemptIndex,
721
+ status: attempt.status,
722
+ ...attempt.cleanup ? {
723
+ cleanup: attempt.cleanup
724
+ } : {},
725
+ ...attempt.pendingOperations ? {
726
+ pendingOperations: attempt.pendingOperations
727
+ } : {},
728
+ ...attempt.executionStatus ? {
729
+ executionStatus: attempt.executionStatus
730
+ } : {},
731
+ ...attempt.retryDecision ? {
732
+ retryDecision: attempt.retryDecision
733
+ } : {},
734
+ resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
735
+ ...attempt.reportPaths?.length ? {
736
+ reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
737
+ } : {}
738
+ }))
739
+ } : {}
740
+ })),
741
+ documents: project.documents.map((document)=>({
742
+ documentId: document.documentId,
743
+ ...void 0 === document.attemptIndex ? {} : {
744
+ attemptIndex: document.attemptIndex,
745
+ documentRunId: document.documentRunId
746
+ },
747
+ sourcePath: document.sourcePath,
748
+ status: document.status,
749
+ ...document.cleanup ? {
750
+ cleanup: document.cleanup
751
+ } : {},
752
+ ...document.pendingOperations ? {
753
+ pendingOperations: document.pendingOperations
754
+ } : {},
755
+ ...document.executionStatus ? {
756
+ executionStatus: document.executionStatus
757
+ } : {},
758
+ ...document.retryDecision ? {
759
+ retryDecision: document.retryDecision
760
+ } : {},
761
+ resultFile: fact(workflowDocumentResultPath(document)),
762
+ ...document.reportPaths?.length ? {
763
+ reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
764
+ } : {}
765
+ })),
766
+ collectionErrors: project.collectionErrors.map((error)=>({
767
+ sourcePath: error.sourcePath,
768
+ errorFile: fact(collectionErrorPath(error.projectId, error.sourcePath))
769
+ }))
770
+ }))
1277
771
  });
1278
- const absolutePath = (0, external_node_path_namespaceObject.resolve)(configPath);
1279
- const extension = assertSupportedConfig(absolutePath);
1280
- let loaded;
1281
- try {
1282
- if ('.mjs' === extension) loaded = await import((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href);
1283
- else {
1284
- const { tsImport } = await import("tsx/esm/api");
1285
- loaded = await tsImport((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href, {
1286
- parentURL: (0, external_node_url_namespaceObject.pathToFileURL)(`${(0, external_node_path_namespaceObject.dirname)(absolutePath)}${external_node_path_namespaceObject.sep}`).href,
1287
- tsconfig: false
1288
- });
1289
- }
1290
- } catch (error) {
1291
- try {
1292
- if ('.mjs' === extension || !canRetryWithCjsLoader(error)) throw error;
1293
- loaded = (0, api_namespaceObject.require)(absolutePath, (0, external_node_url_namespaceObject.pathToFileURL)(absolutePath));
1294
- } catch (fallbackError) {
1295
- const message = fallbackError instanceof Error ? `: ${fallbackError.message}` : '';
1296
- throw new Error(`Failed to load Midscene config "${absolutePath}"${message}`, {
1297
- cause: fallbackError
1298
- });
1299
- }
1300
- }
1301
- return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
1302
- }
772
+ };
1303
773
  const CONFIG_NAMES = [
1304
774
  'midscene.config.ts',
1305
775
  'midscene.config.mjs'
@@ -1321,27 +791,6 @@ const DEFAULT_TEST_FILE_SELECTION = {
1321
791
  ]
1322
792
  };
1323
793
  const project_preparation_toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
1324
- const discoverSelectedTestFiles = (projectRoot, paths)=>{
1325
- const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
1326
- const files = new Set();
1327
- for (const requestedPath of paths){
1328
- if ('string' != typeof requestedPath || 0 === requestedPath.trim().length) throw new TypeError('Test selection paths must be non-empty strings.');
1329
- const absolutePath = (0, external_node_path_namespaceObject.resolve)(root, requestedPath);
1330
- const relativePath = (0, external_node_path_namespaceObject.relative)(root, absolutePath);
1331
- if ('..' === relativePath || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || (0, external_node_path_namespaceObject.isAbsolute)(relativePath)) throw new Error(`Test selection path is outside the project: ${requestedPath}`);
1332
- if (!(0, external_node_fs_namespaceObject.existsSync)(absolutePath)) throw new Error(`Test selection path does not exist: ${requestedPath}`);
1333
- if ((0, external_node_fs_namespaceObject.statSync)(absolutePath).isDirectory()) for (const file of discoverTestFiles(absolutePath))files.add(file);
1334
- else if (/\.ya?ml$/i.test(absolutePath)) files.add(absolutePath);
1335
- else throw new Error(`Test selection file must be YAML: ${requestedPath}`);
1336
- }
1337
- return [
1338
- ...files
1339
- ].sort((a, b)=>{
1340
- const relativeA = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, a));
1341
- const relativeB = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, b));
1342
- return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
1343
- });
1344
- };
1345
794
  const discoverResolvedTestFiles = (projectRoot, selection)=>{
1346
795
  const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
1347
796
  const match = (patterns)=>(0, external_tinyglobby_namespaceObject.globSync)(patterns, {
@@ -1401,14 +850,8 @@ const matchesTags = (tags, selection)=>{
1401
850
  if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
1402
851
  return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
1403
852
  };
1404
- const filterDocumentCases = (document, project, selection)=>{
1405
- const cases = document.cases.filter((item)=>{
1406
- const tags = item.definition.tags ?? [];
1407
- return matchesTags(tags, project.tags) && matchesTags(tags, {
1408
- include: selection?.tags?.include ?? [],
1409
- exclude: selection?.tags?.exclude ?? []
1410
- }) && (!selection?.caseIds?.length || selection.caseIds.includes(item.caseId));
1411
- });
853
+ const filterDocumentCases = (document, project)=>{
854
+ const cases = document.cases.filter((item)=>matchesTags(item.definition.tags ?? [], project.tags));
1412
855
  const filtered = document.cases.length - cases.length;
1413
856
  return 0 === cases.length ? {
1414
857
  filtered
@@ -1433,9 +876,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1433
876
  const platform = options.platform?.trim();
1434
877
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
1435
878
  const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_namespaceObject.resolve)(options.prerequisiteFile) : void 0;
1436
- const projectFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
1437
- const selectedFiles = options.selection?.paths?.length ? new Set(discoverSelectedTestFiles(projectRoot, options.selection.paths)) : void 0;
1438
- const mainFiles = selectedFiles ? projectFiles.filter((file)=>selectedFiles.has(file)) : projectFiles;
879
+ const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
1439
880
  const files = [
1440
881
  ...prerequisiteFile ? [
1441
882
  prerequisiteFile
@@ -1458,7 +899,6 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1458
899
  const invocations = [];
1459
900
  const plans = [];
1460
901
  let filteredCaseCount = 0;
1461
- const availableCaseIds = new Set();
1462
902
  if (0 === sources.length) {
1463
903
  const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
1464
904
  projectName: project.name,
@@ -1489,16 +929,10 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1489
929
  variables: project.variables,
1490
930
  env: process.env
1491
931
  });
1492
- const duplicate = document.cases.find((item)=>availableCaseIds.has(item.caseId));
1493
- if (duplicate) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision in project "${project.name}": ${duplicate.caseId}.`, {
1494
- caseId: duplicate.caseId,
1495
- projectName: project.name
1496
- });
1497
- for (const item of document.cases)availableCaseIds.add(item.caseId);
1498
932
  const filtered = source.absolutePath === prerequisiteFile ? {
1499
933
  document,
1500
934
  filtered: 0
1501
- } : filterDocumentCases(document, project, options.selection);
935
+ } : filterDocumentCases(document, project);
1502
936
  const plan = (0, test_runner_namespaceObject.buildTestRunReportPlan)(document, {
1503
937
  selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
1504
938
  defaultTimeoutMs,
@@ -1546,42 +980,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1546
980
  plans,
1547
981
  collectionErrors,
1548
982
  selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
1549
- filteredCaseCount,
1550
- availableCaseIds: [
1551
- ...availableCaseIds
1552
- ]
983
+ filteredCaseCount
1553
984
  };
1554
985
  }
1555
- const validateUniqueStrings = (values, label)=>{
1556
- if (void 0 === values) return;
1557
- if (!Array.isArray(values) || values.some((value)=>'string' != typeof value || 0 === value.trim().length)) throw new TypeError(`${label} must be an array of non-empty strings.`);
1558
- if (new Set(values).size !== values.length) throw new TypeError(`${label} must not contain duplicates.`);
1559
- return values;
1560
- };
1561
- const validateRunSelection = (options)=>{
1562
- const paths = validateUniqueStrings(options.paths, 'Test selection paths');
1563
- const caseIds = validateUniqueStrings(options.caseIds, 'Test selection caseIds');
1564
- const include = validateUniqueStrings(options.tags?.include, 'Test selection tags.include');
1565
- const exclude = validateUniqueStrings(options.tags?.exclude, 'Test selection tags.exclude');
1566
- return {
1567
- ...paths ? {
1568
- paths
1569
- } : {},
1570
- ...caseIds ? {
1571
- caseIds
1572
- } : {},
1573
- ...include || exclude ? {
1574
- tags: {
1575
- ...include ? {
1576
- include
1577
- } : {},
1578
- ...exclude ? {
1579
- exclude
1580
- } : {}
1581
- }
1582
- } : {}
1583
- };
1584
- };
1585
986
  const padDatePart = (value)=>String(value).padStart(2, '0');
1586
987
  const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
1587
988
  const timestamp = [
@@ -1619,7 +1020,6 @@ async function prepareTestRun(options) {
1619
1020
  const startedAt = new Date();
1620
1021
  const runId = createTestRunId(startedAt);
1621
1022
  const input = resolveRunInput(options);
1622
- const selection = validateRunSelection(options);
1623
1023
  const configPath = options.configPath ? (0, external_node_path_namespaceObject.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
1624
1024
  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}\`.`);
1625
1025
  if (options.configPath && !(0, external_node_fs_namespaceObject.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
@@ -1633,20 +1033,11 @@ async function prepareTestRun(options) {
1633
1033
  });
1634
1034
  (0, external_node_fs_namespaceObject.mkdirSync)(runDir);
1635
1035
  const projects = [];
1636
- for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
1637
- selection,
1638
- ...input.singleFile ? {
1639
- files: [
1640
- input.singleFile
1641
- ]
1642
- } : {}
1643
- }));
1644
- if (selection.caseIds?.length) {
1645
- const availableCaseIds = new Set(projects.flatMap((project)=>project.availableCaseIds ?? []));
1646
- const unknown = selection.caseIds.find((id)=>!availableCaseIds.has(id));
1647
- if (unknown) throw new Error(`Unknown Midscene case id: ${unknown}`);
1648
- }
1649
- if ((selection.paths?.length || selection.caseIds?.length || selection.tags?.include?.length || selection.tags?.exclude?.length) && projects.every((project)=>0 === project.selectedCaseCount && 0 === project.collectionErrors.length)) throw new Error('No Midscene cases matched the requested selection.');
1036
+ for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
1037
+ files: [
1038
+ input.singleFile
1039
+ ]
1040
+ } : void 0));
1650
1041
  const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
1651
1042
  return {
1652
1043
  startedAt,
@@ -1745,601 +1136,20 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
1745
1136
  }));
1746
1137
  return output;
1747
1138
  }
1748
- const asNotRun = (documentId, collectedCase, projectName, reason)=>({
1749
- documentId,
1750
- caseId: collectedCase.caseId,
1751
- projectName,
1752
- name: collectedCase.definition.name,
1753
- sourcePath: collectedCase.sourcePath,
1754
- caseIndex: collectedCase.caseIndex,
1755
- status: 'not-run',
1756
- notRunReason: reason
1757
- });
1139
+ const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
1758
1140
  const formatStep = (info)=>{
1759
- const position = 'case' === info.scope ? info.case : info.document;
1141
+ const position = stepPosition(info);
1760
1142
  const phase = 'steps' === position.phase ? 'step' : position.phase;
1761
1143
  return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
1762
1144
  };
1763
- const formatStepResult = (info, result, indent = '')=>{
1145
+ const formatStepResult = (info, result)=>{
1146
+ const indent = 'case' === info.scope ? ' ' : ' ';
1764
1147
  if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
1765
1148
  const symbol = 'success' === result.status ? '✓' : '✗';
1766
1149
  const error = result.error ? ` — ${result.error.message}` : '';
1767
1150
  const continuation = result.continuedAfterError ? '; continuing' : '';
1768
1151
  return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
1769
1152
  };
1770
- const latestById = (items, id)=>[
1771
- ...new Map(items.map((item)=>[
1772
- id(item),
1773
- item
1774
- ])).values()
1775
- ];
1776
- const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
1777
- const { project } = prepared;
1778
- const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status || item.execution?.lifecycle?.status === 'failed') || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
1779
- return {
1780
- projectId: project.projectId,
1781
- name: project.name,
1782
- ...prepared.platform ? {
1783
- platform: prepared.platform
1784
- } : {},
1785
- status: projectFailed ? 'failed' : 'success',
1786
- retry: project.retry,
1787
- fileSelection: prepared.fileSelection,
1788
- tagSelection: project.tags,
1789
- sourceCount: prepared.sources.length,
1790
- selectedCaseCount: prepared.selectedCaseCount,
1791
- filteredCaseCount: prepared.filteredCaseCount,
1792
- ...lifecycle ? {
1793
- lifecycle
1794
- } : {},
1795
- cases,
1796
- plans: prepared.plans,
1797
- documents,
1798
- collectionErrors: prepared.collectionErrors
1799
- };
1800
- };
1801
- function createExecutorReports(outputDir) {
1802
- const paths = new Map();
1803
- const sources = new Map();
1804
- return {
1805
- materializeSource (source) {
1806
- if (!source.dumpPath) {
1807
- const reference = this.materialize(source.sourcePath);
1808
- sources.set(reference, {
1809
- sourcePath: this.resolve(reference),
1810
- collection: source.collection
1811
- });
1812
- return reference;
1813
- }
1814
- const dump = (0, external_node_fs_namespaceObject.realpathSync)(source.dumpPath);
1815
- const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
1816
- const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
1817
- (0, external_node_fs_namespaceObject.mkdirSync)(destination, {
1818
- recursive: true
1819
- });
1820
- const dumpPath = (0, external_node_path_namespaceObject.join)(destination, 'snapshot.json');
1821
- (0, external_node_fs_namespaceObject.cpSync)(dump, dumpPath);
1822
- const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(dump), 'screenshots');
1823
- if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
1824
- recursive: true,
1825
- dereference: true
1826
- });
1827
- const sourcePath = (0, external_node_path_namespaceObject.join)(destination, 'index.html');
1828
- sources.set(reference, {
1829
- sourcePath,
1830
- dumpPath,
1831
- collection: source.collection
1832
- });
1833
- paths.set(reference, sourcePath);
1834
- return reference;
1835
- },
1836
- resolveSource (reference) {
1837
- return sources.get(reference);
1838
- },
1839
- materialize (sourcePath) {
1840
- const source = (0, external_node_fs_namespaceObject.realpathSync)(sourcePath);
1841
- const entry = (0, external_node_fs_namespaceObject.statSync)(source).isDirectory() ? (0, external_node_path_namespaceObject.join)(source, 'index.html') : source;
1842
- if (!(0, external_node_fs_namespaceObject.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
1843
- kind: 'report'
1844
- });
1845
- const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
1846
- const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
1847
- (0, external_node_fs_namespaceObject.mkdirSync)(destination, {
1848
- recursive: true
1849
- });
1850
- const isDirectoryReport = 'index.html' === (0, external_node_path_namespaceObject.basename)(entry);
1851
- const reportPath = (0, external_node_path_namespaceObject.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
1852
- (0, external_node_fs_namespaceObject.cpSync)(isDirectoryReport ? (0, external_node_path_namespaceObject.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
1853
- recursive: isDirectoryReport,
1854
- dereference: true
1855
- });
1856
- if (!isDirectoryReport) {
1857
- const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(entry), 'screenshots');
1858
- if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
1859
- recursive: true,
1860
- dereference: true
1861
- });
1862
- }
1863
- paths.set(reference, reportPath);
1864
- return reference;
1865
- },
1866
- resolve (reference) {
1867
- const reportPath = paths.get(reference);
1868
- if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
1869
- kind: 'report'
1870
- });
1871
- return reportPath;
1872
- }
1873
- };
1874
- }
1875
- async function runTaskPool(options) {
1876
- if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
1877
- const pending = options.tasks.map((_, index)=>index);
1878
- const results = new Array(options.tasks.length);
1879
- const activeResources = new Set();
1880
- let active = 0;
1881
- let firstError;
1882
- let hasError = false;
1883
- return await new Promise((resolve, reject)=>{
1884
- let settled = false;
1885
- const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
1886
- const finish = ()=>{
1887
- if (settled) return;
1888
- settled = true;
1889
- options.signal?.removeEventListener('abort', pump);
1890
- if (hasError) reject(firstError);
1891
- else resolve(results);
1892
- };
1893
- const release = (resources)=>{
1894
- for (const resource of resources)activeResources.delete(resource);
1895
- };
1896
- const claimRunnable = ()=>{
1897
- const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
1898
- if (pendingIndex < 0) return;
1899
- const [taskIndex] = pending.splice(pendingIndex, 1);
1900
- return taskIndex;
1901
- };
1902
- function pump() {
1903
- if (settled) return;
1904
- while(active < options.maxConcurrency && !stopRequested()){
1905
- const taskIndex = claimRunnable();
1906
- if (void 0 === taskIndex) break;
1907
- const task = options.tasks[taskIndex];
1908
- for (const resource of task.resources)activeResources.add(resource);
1909
- active += 1;
1910
- Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
1911
- results[taskIndex] = result;
1912
- }).catch((error)=>{
1913
- if (!hasError) {
1914
- hasError = true;
1915
- firstError = error;
1916
- }
1917
- }).finally(()=>{
1918
- release(task.resources);
1919
- active -= 1;
1920
- pump();
1921
- });
1922
- }
1923
- if (0 === active && (0 === pending.length || stopRequested())) finish();
1924
- }
1925
- options.signal?.addEventListener('abort', pump, {
1926
- once: true
1927
- });
1928
- pump();
1929
- });
1930
- }
1931
- const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
1932
- const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
1933
- const documentId = isolatedDocumentId(document, collectedCase);
1934
- return {
1935
- projectIndex,
1936
- prepared,
1937
- document,
1938
- collectedCase,
1939
- task: Object.freeze({
1940
- taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
1941
- projectId: prepared.project.projectId,
1942
- projectName: prepared.project.name,
1943
- documentId,
1944
- sourcePath: collectedCase.sourcePath,
1945
- caseId: collectedCase.caseId,
1946
- caseName: collectedCase.definition.name,
1947
- caseIndex: collectedCase.caseIndex,
1948
- tags: Object.freeze([
1949
- ...collectedCase.definition.tags ?? []
1950
- ]),
1951
- resources: Object.freeze([
1952
- ...collectedCase.definition.resources ?? []
1953
- ]),
1954
- retry: prepared.project.retry
1955
- })
1956
- };
1957
- })));
1958
- const reviveError = (error)=>{
1959
- const revived = new test_runner_namespaceObject.WorkflowError(error.message, {
1960
- code: error.code,
1961
- details: error.details,
1962
- cause: error.cause
1963
- });
1964
- revived.name = error.name;
1965
- return revived;
1966
- };
1967
- const reviveStep = (step)=>{
1968
- const { error, output, report, ...base } = step;
1969
- return {
1970
- ...base,
1971
- input: step.input,
1972
- meta: {
1973
- ...step.meta
1974
- },
1975
- ...output ? {
1976
- output: {
1977
- ...output
1978
- }
1979
- } : {},
1980
- ...error ? {
1981
- error: reviveError(error)
1982
- } : {},
1983
- ...report ? {
1984
- report: {
1985
- traces: report.traces.map((trace)=>({
1986
- ...trace
1987
- })),
1988
- ...report.collectionErrors ? {
1989
- collectionErrors: report.collectionErrors.map(reviveError)
1990
- } : {}
1991
- }
1992
- } : {}
1993
- };
1994
- };
1995
- const reviveAttempt = (attempt)=>{
1996
- const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
1997
- return {
1998
- ...base,
1999
- beforeEach: beforeEach.map(reviveStep),
2000
- steps: steps.map(reviveStep),
2001
- afterEach: afterEach.map(reviveStep),
2002
- ...executionErrors ? {
2003
- executionErrors: executionErrors.map(reviveError)
2004
- } : {},
2005
- ...teardownErrors ? {
2006
- teardownErrors: teardownErrors.map(reviveError)
2007
- } : {}
2008
- };
2009
- };
2010
- const reviveDocument = (document)=>{
2011
- const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
2012
- return {
2013
- ...base,
2014
- ...hostErrors ? {
2015
- hostErrors: hostErrors.map((error)=>({
2016
- ...error
2017
- }))
2018
- } : {},
2019
- beforeAll: beforeAll.map(reviveStep),
2020
- afterAll: afterAll.map(reviveStep),
2021
- ...executionErrors ? {
2022
- executionErrors: executionErrors.map(reviveError)
2023
- } : {},
2024
- ...teardownErrors ? {
2025
- teardownErrors: teardownErrors.map(reviveError)
2026
- } : {}
2027
- };
2028
- };
2029
- const reviveLifecycle = (lifecycle)=>{
2030
- const { setupError, teardownErrors, ...base } = lifecycle;
2031
- return {
2032
- ...base,
2033
- ...setupError ? {
2034
- setupError: reviveError(setupError)
2035
- } : {},
2036
- ...teardownErrors ? {
2037
- teardownErrors: teardownErrors.map(reviveError)
2038
- } : {}
2039
- };
2040
- };
2041
- const toTransportResult = (result, materializeReport, materializeSource)=>{
2042
- const transportReports = (scope)=>[
2043
- ...(scope.reportSources ?? []).map(materializeSource),
2044
- ...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
2045
- ];
2046
- const mapStep = (step)=>{
2047
- if (!step.report) return step;
2048
- const { sources: _sources, ...report } = step.report;
2049
- return {
2050
- ...step,
2051
- report
2052
- };
2053
- };
2054
- const mapAttempt = (attempt)=>{
2055
- const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
2056
- const reportRefs = transportReports(attempt);
2057
- return {
2058
- ...transportAttempt,
2059
- beforeEach: attempt.beforeEach.map(mapStep),
2060
- steps: attempt.steps.map(mapStep),
2061
- afterEach: attempt.afterEach.map(mapStep),
2062
- ...reportRefs.length ? {
2063
- reportRefs
2064
- } : {}
2065
- };
2066
- };
2067
- const attempts = result.case.attempts?.map(mapAttempt);
2068
- const run = attempts?.at(-1);
2069
- const { run: _run, attempts: _attempts, ...transportCase } = result.case;
2070
- const document = result.document ? (()=>{
2071
- const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
2072
- const reportRefs = transportReports(result.document);
2073
- return {
2074
- ...transportDocument,
2075
- ...result.document.hostErrors ? {
2076
- hostErrors: result.document.hostErrors.map(({ phase, error })=>({
2077
- phase,
2078
- error: (0, test_runner_namespaceObject.sanitizeReportError)(error)
2079
- }))
2080
- } : {},
2081
- beforeAll: result.document.beforeAll.map(mapStep),
2082
- afterAll: result.document.afterAll.map(mapStep),
2083
- ...reportRefs.length ? {
2084
- reportRefs
2085
- } : {}
2086
- };
2087
- })() : void 0;
2088
- return JSON.parse(JSON.stringify({
2089
- case: {
2090
- ...transportCase,
2091
- ...run ? {
2092
- run
2093
- } : {},
2094
- ...attempts ? {
2095
- attempts
2096
- } : {}
2097
- },
2098
- ...document ? {
2099
- document
2100
- } : {},
2101
- ...result.lifecycle ? {
2102
- lifecycle: result.lifecycle
2103
- } : {}
2104
- }));
2105
- };
2106
- const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
2107
- const resolveReports = (refs)=>{
2108
- const reportPaths = [];
2109
- const reportSources = [];
2110
- for (const ref of refs ?? []){
2111
- const path = resolveReportReference(ref);
2112
- const source = resolveSource(ref);
2113
- if (source) reportSources.push(source);
2114
- else reportPaths.push(path);
2115
- }
2116
- return {
2117
- ...reportPaths.length ? {
2118
- reportPaths
2119
- } : {},
2120
- ...reportSources.length ? {
2121
- reportSources
2122
- } : {}
2123
- };
2124
- };
2125
- const attempts = result.case.attempts?.map((attempt)=>({
2126
- ...reviveAttempt(attempt),
2127
- ...resolveReports(attempt.reportRefs)
2128
- }));
2129
- const run = attempts?.at(-1);
2130
- const { run: _run, attempts: _attempts, ...caseBase } = result.case;
2131
- return {
2132
- case: {
2133
- ...caseBase,
2134
- ...run ? {
2135
- run
2136
- } : {},
2137
- ...attempts ? {
2138
- attempts
2139
- } : {}
2140
- },
2141
- ...result.document ? {
2142
- document: {
2143
- ...reviveDocument(result.document),
2144
- ...resolveReports(result.document.reportRefs)
2145
- }
2146
- } : {},
2147
- ...result.lifecycle ? {
2148
- lifecycle: reviveLifecycle(result.lifecycle)
2149
- } : {}
2150
- };
2151
- };
2152
- const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
2153
- const { prepared, document, collectedCase, task } = preparedTask;
2154
- const { project } = prepared;
2155
- const isolatedDocument = {
2156
- ...document,
2157
- documentId: task.documentId,
2158
- cases: [
2159
- collectedCase
2160
- ]
2161
- };
2162
- const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
2163
- project,
2164
- setup: project.setup,
2165
- signal: options.signal
2166
- });
2167
- let lifecycle;
2168
- let execution;
2169
- let executionError;
2170
- try {
2171
- await runtime.start();
2172
- if (runtime.canRun) execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(isolatedDocument, {
2173
- resolveNode: project.nodes.require.bind(project.nodes),
2174
- project,
2175
- projectContext: runtime.context,
2176
- retry: project.retry,
2177
- signal: runtime.signal,
2178
- defaultTimeoutMs: options.test.testTimeout,
2179
- shouldStop: ()=>options.signal.aborted,
2180
- stopReason: ()=>'interrupted',
2181
- isFatalError: (run)=>[
2182
- ...run.beforeEach,
2183
- ...run.steps,
2184
- ...run.afterEach
2185
- ].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (run.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError),
2186
- onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
2187
- onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
2188
- onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
2189
- });
2190
- } catch (error) {
2191
- executionError = error;
2192
- if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) execution = error.result;
2193
- } finally{
2194
- const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
2195
- lifecycle = await runtime.finish(failed ? 'failed' : 'success');
2196
- }
2197
- if (executionError) {
2198
- if (!execution) throw executionError;
2199
- options.onInfrastructureError(executionError);
2200
- }
2201
- if (!execution) return toTransportResult({
2202
- case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
2203
- lifecycle
2204
- }, materializeReport, materializeSource);
2205
- const outcome = execution.cases[0];
2206
- if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
2207
- return toTransportResult({
2208
- case: {
2209
- ...outcome,
2210
- documentId: task.documentId
2211
- },
2212
- document: execution.document,
2213
- lifecycle
2214
- }, materializeReport, materializeSource);
2215
- };
2216
- const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
2217
- const { prepared, task } = preparedTask;
2218
- const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
2219
- taskProgress('started');
2220
- const outputDir = (0, external_node_path_namespaceObject.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
2221
- (0, external_node_fs_namespaceObject.mkdirSync)(outputDir, {
2222
- recursive: true
2223
- });
2224
- const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
2225
- if (prepared.collectionErrors.length > 0) return {
2226
- case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
2227
- };
2228
- const executor = options.executor ?? localTestExecutor;
2229
- let result;
2230
- let executorFailure;
2231
- let executorAttempts = 0;
2232
- for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
2233
- executorAttempts = attemptIndex + 1;
2234
- try {
2235
- result = await executor.execute(task, {
2236
- signal: options.signal,
2237
- onProgress: taskProgress,
2238
- outputDir,
2239
- materializeReport,
2240
- runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
2241
- });
2242
- executorFailure = void 0;
2243
- break;
2244
- } catch (error) {
2245
- if (!(error instanceof TestExecutorError)) throw error;
2246
- executorFailure = error;
2247
- if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
2248
- taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
2249
- }
2250
- }
2251
- if (!result) {
2252
- if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
2253
- taskProgress(`executor-failed: ${executorFailure.message}`);
2254
- return {
2255
- case: {
2256
- ...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
2257
- execution: {
2258
- executor: executor.name,
2259
- resources: task.resources,
2260
- attempts: executorAttempts,
2261
- failure: {
2262
- kind: executorFailure.kind,
2263
- message: executorFailure.message,
2264
- retryable: executorFailure.retryable
2265
- }
2266
- }
2267
- }
2268
- };
2269
- }
2270
- assertTestCaseTaskRunResult(task, result);
2271
- const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
2272
- const completed = {
2273
- ...internalResult,
2274
- case: {
2275
- ...internalResult.case,
2276
- execution: {
2277
- executor: executor.name,
2278
- resources: task.resources,
2279
- attempts: executorAttempts,
2280
- ...internalResult.lifecycle ? {
2281
- lifecycle: internalResult.lifecycle
2282
- } : {},
2283
- ...result.artifacts ? {
2284
- artifacts: result.artifacts
2285
- } : {},
2286
- ...result.metadata ? {
2287
- metadata: result.metadata
2288
- } : {}
2289
- }
2290
- }
2291
- };
2292
- onCompleted(completed);
2293
- for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
2294
- if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
2295
- taskProgress(internalResult.case.status);
2296
- return completed;
2297
- };
2298
- const runCaseExecution = async (options)=>{
2299
- const tasks = prepareCaseTasks(options.projects);
2300
- let failedCaseCount = 0;
2301
- const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
2302
- const results = new Array(tasks.length);
2303
- if (tasks.length > 0) try {
2304
- await runTaskPool({
2305
- tasks: tasks.map((preparedTask)=>preparedTask.task),
2306
- maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
2307
- signal: options.signal,
2308
- shouldStop: bailReached,
2309
- run: async (_task, taskIndex)=>{
2310
- try {
2311
- const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
2312
- results[taskIndex] = completed;
2313
- });
2314
- results[taskIndex] = result;
2315
- if ('failed' === result.case.status) failedCaseCount += 1;
2316
- return result;
2317
- } catch (error) {
2318
- options.onInfrastructureError(error);
2319
- throw error;
2320
- }
2321
- }
2322
- });
2323
- } catch (error) {
2324
- options.onInfrastructureError(error);
2325
- }
2326
- return options.projects.map((prepared, projectIndex)=>{
2327
- const projectTasks = tasks.map((task, taskIndex)=>({
2328
- task,
2329
- result: results[taskIndex]
2330
- })).filter(({ task })=>task.projectIndex === projectIndex);
2331
- const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
2332
- const cases = projectTasks.map(({ task, result })=>{
2333
- if (result) return result.case;
2334
- if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
2335
- return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
2336
- });
2337
- const documents = projectTasks.flatMap(({ result })=>result?.document ? [
2338
- result.document
2339
- ] : []);
2340
- return buildProjectResult(prepared, cases, documents);
2341
- });
2342
- };
2343
1153
  const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
2344
1154
  ...attempt.beforeEach,
2345
1155
  ...attempt.steps,
@@ -2385,7 +1195,7 @@ async function executeDocumentInvocation(options) {
2385
1195
  },
2386
1196
  onStepResult: async (info, result)=>{
2387
1197
  await invocation.bindings.onStepResult?.(info, result);
2388
- options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
1198
+ options.onProgress(formatStepResult(info, result));
2389
1199
  },
2390
1200
  onCaseResult: async (attempt)=>{
2391
1201
  await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
@@ -2503,6 +1313,16 @@ function createOperationJournal(runDir) {
2503
1313
  }
2504
1314
  };
2505
1315
  }
1316
+ const asNotRun = (documentId, collectedCase, projectName, reason)=>({
1317
+ documentId,
1318
+ caseId: collectedCase.caseId,
1319
+ projectName,
1320
+ name: collectedCase.definition.name,
1321
+ sourcePath: collectedCase.sourcePath,
1322
+ caseIndex: collectedCase.caseIndex,
1323
+ status: 'not-run',
1324
+ notRunReason: reason
1325
+ });
2506
1326
  const summarize = (projects)=>{
2507
1327
  const healthCases = projects.flatMap((project)=>{
2508
1328
  const documentAttemptByRunId = new Map(project.documents.map((document)=>[
@@ -2536,6 +1356,12 @@ const summarize = (projects)=>{
2536
1356
  projectFailures: projects.filter((project)=>'failed' === project.status).length
2537
1357
  };
2538
1358
  };
1359
+ const latestById = (items, id)=>[
1360
+ ...new Map(items.map((item)=>[
1361
+ id(item),
1362
+ item
1363
+ ])).values()
1364
+ ];
2539
1365
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
2540
1366
  async function runPreparedTestProject(plan) {
2541
1367
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
@@ -2548,7 +1374,7 @@ async function runPreparedTestProject(plan) {
2548
1374
  await writeTestRunPlan(runDir, preparedProjects);
2549
1375
  const journal = createOperationJournal(runDir);
2550
1376
  const rootController = new AbortController();
2551
- (0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, ('case' === definition.test.executionUnit ? Math.min(definition.test.maxConcurrency, totalCases) : effectiveConcurrency) + 1), rootController.signal);
1377
+ (0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, effectiveConcurrency + 1), rootController.signal);
2552
1378
  const handleSignal = (signal)=>{
2553
1379
  rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
2554
1380
  };
@@ -2590,6 +1416,31 @@ async function runPreparedTestProject(plan) {
2590
1416
  const { project } = prepared;
2591
1417
  runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
2592
1418
  };
1419
+ const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
1420
+ const { project } = prepared;
1421
+ 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';
1422
+ return {
1423
+ projectId: project.projectId,
1424
+ name: project.name,
1425
+ ...prepared.platform ? {
1426
+ platform: prepared.platform
1427
+ } : {},
1428
+ status: projectFailed ? 'failed' : 'success',
1429
+ retry: project.retry,
1430
+ fileSelection: prepared.fileSelection,
1431
+ tagSelection: project.tags,
1432
+ sourceCount: prepared.sources.length,
1433
+ selectedCaseCount: prepared.selectedCaseCount,
1434
+ filteredCaseCount: prepared.filteredCaseCount,
1435
+ ...lifecycle ? {
1436
+ lifecycle
1437
+ } : {},
1438
+ cases,
1439
+ plans: prepared.plans,
1440
+ documents,
1441
+ collectionErrors: prepared.collectionErrors
1442
+ };
1443
+ };
2593
1444
  const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
2594
1445
  announceProject(prepared, projectIndex);
2595
1446
  return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
@@ -2690,7 +1541,7 @@ async function runPreparedTestProject(plan) {
2690
1541
  };
2691
1542
  const projectResults = new Array(preparedProjects.length);
2692
1543
  try {
2693
- if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
1544
+ await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
2694
1545
  concurrency: effectiveConcurrency,
2695
1546
  shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
2696
1547
  }, async (prepared, index)=>{
@@ -2701,20 +1552,6 @@ async function runPreparedTestProject(plan) {
2701
1552
  throw error;
2702
1553
  }
2703
1554
  });
2704
- else {
2705
- const results = await runCaseExecution({
2706
- projects: preparedProjects,
2707
- test: definition.test,
2708
- executor: definition.executor,
2709
- runDir,
2710
- signal: rootController.signal,
2711
- progress,
2712
- onInfrastructureError: recordInfrastructureError
2713
- });
2714
- results.forEach((result, index)=>{
2715
- projectResults[index] = result;
2716
- });
2717
- }
2718
1555
  } catch (error) {
2719
1556
  recordInfrastructureError(error);
2720
1557
  } finally{
@@ -2821,32 +1658,22 @@ async function runTestProject(options = {}) {
2821
1658
  return runPreparedTestProject(await prepareTestRun(options));
2822
1659
  }
2823
1660
  exports.DEFAULT_TEST_FILE_SELECTION = __webpack_exports__.DEFAULT_TEST_FILE_SELECTION;
2824
- exports.TestExecutorError = __webpack_exports__.TestExecutorError;
2825
- exports.assertTestCaseTaskRunResult = __webpack_exports__.assertTestCaseTaskRunResult;
2826
1661
  exports.createTestRunId = __webpack_exports__.createTestRunId;
2827
1662
  exports.defineProjectSetup = __webpack_exports__.defineProjectSetup;
2828
- exports.defineTestExecutor = __webpack_exports__.defineTestExecutor;
2829
1663
  exports.defineTestProject = __webpack_exports__.defineTestProject;
2830
- exports.discoverSelectedTestFiles = __webpack_exports__.discoverSelectedTestFiles;
2831
1664
  exports.discoverTestConfig = __webpack_exports__.discoverTestConfig;
2832
1665
  exports.discoverTestFiles = __webpack_exports__.discoverTestFiles;
2833
1666
  exports.loadTestProject = __webpack_exports__.loadTestProject;
2834
- exports.localTestExecutor = __webpack_exports__.localTestExecutor;
2835
1667
  exports.runTestProject = __webpack_exports__.runTestProject;
2836
1668
  exports.validateTestFileSelection = __webpack_exports__.validateTestFileSelection;
2837
1669
  for(var __rspack_i in __webpack_exports__)if (-1 === [
2838
1670
  "DEFAULT_TEST_FILE_SELECTION",
2839
- "TestExecutorError",
2840
- "assertTestCaseTaskRunResult",
2841
1671
  "createTestRunId",
2842
1672
  "defineProjectSetup",
2843
- "defineTestExecutor",
2844
1673
  "defineTestProject",
2845
- "discoverSelectedTestFiles",
2846
1674
  "discoverTestConfig",
2847
1675
  "discoverTestFiles",
2848
1676
  "loadTestProject",
2849
- "localTestExecutor",
2850
1677
  "runTestProject",
2851
1678
  "validateTestFileSelection"
2852
1679
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];