@midscene/test 1.15.3 → 1.15.4-beta-20261013083552.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,9 +20,7 @@ var __webpack_modules__ = {
20
20
  module.exports = __rspack_external_node_path_c5b9b54f;
21
21
  },
22
22
  "./package.json" (module) {
23
- module.exports = {
24
- rE: "1.15.3"
25
- };
23
+ module.exports = JSON.parse('{"rE":"1.15.4-beta-20261013083552.0"}');
26
24
  }
27
25
  };
28
26
  var __webpack_module_cache__ = {};
@@ -269,7 +267,7 @@ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = fal
269
267
  return pattern;
270
268
  });
271
269
  };
272
- const test_project_validateTestFileSelection = (value, label = 'files')=>{
270
+ const validateTestFileSelection = (value, label = 'files')=>{
273
271
  if (void 0 === value) return;
274
272
  if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
275
273
  rejectUnknownKeys(value, [
@@ -393,7 +391,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
393
391
  if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
394
392
  if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
395
393
  names.add(candidate.name);
396
- const files = test_project_validateTestFileSelection(candidate.files, `${label}.files`);
394
+ const files = validateTestFileSelection(candidate.files, `${label}.files`);
397
395
  if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
398
396
  const localNodes = new registry_NodeRegistry(candidate.nodes);
399
397
  const nodes = new registry_NodeRegistry([
@@ -423,12 +421,27 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
423
421
  const validateTestOptions = (value)=>{
424
422
  if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
425
423
  const candidate = value ?? {};
424
+ const executionUnit = candidate.executionUnit ?? 'project';
425
+ if ('project' !== executionUnit && 'case' !== executionUnit) throw new TypeError('Midscene config test.executionUnit must be "project" or "case".');
426
426
  return Object.freeze({
427
427
  maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
428
428
  bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
429
- testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
429
+ testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout'),
430
+ executionUnit,
431
+ executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
430
432
  });
431
433
  };
434
+ const validateTestExecutor = (value)=>{
435
+ if (void 0 === value) return;
436
+ if (!isRecord(value)) throw new TypeError('Midscene config executor must be an object.');
437
+ rejectUnknownKeys(value, [
438
+ 'name',
439
+ 'execute'
440
+ ], 'executor');
441
+ if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError('Midscene config executor.name must be non-empty.');
442
+ if ('function' != typeof value.execute) throw new TypeError('Midscene config executor.execute must be a function.');
443
+ return Object.freeze(value);
444
+ };
432
445
  const validateOutputPath = (value, fallback, label)=>{
433
446
  if (void 0 === value) return fallback;
434
447
  if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
@@ -457,15 +470,22 @@ const validateTestProjectDefinition = (definition)=>{
457
470
  'projects',
458
471
  'test',
459
472
  'output',
460
- 'nodes'
473
+ 'nodes',
474
+ 'executor'
461
475
  ], 'root');
462
476
  const nodes = new registry_NodeRegistry(definition.nodes);
463
477
  const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
478
+ const test = validateTestOptions(definition.test);
479
+ const executor = validateTestExecutor(definition.executor);
480
+ if (executor && 'case' !== test.executionUnit) throw new TypeError('Midscene config executor requires test.executionUnit to be "case".');
464
481
  return {
465
482
  ...resolvedProjects,
466
- test: validateTestOptions(definition.test),
483
+ test,
467
484
  output: validateOutput(definition.output),
468
485
  nodes,
486
+ ...executor ? {
487
+ executor
488
+ } : {},
469
489
  resolveNode: (name)=>nodes.get(name)
470
490
  };
471
491
  };
@@ -510,6 +530,7 @@ async function loadTestProject(configPath) {
510
530
  }
511
531
  return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
512
532
  }
533
+ const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
513
534
  const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
514
535
  projectId,
515
536
  sourcePath,
@@ -729,10 +750,15 @@ function collectWorkflowDocument(source, options) {
729
750
  caseIndex
730
751
  });
731
752
  collect_rejectUnknownKeys(definition, [
753
+ 'id',
732
754
  'name',
733
755
  'tags',
756
+ 'resources',
734
757
  'steps'
735
758
  ], `Case ${caseIndex + 1}`);
759
+ if (void 0 !== definition.id && !isSafeResultPathSegment(definition.id)) throw new 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.`, {
760
+ caseIndex
761
+ });
736
762
  if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
737
763
  caseIndex
738
764
  });
@@ -743,6 +769,13 @@ function collectWorkflowDocument(source, options) {
743
769
  if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
744
770
  caseIndex
745
771
  });
772
+ const resources = definition.resources ?? [];
773
+ if (!Array.isArray(resources) || resources.some((resource)=>'string' != typeof resource || 0 === resource.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} resources must be an array of non-empty strings.`, {
774
+ caseIndex
775
+ });
776
+ if (new Set(resources).size !== resources.length) throw new WorkflowParseError(`Case ${caseIndex + 1} resources must not contain duplicates.`, {
777
+ caseIndex
778
+ });
746
779
  const steps = normalize_normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
747
780
  if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
748
781
  caseIndex,
@@ -751,7 +784,7 @@ function collectWorkflowDocument(source, options) {
751
784
  });
752
785
  return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
753
786
  });
754
- const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
787
+ const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
755
788
  if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
756
789
  caseId
757
790
  });
@@ -762,8 +795,14 @@ function collectWorkflowDocument(source, options) {
762
795
  sourcePath,
763
796
  caseIndex,
764
797
  definition: {
798
+ ...'string' == typeof definition.id ? {
799
+ id: definition.id
800
+ } : {},
765
801
  name: definition.name,
766
802
  tags: tags,
803
+ ...resources.length > 0 ? {
804
+ resources: resources
805
+ } : {},
767
806
  steps
768
807
  }
769
808
  };
@@ -903,6 +942,38 @@ const writeTestProjectRunResult = (options)=>{
903
942
  ...outcome.notRunReason ? {
904
943
  notRunReason: outcome.notRunReason
905
944
  } : {},
945
+ ...outcome.execution ? {
946
+ execution: {
947
+ executor: outcome.execution.executor,
948
+ resources: outcome.execution.resources,
949
+ ...void 0 === outcome.execution.attempts ? {} : {
950
+ attempts: outcome.execution.attempts
951
+ },
952
+ ...outcome.execution.failure ? {
953
+ failure: outcome.execution.failure
954
+ } : {},
955
+ ...outcome.execution.artifacts ? {
956
+ artifacts: outcome.execution.artifacts
957
+ } : {},
958
+ ...outcome.execution.metadata ? {
959
+ metadata: outcome.execution.metadata
960
+ } : {},
961
+ ...outcome.execution.lifecycle ? {
962
+ lifecycle: {
963
+ status: outcome.execution.lifecycle.status,
964
+ startedAt: outcome.execution.lifecycle.startedAt,
965
+ endedAt: outcome.execution.lifecycle.endedAt,
966
+ durationMs: outcome.execution.lifecycle.durationMs,
967
+ ...outcome.execution.lifecycle.setupError ? {
968
+ setupError: errorJson(outcome.execution.lifecycle.setupError)
969
+ } : {},
970
+ ...outcome.execution.lifecycle.teardownErrors ? {
971
+ teardownErrors: outcome.execution.lifecycle.teardownErrors.map(errorJson)
972
+ } : {}
973
+ }
974
+ } : {}
975
+ }
976
+ } : {},
906
977
  ...outcome.attempts ? {
907
978
  attempts: outcome.attempts.map((attempt)=>({
908
979
  attemptId: attempt.runId,
@@ -980,6 +1051,27 @@ const DEFAULT_TEST_FILE_SELECTION = {
980
1051
  ]
981
1052
  };
982
1053
  const project_preparation_toPosix = (value)=>value.split(external_node_path_.sep).join('/');
1054
+ const discoverSelectedTestFiles = (projectRoot, paths)=>{
1055
+ const root = (0, external_node_path_.resolve)(projectRoot);
1056
+ const files = new Set();
1057
+ for (const requestedPath of paths){
1058
+ if ('string' != typeof requestedPath || 0 === requestedPath.trim().length) throw new TypeError('Test selection paths must be non-empty strings.');
1059
+ const absolutePath = (0, external_node_path_.resolve)(root, requestedPath);
1060
+ const relativePath = (0, external_node_path_.relative)(root, absolutePath);
1061
+ if ('..' === relativePath || relativePath.startsWith(`..${external_node_path_.sep}`) || (0, external_node_path_.isAbsolute)(relativePath)) throw new Error(`Test selection path is outside the project: ${requestedPath}`);
1062
+ if (!(0, external_node_fs_.existsSync)(absolutePath)) throw new Error(`Test selection path does not exist: ${requestedPath}`);
1063
+ if ((0, external_node_fs_.statSync)(absolutePath).isDirectory()) for (const file of discoverTestFiles(absolutePath))files.add(file);
1064
+ else if (/\.ya?ml$/i.test(absolutePath)) files.add(absolutePath);
1065
+ else throw new Error(`Test selection file must be YAML: ${requestedPath}`);
1066
+ }
1067
+ return [
1068
+ ...files
1069
+ ].sort((a, b)=>{
1070
+ const relativeA = project_preparation_toPosix((0, external_node_path_.relative)(root, a));
1071
+ const relativeB = project_preparation_toPosix((0, external_node_path_.relative)(root, b));
1072
+ return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
1073
+ });
1074
+ };
983
1075
  const discoverResolvedTestFiles = (projectRoot, selection)=>{
984
1076
  const root = (0, external_node_path_.resolve)(projectRoot);
985
1077
  const match = (patterns)=>globSync(patterns, {
@@ -1004,6 +1096,11 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
1004
1096
  return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
1005
1097
  });
1006
1098
  };
1099
+ const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)=>{
1100
+ const normalized = validateTestFileSelection(selection);
1101
+ if (!normalized) throw new TypeError('Test file selection is required.');
1102
+ return discoverResolvedTestFiles(projectRoot, normalized);
1103
+ };
1007
1104
  const discoverTestConfig = (projectRoot)=>{
1008
1105
  const root = (0, external_node_path_.resolve)(projectRoot);
1009
1106
  const candidates = (0, external_node_fs_.readdirSync)(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
@@ -1034,8 +1131,14 @@ const matchesTags = (tags, selection)=>{
1034
1131
  if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
1035
1132
  return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
1036
1133
  };
1037
- const filterDocumentCases = (document, project)=>{
1038
- const cases = document.cases.filter((item)=>matchesTags(item.definition.tags ?? [], project.tags));
1134
+ const filterDocumentCases = (document, project, selection)=>{
1135
+ const cases = document.cases.filter((item)=>{
1136
+ const tags = item.definition.tags ?? [];
1137
+ return matchesTags(tags, project.tags) && matchesTags(tags, {
1138
+ include: selection?.tags?.include ?? [],
1139
+ exclude: selection?.tags?.exclude ?? []
1140
+ }) && (!selection?.caseIds?.length || selection.caseIds.includes(item.caseId));
1141
+ });
1039
1142
  const filtered = document.cases.length - cases.length;
1040
1143
  return 0 === cases.length ? {
1041
1144
  filtered
@@ -1060,7 +1163,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1060
1163
  const platform = options.platform?.trim();
1061
1164
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
1062
1165
  const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_.resolve)(options.prerequisiteFile) : void 0;
1063
- const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
1166
+ const projectFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
1167
+ const selectedFiles = options.selection?.paths?.length ? new Set(discoverSelectedTestFiles(projectRoot, options.selection.paths)) : void 0;
1168
+ const mainFiles = selectedFiles ? projectFiles.filter((file)=>selectedFiles.has(file)) : projectFiles;
1064
1169
  const files = [
1065
1170
  ...prerequisiteFile ? [
1066
1171
  prerequisiteFile
@@ -1083,6 +1188,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1083
1188
  const invocations = [];
1084
1189
  const plans = [];
1085
1190
  let filteredCaseCount = 0;
1191
+ const availableCaseIds = new Set();
1086
1192
  if (0 === sources.length) {
1087
1193
  const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
1088
1194
  projectName: project.name,
@@ -1113,10 +1219,16 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1113
1219
  variables: project.variables,
1114
1220
  env: process.env
1115
1221
  });
1222
+ const duplicate = document.cases.find((item)=>availableCaseIds.has(item.caseId));
1223
+ if (duplicate) throw new WorkflowParseError(`Case id collision in project "${project.name}": ${duplicate.caseId}.`, {
1224
+ caseId: duplicate.caseId,
1225
+ projectName: project.name
1226
+ });
1227
+ for (const item of document.cases)availableCaseIds.add(item.caseId);
1116
1228
  const filtered = source.absolutePath === prerequisiteFile ? {
1117
1229
  document,
1118
1230
  filtered: 0
1119
- } : filterDocumentCases(document, project);
1231
+ } : filterDocumentCases(document, project, options.selection);
1120
1232
  const plan = buildTestRunReportPlan(document, {
1121
1233
  selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
1122
1234
  defaultTimeoutMs,
@@ -1164,9 +1276,42 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1164
1276
  plans,
1165
1277
  collectionErrors,
1166
1278
  selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
1167
- filteredCaseCount
1279
+ filteredCaseCount,
1280
+ availableCaseIds: [
1281
+ ...availableCaseIds
1282
+ ]
1168
1283
  };
1169
1284
  }
1285
+ const validateUniqueStrings = (values, label)=>{
1286
+ if (void 0 === values) return;
1287
+ 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.`);
1288
+ if (new Set(values).size !== values.length) throw new TypeError(`${label} must not contain duplicates.`);
1289
+ return values;
1290
+ };
1291
+ const validateRunSelection = (options)=>{
1292
+ const paths = validateUniqueStrings(options.paths, 'Test selection paths');
1293
+ const caseIds = validateUniqueStrings(options.caseIds, 'Test selection caseIds');
1294
+ const include = validateUniqueStrings(options.tags?.include, 'Test selection tags.include');
1295
+ const exclude = validateUniqueStrings(options.tags?.exclude, 'Test selection tags.exclude');
1296
+ return {
1297
+ ...paths ? {
1298
+ paths
1299
+ } : {},
1300
+ ...caseIds ? {
1301
+ caseIds
1302
+ } : {},
1303
+ ...include || exclude ? {
1304
+ tags: {
1305
+ ...include ? {
1306
+ include
1307
+ } : {},
1308
+ ...exclude ? {
1309
+ exclude
1310
+ } : {}
1311
+ }
1312
+ } : {}
1313
+ };
1314
+ };
1170
1315
  const padDatePart = (value)=>String(value).padStart(2, '0');
1171
1316
  const createTestRunId = (date = new Date(), uuid = randomUUID())=>{
1172
1317
  const timestamp = [
@@ -1204,6 +1349,7 @@ async function prepareTestRun(options) {
1204
1349
  const startedAt = new Date();
1205
1350
  const runId = createTestRunId(startedAt);
1206
1351
  const input = resolveRunInput(options);
1352
+ const selection = validateRunSelection(options);
1207
1353
  const configPath = options.configPath ? (0, external_node_path_.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
1208
1354
  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}\`.`);
1209
1355
  if (options.configPath && !(0, external_node_fs_.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
@@ -1217,11 +1363,20 @@ async function prepareTestRun(options) {
1217
1363
  });
1218
1364
  (0, external_node_fs_.mkdirSync)(runDir);
1219
1365
  const projects = [];
1220
- for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
1221
- files: [
1222
- input.singleFile
1223
- ]
1224
- } : void 0));
1366
+ for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
1367
+ selection,
1368
+ ...input.singleFile ? {
1369
+ files: [
1370
+ input.singleFile
1371
+ ]
1372
+ } : {}
1373
+ }));
1374
+ if (selection.caseIds?.length) {
1375
+ const availableCaseIds = new Set(projects.flatMap((project)=>project.availableCaseIds ?? []));
1376
+ const unknown = selection.caseIds.find((id)=>!availableCaseIds.has(id));
1377
+ if (unknown) throw new Error(`Unknown Midscene case id: ${unknown}`);
1378
+ }
1379
+ 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.');
1225
1380
  const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
1226
1381
  return {
1227
1382
  startedAt,
@@ -1317,20 +1472,1051 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
1317
1472
  }));
1318
1473
  return output;
1319
1474
  }
1320
- const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
1475
+ const asNotRun = (documentId, collectedCase, projectName, reason)=>({
1476
+ documentId,
1477
+ caseId: collectedCase.caseId,
1478
+ projectName,
1479
+ name: collectedCase.definition.name,
1480
+ sourcePath: collectedCase.sourcePath,
1481
+ caseIndex: collectedCase.caseIndex,
1482
+ status: 'not-run',
1483
+ notRunReason: reason
1484
+ });
1321
1485
  const formatStep = (info)=>{
1322
- const position = stepPosition(info);
1486
+ const position = 'case' === info.scope ? info.case : info.document;
1323
1487
  const phase = 'steps' === position.phase ? 'step' : position.phase;
1324
1488
  return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
1325
1489
  };
1326
- const formatStepResult = (info, result)=>{
1327
- const indent = 'case' === info.scope ? ' ' : ' ';
1490
+ const formatStepResult = (info, result, indent = '')=>{
1328
1491
  if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
1329
1492
  const symbol = 'success' === result.status ? '✓' : '✗';
1330
1493
  const error = result.error ? ` — ${result.error.message}` : '';
1331
1494
  const continuation = result.continuedAfterError ? '; continuing' : '';
1332
1495
  return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
1333
1496
  };
1497
+ const latestById = (items, id)=>[
1498
+ ...new Map(items.map((item)=>[
1499
+ id(item),
1500
+ item
1501
+ ])).values()
1502
+ ];
1503
+ const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
1504
+ const { project } = prepared;
1505
+ 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';
1506
+ return {
1507
+ projectId: project.projectId,
1508
+ name: project.name,
1509
+ ...prepared.platform ? {
1510
+ platform: prepared.platform
1511
+ } : {},
1512
+ status: projectFailed ? 'failed' : 'success',
1513
+ retry: project.retry,
1514
+ fileSelection: prepared.fileSelection,
1515
+ tagSelection: project.tags,
1516
+ sourceCount: prepared.sources.length,
1517
+ selectedCaseCount: prepared.selectedCaseCount,
1518
+ filteredCaseCount: prepared.filteredCaseCount,
1519
+ ...lifecycle ? {
1520
+ lifecycle
1521
+ } : {},
1522
+ cases,
1523
+ plans: prepared.plans,
1524
+ documents,
1525
+ collectionErrors: prepared.collectionErrors
1526
+ };
1527
+ };
1528
+ const json = z.lazy(()=>z.union([
1529
+ z["null"](),
1530
+ z.string(),
1531
+ z.number().finite(),
1532
+ z.boolean(),
1533
+ z.array(json),
1534
+ z.record(z.string(), json)
1535
+ ]));
1536
+ const time = z.string().refine((value)=>Number.isFinite(Date.parse(value)));
1537
+ const executor_runtime_schema_count = z.number().int().nonnegative();
1538
+ const executor_runtime_schema_value = z.strictObject({
1539
+ value: json,
1540
+ truncatedPaths: z.array(z.string()).optional(),
1541
+ redactedPaths: z.array(z.string()).optional()
1542
+ });
1543
+ const reportErrorSchema = z.lazy(()=>z.strictObject({
1544
+ format: z.literal('midscene-error').optional(),
1545
+ name: z.string(),
1546
+ message: z.string(),
1547
+ code: z.string().optional(),
1548
+ details: executor_runtime_schema_value.optional(),
1549
+ cause: reportErrorSchema.optional(),
1550
+ errors: z.array(reportErrorSchema).optional(),
1551
+ truncated: z.boolean().optional()
1552
+ }));
1553
+ const executor_runtime_schema_operation = z.strictObject({
1554
+ operationId: z.string().min(1),
1555
+ scopeId: z.string().optional(),
1556
+ stepId: z.string().optional(),
1557
+ node: z.string().optional()
1558
+ });
1559
+ const cleanupSchema = z.strictObject({
1560
+ status: z["enum"]([
1561
+ 'not-required',
1562
+ 'completed',
1563
+ 'pending',
1564
+ 'failed'
1565
+ ]),
1566
+ capturedAt: time,
1567
+ operationId: z.string().optional(),
1568
+ blockingOperations: z.array(executor_runtime_schema_operation).optional(),
1569
+ error: reportErrorSchema.optional()
1570
+ });
1571
+ const pendingOperationsSchema = z.array(executor_runtime_schema_operation);
1572
+ const collectionStatus = z["enum"]([
1573
+ 'complete',
1574
+ 'partial',
1575
+ 'unknown'
1576
+ ]);
1577
+ const metricsSchema = z.strictObject({
1578
+ modelCallCount: executor_runtime_schema_count,
1579
+ modelTimeMs: z.number().finite().nonnegative(),
1580
+ promptTokens: executor_runtime_schema_count,
1581
+ cachedInputTokens: executor_runtime_schema_count,
1582
+ completionTokens: executor_runtime_schema_count,
1583
+ totalTokens: executor_runtime_schema_count,
1584
+ collection: z.strictObject({
1585
+ status: collectionStatus,
1586
+ expectedExecutionCount: executor_runtime_schema_count,
1587
+ capturedExecutionCount: executor_runtime_schema_count,
1588
+ reasons: z.array(z.string()),
1589
+ fields: z.strictObject({
1590
+ modelCallCount: collectionStatus,
1591
+ modelTimeMs: collectionStatus,
1592
+ totalTokens: collectionStatus
1593
+ }).optional()
1594
+ }).optional()
1595
+ });
1596
+ const operationsSchema = z.strictObject({
1597
+ capturedAt: time,
1598
+ events: z.array(z.strictObject({
1599
+ operationId: z.string().min(1),
1600
+ operationKind: z["enum"]([
1601
+ 'node',
1602
+ 'resource',
1603
+ 'acquisition',
1604
+ 'cleanup',
1605
+ 'readiness'
1606
+ ]),
1607
+ kind: z["enum"]([
1608
+ 'started',
1609
+ 'cancel-requested',
1610
+ 'settled',
1611
+ 'runner-returned',
1612
+ 'cleanup-deferred'
1613
+ ]),
1614
+ at: time,
1615
+ stepId: z.string().optional(),
1616
+ scopeId: z.string().optional(),
1617
+ node: z.string().optional(),
1618
+ status: z["enum"]([
1619
+ 'success',
1620
+ 'failed',
1621
+ 'not-run'
1622
+ ]).optional(),
1623
+ pending: z.boolean().optional(),
1624
+ error: reportErrorSchema.optional(),
1625
+ blockingOperations: z.array(executor_runtime_schema_operation).optional()
1626
+ }))
1627
+ });
1628
+ const runtimeResultFields = {
1629
+ executionStatus: z["enum"]([
1630
+ 'success',
1631
+ 'failed',
1632
+ 'not-run'
1633
+ ]).optional(),
1634
+ phases: z.partialRecord(z["enum"]([
1635
+ 'beforeAll',
1636
+ 'beforeEach',
1637
+ 'steps',
1638
+ 'afterEach',
1639
+ 'afterAll'
1640
+ ]), z.strictObject({
1641
+ status: z["enum"]([
1642
+ 'success',
1643
+ 'failed',
1644
+ 'not-run',
1645
+ 'not-required'
1646
+ ]),
1647
+ plannedStepCount: executor_runtime_schema_count,
1648
+ executedStepCount: executor_runtime_schema_count
1649
+ })).optional(),
1650
+ cleanup: cleanupSchema.optional(),
1651
+ pendingOperations: pendingOperationsSchema.optional(),
1652
+ retryDecision: z.strictObject({
1653
+ scope: z["enum"]([
1654
+ 'case',
1655
+ 'document'
1656
+ ]),
1657
+ retryCount: executor_runtime_schema_count,
1658
+ attemptIndex: executor_runtime_schema_count,
1659
+ decision: z["enum"]([
1660
+ 'retry',
1661
+ 'stop'
1662
+ ]),
1663
+ reason: z["enum"]([
1664
+ 'passed',
1665
+ 'attempt-failed',
1666
+ 'budget-exhausted',
1667
+ 'cleanup-pending',
1668
+ 'cleanup-failed',
1669
+ 'operation-pending',
1670
+ 'fatal-error',
1671
+ 'interrupted',
1672
+ 'bail',
1673
+ 'infrastructure-error'
1674
+ ])
1675
+ }).optional()
1676
+ };
1677
+ const plannedStep = z.strictObject({
1678
+ id: z.string(),
1679
+ phase: z["enum"]([
1680
+ 'beforeAll',
1681
+ 'beforeEach',
1682
+ 'steps',
1683
+ 'afterEach',
1684
+ 'afterAll'
1685
+ ]),
1686
+ stepIndex: executor_runtime_schema_count,
1687
+ node: z.string(),
1688
+ title: z.string().optional(),
1689
+ input: executor_runtime_schema_value,
1690
+ inputMode: z["enum"]([
1691
+ 'template',
1692
+ 'resolved'
1693
+ ]),
1694
+ timeoutMs: z.number().positive().optional(),
1695
+ continueOnError: z.boolean()
1696
+ });
1697
+ const documentPlanSchema = z.strictObject({
1698
+ documentId: z.string(),
1699
+ sourcePath: z.string(),
1700
+ sourceYaml: executor_runtime_schema_value.optional(),
1701
+ retry: z.strictObject({
1702
+ scope: z["enum"]([
1703
+ 'case',
1704
+ 'document'
1705
+ ]),
1706
+ count: executor_runtime_schema_count
1707
+ }),
1708
+ beforeAll: z.array(plannedStep),
1709
+ afterAll: z.array(plannedStep),
1710
+ cases: z.array(z.strictObject({
1711
+ caseId: z.string(),
1712
+ name: z.string(),
1713
+ caseIndex: executor_runtime_schema_count,
1714
+ selected: z.boolean(),
1715
+ tags: z.array(z.string()),
1716
+ onFailure: z["enum"]([
1717
+ 'continue',
1718
+ 'stop-document'
1719
+ ]).optional(),
1720
+ beforeEach: z.array(plannedStep),
1721
+ steps: z.array(plannedStep),
1722
+ afterEach: z.array(plannedStep)
1723
+ }))
1724
+ });
1725
+ function test_executor_define_property(obj, key, value) {
1726
+ if (key in obj) Object.defineProperty(obj, key, {
1727
+ value: value,
1728
+ enumerable: true,
1729
+ configurable: true,
1730
+ writable: true
1731
+ });
1732
+ else obj[key] = value;
1733
+ return obj;
1734
+ }
1735
+ class TestExecutorError extends Error {
1736
+ constructor(message, options = {}){
1737
+ super(message, {
1738
+ cause: options.cause
1739
+ }), test_executor_define_property(this, "kind", void 0), test_executor_define_property(this, "retryable", void 0);
1740
+ this.name = 'TestExecutorError';
1741
+ this.kind = options.kind ?? 'unknown';
1742
+ this.retryable = options.retryable ?? false;
1743
+ }
1744
+ }
1745
+ const localTestExecutor = Object.freeze({
1746
+ name: 'local',
1747
+ execute: (_task, context)=>context.runLocal()
1748
+ });
1749
+ const jsonValueSchema = z.lazy(()=>z.union([
1750
+ z["null"](),
1751
+ z.string(),
1752
+ z.number().finite(),
1753
+ z.boolean(),
1754
+ z.array(jsonValueSchema),
1755
+ z.record(z.string(), jsonValueSchema)
1756
+ ]));
1757
+ const timestampSchema = z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
1758
+ message: 'Expected an ISO-compatible timestamp.'
1759
+ });
1760
+ const errorSchema = z.strictObject({
1761
+ format: z.literal('midscene-error').optional(),
1762
+ cause: reportErrorSchema.optional(),
1763
+ name: z.string().min(1),
1764
+ message: z.string(),
1765
+ code: z.string().min(1),
1766
+ details: jsonValueSchema.optional()
1767
+ });
1768
+ const stepBaseSchema = z.strictObject({
1769
+ phase: z["enum"]([
1770
+ 'beforeAll',
1771
+ 'beforeEach',
1772
+ 'steps',
1773
+ 'afterEach',
1774
+ 'afterAll'
1775
+ ]),
1776
+ stepIndex: z.number().int().nonnegative(),
1777
+ node: z.string().min(1),
1778
+ input: jsonValueSchema,
1779
+ meta: z.strictObject({
1780
+ timeoutMs: z.number().positive().finite().optional(),
1781
+ continueOnError: z.boolean()
1782
+ }),
1783
+ continuedAfterError: z.boolean(),
1784
+ waitForPreviousOperationMs: z.number().nonnegative().finite().optional(),
1785
+ operations: operationsSchema.optional(),
1786
+ output: z.strictObject({
1787
+ summary: z.string().optional(),
1788
+ data: jsonValueSchema.optional()
1789
+ }).optional(),
1790
+ error: errorSchema.optional(),
1791
+ report: z.strictObject({
1792
+ traces: z.array(z.strictObject({
1793
+ type: z.literal('midscene-execution'),
1794
+ executionId: z.string().min(1),
1795
+ metrics: metricsSchema.optional()
1796
+ })),
1797
+ collectionErrors: z.array(errorSchema).optional()
1798
+ }).optional()
1799
+ });
1800
+ const stepSchema = z.discriminatedUnion('status', [
1801
+ stepBaseSchema.extend({
1802
+ status: z["enum"]([
1803
+ 'success',
1804
+ 'failed'
1805
+ ]),
1806
+ startedAt: timestampSchema,
1807
+ endedAt: timestampSchema,
1808
+ durationMs: z.number().nonnegative().finite()
1809
+ }),
1810
+ stepBaseSchema.extend({
1811
+ status: z.literal('not-run'),
1812
+ notRunReason: z.string().min(1),
1813
+ blockedBy: z.string().optional()
1814
+ })
1815
+ ]);
1816
+ const validatedStepSchema = stepSchema.superRefine((step, context)=>{
1817
+ if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
1818
+ code: 'custom',
1819
+ message: 'Successful steps cannot contain failure evidence.'
1820
+ });
1821
+ if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
1822
+ code: 'custom',
1823
+ message: 'Continuation requires continueOnError.'
1824
+ });
1825
+ });
1826
+ const attemptSchema = z.strictObject({
1827
+ ...runtimeResultFields,
1828
+ caseId: z.string().min(1),
1829
+ runId: z.string().min(1),
1830
+ projectName: z.string().min(1),
1831
+ attemptIndex: z.number().int().nonnegative(),
1832
+ name: z.string(),
1833
+ sourcePath: z.string().min(1),
1834
+ caseIndex: z.number().int().nonnegative(),
1835
+ status: z["enum"]([
1836
+ 'success',
1837
+ 'failed'
1838
+ ]),
1839
+ beforeEach: z.array(validatedStepSchema),
1840
+ steps: z.array(validatedStepSchema),
1841
+ afterEach: z.array(validatedStepSchema),
1842
+ executionErrors: z.array(errorSchema).optional(),
1843
+ teardownErrors: z.array(errorSchema).optional(),
1844
+ reportRefs: z.array(z.string().min(1)).optional(),
1845
+ startedAt: timestampSchema,
1846
+ endedAt: timestampSchema,
1847
+ durationMs: z.number().nonnegative().finite()
1848
+ });
1849
+ const notRunReasonSchema = z["enum"]([
1850
+ 'document-start-failed',
1851
+ 'project-preflight-failed',
1852
+ 'project-setup-failed',
1853
+ 'interrupted',
1854
+ 'bail',
1855
+ 'fatal-error',
1856
+ 'executor-failed',
1857
+ 'cleanup-pending',
1858
+ 'cleanup-failed',
1859
+ 'operation-pending'
1860
+ ]);
1861
+ const resultSchema = z.strictObject({
1862
+ case: z.strictObject({
1863
+ documentId: z.string().min(1),
1864
+ caseId: z.string().min(1),
1865
+ projectName: z.string().min(1),
1866
+ name: z.string(),
1867
+ sourcePath: z.string().min(1),
1868
+ caseIndex: z.number().int().nonnegative(),
1869
+ status: z["enum"]([
1870
+ 'success',
1871
+ 'failed',
1872
+ 'not-run'
1873
+ ]),
1874
+ run: attemptSchema.optional(),
1875
+ attempts: z.array(attemptSchema).optional(),
1876
+ notRunReason: notRunReasonSchema.optional()
1877
+ }),
1878
+ document: z.strictObject({
1879
+ ...runtimeResultFields,
1880
+ plan: documentPlanSchema.optional(),
1881
+ attemptIndex: z.number().int().nonnegative().optional(),
1882
+ hostErrors: z.array(z.strictObject({
1883
+ phase: z["enum"]([
1884
+ 'setup',
1885
+ 'execution',
1886
+ 'cleanup',
1887
+ 'report',
1888
+ 'publication',
1889
+ 'observer'
1890
+ ]),
1891
+ error: reportErrorSchema
1892
+ })).optional(),
1893
+ documentId: z.string().min(1),
1894
+ documentRunId: z.string().min(1),
1895
+ projectId: z.string().min(1),
1896
+ projectName: z.string().min(1),
1897
+ sourcePath: z.string().min(1),
1898
+ status: z["enum"]([
1899
+ 'success',
1900
+ 'failed'
1901
+ ]),
1902
+ startedAt: timestampSchema,
1903
+ endedAt: timestampSchema,
1904
+ durationMs: z.number().nonnegative().finite(),
1905
+ beforeAll: z.array(validatedStepSchema),
1906
+ afterAll: z.array(validatedStepSchema),
1907
+ executionErrors: z.array(errorSchema).optional(),
1908
+ teardownErrors: z.array(errorSchema).optional(),
1909
+ reportRefs: z.array(z.string().min(1)).optional()
1910
+ }).optional(),
1911
+ lifecycle: z.strictObject({
1912
+ cleanup: cleanupSchema.optional(),
1913
+ pendingOperations: pendingOperationsSchema.optional(),
1914
+ projectName: z.string().min(1),
1915
+ status: z["enum"]([
1916
+ 'success',
1917
+ 'failed'
1918
+ ]),
1919
+ setupError: errorSchema.optional(),
1920
+ teardownErrors: z.array(errorSchema).optional(),
1921
+ startedAt: timestampSchema,
1922
+ endedAt: timestampSchema,
1923
+ durationMs: z.number().nonnegative().finite()
1924
+ }).optional(),
1925
+ artifacts: z.array(z.strictObject({
1926
+ name: z.string().min(1),
1927
+ uri: z.string().min(1),
1928
+ mediaType: z.string().min(1).optional()
1929
+ })).optional(),
1930
+ metadata: z.record(z.string(), z.union([
1931
+ z.string(),
1932
+ z.number().finite(),
1933
+ z.boolean(),
1934
+ z["null"]()
1935
+ ])).optional()
1936
+ });
1937
+ 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);
1938
+ const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
1939
+ 'completed',
1940
+ 'not-required'
1941
+ ].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
1942
+ function assertTestCaseTaskRunResult(task, value) {
1943
+ const parsed = resultSchema.safeParse(value);
1944
+ if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${z.prettifyError(parsed.error)}`);
1945
+ const outcome = parsed.data.case;
1946
+ 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}.`);
1947
+ const attempts = outcome.attempts ?? [];
1948
+ for (const attempt of attempts){
1949
+ const steps = [
1950
+ ...attempt.beforeEach,
1951
+ ...attempt.steps,
1952
+ ...attempt.afterEach
1953
+ ];
1954
+ 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}.`);
1955
+ }
1956
+ if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
1957
+ if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
1958
+ 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}.`);
1959
+ const finalAttempt = attempts.at(-1);
1960
+ if ('not-run' === outcome.status) {
1961
+ if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
1962
+ } 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}.`);
1963
+ const reportRefs = [
1964
+ ...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
1965
+ ...parsed.data.document?.reportRefs ?? []
1966
+ ];
1967
+ if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
1968
+ const document = parsed.data.document;
1969
+ if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
1970
+ ...document.beforeAll,
1971
+ ...document.afterAll
1972
+ ].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
1973
+ const lifecycle = parsed.data.lifecycle;
1974
+ 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}.`);
1975
+ 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}.`);
1976
+ if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
1977
+ }
1978
+ function createExecutorReports(outputDir) {
1979
+ const paths = new Map();
1980
+ const sources = new Map();
1981
+ return {
1982
+ materializeSource (source) {
1983
+ if (!source.dumpPath) {
1984
+ const reference = this.materialize(source.sourcePath);
1985
+ sources.set(reference, {
1986
+ sourcePath: this.resolve(reference),
1987
+ collection: source.collection
1988
+ });
1989
+ return reference;
1990
+ }
1991
+ const dump = (0, external_node_fs_.realpathSync)(source.dumpPath);
1992
+ const reference = randomUUID();
1993
+ const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
1994
+ (0, external_node_fs_.mkdirSync)(destination, {
1995
+ recursive: true
1996
+ });
1997
+ const dumpPath = (0, external_node_path_.join)(destination, 'snapshot.json');
1998
+ (0, external_node_fs_.cpSync)(dump, dumpPath);
1999
+ const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(dump), 'screenshots');
2000
+ if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
2001
+ recursive: true,
2002
+ dereference: true
2003
+ });
2004
+ const sourcePath = (0, external_node_path_.join)(destination, 'index.html');
2005
+ sources.set(reference, {
2006
+ sourcePath,
2007
+ dumpPath,
2008
+ collection: source.collection
2009
+ });
2010
+ paths.set(reference, sourcePath);
2011
+ return reference;
2012
+ },
2013
+ resolveSource (reference) {
2014
+ return sources.get(reference);
2015
+ },
2016
+ materialize (sourcePath) {
2017
+ const source = (0, external_node_fs_.realpathSync)(sourcePath);
2018
+ const entry = (0, external_node_fs_.statSync)(source).isDirectory() ? (0, external_node_path_.join)(source, 'index.html') : source;
2019
+ if (!(0, external_node_fs_.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
2020
+ kind: 'report'
2021
+ });
2022
+ const reference = randomUUID();
2023
+ const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
2024
+ (0, external_node_fs_.mkdirSync)(destination, {
2025
+ recursive: true
2026
+ });
2027
+ const isDirectoryReport = 'index.html' === (0, external_node_path_.basename)(entry);
2028
+ const reportPath = (0, external_node_path_.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
2029
+ (0, external_node_fs_.cpSync)(isDirectoryReport ? (0, external_node_path_.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
2030
+ recursive: isDirectoryReport,
2031
+ dereference: true
2032
+ });
2033
+ if (!isDirectoryReport) {
2034
+ const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(entry), 'screenshots');
2035
+ if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
2036
+ recursive: true,
2037
+ dereference: true
2038
+ });
2039
+ }
2040
+ paths.set(reference, reportPath);
2041
+ return reference;
2042
+ },
2043
+ resolve (reference) {
2044
+ const reportPath = paths.get(reference);
2045
+ if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
2046
+ kind: 'report'
2047
+ });
2048
+ return reportPath;
2049
+ }
2050
+ };
2051
+ }
2052
+ async function runTaskPool(options) {
2053
+ if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
2054
+ const pending = options.tasks.map((_, index)=>index);
2055
+ const results = new Array(options.tasks.length);
2056
+ const activeResources = new Set();
2057
+ let active = 0;
2058
+ let firstError;
2059
+ let hasError = false;
2060
+ return await new Promise((resolve, reject)=>{
2061
+ let settled = false;
2062
+ const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
2063
+ const finish = ()=>{
2064
+ if (settled) return;
2065
+ settled = true;
2066
+ options.signal?.removeEventListener('abort', pump);
2067
+ if (hasError) reject(firstError);
2068
+ else resolve(results);
2069
+ };
2070
+ const release = (resources)=>{
2071
+ for (const resource of resources)activeResources.delete(resource);
2072
+ };
2073
+ const claimRunnable = ()=>{
2074
+ const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
2075
+ if (pendingIndex < 0) return;
2076
+ const [taskIndex] = pending.splice(pendingIndex, 1);
2077
+ return taskIndex;
2078
+ };
2079
+ function pump() {
2080
+ if (settled) return;
2081
+ while(active < options.maxConcurrency && !stopRequested()){
2082
+ const taskIndex = claimRunnable();
2083
+ if (void 0 === taskIndex) break;
2084
+ const task = options.tasks[taskIndex];
2085
+ for (const resource of task.resources)activeResources.add(resource);
2086
+ active += 1;
2087
+ Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
2088
+ results[taskIndex] = result;
2089
+ }).catch((error)=>{
2090
+ if (!hasError) {
2091
+ hasError = true;
2092
+ firstError = error;
2093
+ }
2094
+ }).finally(()=>{
2095
+ release(task.resources);
2096
+ active -= 1;
2097
+ pump();
2098
+ });
2099
+ }
2100
+ if (0 === active && (0 === pending.length || stopRequested())) finish();
2101
+ }
2102
+ options.signal?.addEventListener('abort', pump, {
2103
+ once: true
2104
+ });
2105
+ pump();
2106
+ });
2107
+ }
2108
+ const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
2109
+ const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
2110
+ const documentId = isolatedDocumentId(document, collectedCase);
2111
+ return {
2112
+ projectIndex,
2113
+ prepared,
2114
+ document,
2115
+ collectedCase,
2116
+ task: Object.freeze({
2117
+ taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
2118
+ projectId: prepared.project.projectId,
2119
+ projectName: prepared.project.name,
2120
+ documentId,
2121
+ sourcePath: collectedCase.sourcePath,
2122
+ caseId: collectedCase.caseId,
2123
+ caseName: collectedCase.definition.name,
2124
+ caseIndex: collectedCase.caseIndex,
2125
+ tags: Object.freeze([
2126
+ ...collectedCase.definition.tags ?? []
2127
+ ]),
2128
+ resources: Object.freeze([
2129
+ ...collectedCase.definition.resources ?? []
2130
+ ]),
2131
+ retry: prepared.project.retry
2132
+ })
2133
+ };
2134
+ })));
2135
+ const reviveError = (error)=>{
2136
+ const revived = new WorkflowError(error.message, {
2137
+ code: error.code,
2138
+ details: error.details,
2139
+ cause: error.cause
2140
+ });
2141
+ revived.name = error.name;
2142
+ return revived;
2143
+ };
2144
+ const reviveStep = (step)=>{
2145
+ const { error, output, report, ...base } = step;
2146
+ return {
2147
+ ...base,
2148
+ input: step.input,
2149
+ meta: {
2150
+ ...step.meta
2151
+ },
2152
+ ...output ? {
2153
+ output: {
2154
+ ...output
2155
+ }
2156
+ } : {},
2157
+ ...error ? {
2158
+ error: reviveError(error)
2159
+ } : {},
2160
+ ...report ? {
2161
+ report: {
2162
+ traces: report.traces.map((trace)=>({
2163
+ ...trace
2164
+ })),
2165
+ ...report.collectionErrors ? {
2166
+ collectionErrors: report.collectionErrors.map(reviveError)
2167
+ } : {}
2168
+ }
2169
+ } : {}
2170
+ };
2171
+ };
2172
+ const reviveAttempt = (attempt)=>{
2173
+ const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
2174
+ return {
2175
+ ...base,
2176
+ beforeEach: beforeEach.map(reviveStep),
2177
+ steps: steps.map(reviveStep),
2178
+ afterEach: afterEach.map(reviveStep),
2179
+ ...executionErrors ? {
2180
+ executionErrors: executionErrors.map(reviveError)
2181
+ } : {},
2182
+ ...teardownErrors ? {
2183
+ teardownErrors: teardownErrors.map(reviveError)
2184
+ } : {}
2185
+ };
2186
+ };
2187
+ const reviveDocument = (document)=>{
2188
+ const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
2189
+ return {
2190
+ ...base,
2191
+ ...hostErrors ? {
2192
+ hostErrors: hostErrors.map((error)=>({
2193
+ ...error
2194
+ }))
2195
+ } : {},
2196
+ beforeAll: beforeAll.map(reviveStep),
2197
+ afterAll: afterAll.map(reviveStep),
2198
+ ...executionErrors ? {
2199
+ executionErrors: executionErrors.map(reviveError)
2200
+ } : {},
2201
+ ...teardownErrors ? {
2202
+ teardownErrors: teardownErrors.map(reviveError)
2203
+ } : {}
2204
+ };
2205
+ };
2206
+ const reviveLifecycle = (lifecycle)=>{
2207
+ const { setupError, teardownErrors, ...base } = lifecycle;
2208
+ return {
2209
+ ...base,
2210
+ ...setupError ? {
2211
+ setupError: reviveError(setupError)
2212
+ } : {},
2213
+ ...teardownErrors ? {
2214
+ teardownErrors: teardownErrors.map(reviveError)
2215
+ } : {}
2216
+ };
2217
+ };
2218
+ const toTransportResult = (result, materializeReport, materializeSource)=>{
2219
+ const transportReports = (scope)=>[
2220
+ ...(scope.reportSources ?? []).map(materializeSource),
2221
+ ...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
2222
+ ];
2223
+ const mapStep = (step)=>{
2224
+ if (!step.report) return step;
2225
+ const { sources: _sources, ...report } = step.report;
2226
+ return {
2227
+ ...step,
2228
+ report
2229
+ };
2230
+ };
2231
+ const mapAttempt = (attempt)=>{
2232
+ const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
2233
+ const reportRefs = transportReports(attempt);
2234
+ return {
2235
+ ...transportAttempt,
2236
+ beforeEach: attempt.beforeEach.map(mapStep),
2237
+ steps: attempt.steps.map(mapStep),
2238
+ afterEach: attempt.afterEach.map(mapStep),
2239
+ ...reportRefs.length ? {
2240
+ reportRefs
2241
+ } : {}
2242
+ };
2243
+ };
2244
+ const attempts = result.case.attempts?.map(mapAttempt);
2245
+ const run = attempts?.at(-1);
2246
+ const { run: _run, attempts: _attempts, ...transportCase } = result.case;
2247
+ const document = result.document ? (()=>{
2248
+ const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
2249
+ const reportRefs = transportReports(result.document);
2250
+ return {
2251
+ ...transportDocument,
2252
+ ...result.document.hostErrors ? {
2253
+ hostErrors: result.document.hostErrors.map(({ phase, error })=>({
2254
+ phase,
2255
+ error: sanitizeReportError(error)
2256
+ }))
2257
+ } : {},
2258
+ beforeAll: result.document.beforeAll.map(mapStep),
2259
+ afterAll: result.document.afterAll.map(mapStep),
2260
+ ...reportRefs.length ? {
2261
+ reportRefs
2262
+ } : {}
2263
+ };
2264
+ })() : void 0;
2265
+ return JSON.parse(JSON.stringify({
2266
+ case: {
2267
+ ...transportCase,
2268
+ ...run ? {
2269
+ run
2270
+ } : {},
2271
+ ...attempts ? {
2272
+ attempts
2273
+ } : {}
2274
+ },
2275
+ ...document ? {
2276
+ document
2277
+ } : {},
2278
+ ...result.lifecycle ? {
2279
+ lifecycle: result.lifecycle
2280
+ } : {}
2281
+ }));
2282
+ };
2283
+ const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
2284
+ const resolveReports = (refs)=>{
2285
+ const reportPaths = [];
2286
+ const reportSources = [];
2287
+ for (const ref of refs ?? []){
2288
+ const path = resolveReportReference(ref);
2289
+ const source = resolveSource(ref);
2290
+ if (source) reportSources.push(source);
2291
+ else reportPaths.push(path);
2292
+ }
2293
+ return {
2294
+ ...reportPaths.length ? {
2295
+ reportPaths
2296
+ } : {},
2297
+ ...reportSources.length ? {
2298
+ reportSources
2299
+ } : {}
2300
+ };
2301
+ };
2302
+ const attempts = result.case.attempts?.map((attempt)=>({
2303
+ ...reviveAttempt(attempt),
2304
+ ...resolveReports(attempt.reportRefs)
2305
+ }));
2306
+ const run = attempts?.at(-1);
2307
+ const { run: _run, attempts: _attempts, ...caseBase } = result.case;
2308
+ return {
2309
+ case: {
2310
+ ...caseBase,
2311
+ ...run ? {
2312
+ run
2313
+ } : {},
2314
+ ...attempts ? {
2315
+ attempts
2316
+ } : {}
2317
+ },
2318
+ ...result.document ? {
2319
+ document: {
2320
+ ...reviveDocument(result.document),
2321
+ ...resolveReports(result.document.reportRefs)
2322
+ }
2323
+ } : {},
2324
+ ...result.lifecycle ? {
2325
+ lifecycle: reviveLifecycle(result.lifecycle)
2326
+ } : {}
2327
+ };
2328
+ };
2329
+ const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
2330
+ const { prepared, document, collectedCase, task } = preparedTask;
2331
+ const { project } = prepared;
2332
+ const isolatedDocument = {
2333
+ ...document,
2334
+ documentId: task.documentId,
2335
+ cases: [
2336
+ collectedCase
2337
+ ]
2338
+ };
2339
+ const runtime = createProjectRuntime({
2340
+ project,
2341
+ setup: project.setup,
2342
+ signal: options.signal
2343
+ });
2344
+ let lifecycle;
2345
+ let execution;
2346
+ let executionError;
2347
+ try {
2348
+ await runtime.start();
2349
+ if (runtime.canRun) execution = await runWorkflowDocument(isolatedDocument, {
2350
+ resolveNode: project.nodes.require.bind(project.nodes),
2351
+ project,
2352
+ projectContext: runtime.context,
2353
+ retry: project.retry,
2354
+ signal: runtime.signal,
2355
+ defaultTimeoutMs: options.test.testTimeout,
2356
+ shouldStop: ()=>options.signal.aborted,
2357
+ stopReason: ()=>'interrupted',
2358
+ isFatalError: (run)=>[
2359
+ ...run.beforeEach,
2360
+ ...run.steps,
2361
+ ...run.afterEach
2362
+ ].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
2363
+ onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
2364
+ onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
2365
+ onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
2366
+ });
2367
+ } catch (error) {
2368
+ executionError = error;
2369
+ if (error instanceof WorkflowExecutionFailure) execution = error.result;
2370
+ } finally{
2371
+ const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
2372
+ lifecycle = await runtime.finish(failed ? 'failed' : 'success');
2373
+ }
2374
+ if (executionError) {
2375
+ if (!execution) throw executionError;
2376
+ options.onInfrastructureError(executionError);
2377
+ }
2378
+ if (!execution) return toTransportResult({
2379
+ case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
2380
+ lifecycle
2381
+ }, materializeReport, materializeSource);
2382
+ const outcome = execution.cases[0];
2383
+ if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
2384
+ return toTransportResult({
2385
+ case: {
2386
+ ...outcome,
2387
+ documentId: task.documentId
2388
+ },
2389
+ document: execution.document,
2390
+ lifecycle
2391
+ }, materializeReport, materializeSource);
2392
+ };
2393
+ const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
2394
+ const { prepared, task } = preparedTask;
2395
+ const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
2396
+ taskProgress('started');
2397
+ const outputDir = (0, external_node_path_.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
2398
+ (0, external_node_fs_.mkdirSync)(outputDir, {
2399
+ recursive: true
2400
+ });
2401
+ const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
2402
+ if (prepared.collectionErrors.length > 0) return {
2403
+ case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
2404
+ };
2405
+ const executor = options.executor ?? localTestExecutor;
2406
+ let result;
2407
+ let executorFailure;
2408
+ let executorAttempts = 0;
2409
+ for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
2410
+ executorAttempts = attemptIndex + 1;
2411
+ try {
2412
+ result = await executor.execute(task, {
2413
+ signal: options.signal,
2414
+ onProgress: taskProgress,
2415
+ outputDir,
2416
+ materializeReport,
2417
+ runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
2418
+ });
2419
+ executorFailure = void 0;
2420
+ break;
2421
+ } catch (error) {
2422
+ if (!(error instanceof TestExecutorError)) throw error;
2423
+ executorFailure = error;
2424
+ if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
2425
+ taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
2426
+ }
2427
+ }
2428
+ if (!result) {
2429
+ if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
2430
+ taskProgress(`executor-failed: ${executorFailure.message}`);
2431
+ return {
2432
+ case: {
2433
+ ...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
2434
+ execution: {
2435
+ executor: executor.name,
2436
+ resources: task.resources,
2437
+ attempts: executorAttempts,
2438
+ failure: {
2439
+ kind: executorFailure.kind,
2440
+ message: executorFailure.message,
2441
+ retryable: executorFailure.retryable
2442
+ }
2443
+ }
2444
+ }
2445
+ };
2446
+ }
2447
+ assertTestCaseTaskRunResult(task, result);
2448
+ const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
2449
+ const completed = {
2450
+ ...internalResult,
2451
+ case: {
2452
+ ...internalResult.case,
2453
+ execution: {
2454
+ executor: executor.name,
2455
+ resources: task.resources,
2456
+ attempts: executorAttempts,
2457
+ ...internalResult.lifecycle ? {
2458
+ lifecycle: internalResult.lifecycle
2459
+ } : {},
2460
+ ...result.artifacts ? {
2461
+ artifacts: result.artifacts
2462
+ } : {},
2463
+ ...result.metadata ? {
2464
+ metadata: result.metadata
2465
+ } : {}
2466
+ }
2467
+ }
2468
+ };
2469
+ onCompleted(completed);
2470
+ for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
2471
+ if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
2472
+ taskProgress(internalResult.case.status);
2473
+ return completed;
2474
+ };
2475
+ const runCaseExecution = async (options)=>{
2476
+ const tasks = prepareCaseTasks(options.projects);
2477
+ let failedCaseCount = 0;
2478
+ const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
2479
+ const results = new Array(tasks.length);
2480
+ if (tasks.length > 0) try {
2481
+ await runTaskPool({
2482
+ tasks: tasks.map((preparedTask)=>preparedTask.task),
2483
+ maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
2484
+ signal: options.signal,
2485
+ shouldStop: bailReached,
2486
+ run: async (_task, taskIndex)=>{
2487
+ try {
2488
+ const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
2489
+ results[taskIndex] = completed;
2490
+ });
2491
+ results[taskIndex] = result;
2492
+ if ('failed' === result.case.status) failedCaseCount += 1;
2493
+ return result;
2494
+ } catch (error) {
2495
+ options.onInfrastructureError(error);
2496
+ throw error;
2497
+ }
2498
+ }
2499
+ });
2500
+ } catch (error) {
2501
+ options.onInfrastructureError(error);
2502
+ }
2503
+ return options.projects.map((prepared, projectIndex)=>{
2504
+ const projectTasks = tasks.map((task, taskIndex)=>({
2505
+ task,
2506
+ result: results[taskIndex]
2507
+ })).filter(({ task })=>task.projectIndex === projectIndex);
2508
+ const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
2509
+ const cases = projectTasks.map(({ task, result })=>{
2510
+ if (result) return result.case;
2511
+ if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
2512
+ return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
2513
+ });
2514
+ const documents = projectTasks.flatMap(({ result })=>result?.document ? [
2515
+ result.document
2516
+ ] : []);
2517
+ return buildProjectResult(prepared, cases, documents);
2518
+ });
2519
+ };
1334
2520
  const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
1335
2521
  ...attempt.beforeEach,
1336
2522
  ...attempt.steps,
@@ -1376,7 +2562,7 @@ async function executeDocumentInvocation(options) {
1376
2562
  },
1377
2563
  onStepResult: async (info, result)=>{
1378
2564
  await invocation.bindings.onStepResult?.(info, result);
1379
- options.onProgress(formatStepResult(info, result));
2565
+ options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
1380
2566
  },
1381
2567
  onCaseResult: async (attempt)=>{
1382
2568
  await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
@@ -1494,16 +2680,6 @@ function createOperationJournal(runDir) {
1494
2680
  }
1495
2681
  };
1496
2682
  }
1497
- const asNotRun = (documentId, collectedCase, projectName, reason)=>({
1498
- documentId,
1499
- caseId: collectedCase.caseId,
1500
- projectName,
1501
- name: collectedCase.definition.name,
1502
- sourcePath: collectedCase.sourcePath,
1503
- caseIndex: collectedCase.caseIndex,
1504
- status: 'not-run',
1505
- notRunReason: reason
1506
- });
1507
2683
  const summarize = (projects)=>{
1508
2684
  const healthCases = projects.flatMap((project)=>{
1509
2685
  const documentAttemptByRunId = new Map(project.documents.map((document)=>[
@@ -1537,12 +2713,6 @@ const summarize = (projects)=>{
1537
2713
  projectFailures: projects.filter((project)=>'failed' === project.status).length
1538
2714
  };
1539
2715
  };
1540
- const latestById = (items, id)=>[
1541
- ...new Map(items.map((item)=>[
1542
- id(item),
1543
- item
1544
- ])).values()
1545
- ];
1546
2716
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
1547
2717
  async function runPreparedTestProject(plan) {
1548
2718
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
@@ -1555,7 +2725,7 @@ async function runPreparedTestProject(plan) {
1555
2725
  await writeTestRunPlan(runDir, preparedProjects);
1556
2726
  const journal = createOperationJournal(runDir);
1557
2727
  const rootController = new AbortController();
1558
- setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
2728
+ setMaxListeners(Math.max(10, ('case' === definition.test.executionUnit ? Math.min(definition.test.maxConcurrency, totalCases) : effectiveConcurrency) + 1), rootController.signal);
1559
2729
  const handleSignal = (signal)=>{
1560
2730
  rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
1561
2731
  };
@@ -1597,31 +2767,6 @@ async function runPreparedTestProject(plan) {
1597
2767
  const { project } = prepared;
1598
2768
  runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
1599
2769
  };
1600
- const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
1601
- const { project } = prepared;
1602
- 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';
1603
- return {
1604
- projectId: project.projectId,
1605
- name: project.name,
1606
- ...prepared.platform ? {
1607
- platform: prepared.platform
1608
- } : {},
1609
- status: projectFailed ? 'failed' : 'success',
1610
- retry: project.retry,
1611
- fileSelection: prepared.fileSelection,
1612
- tagSelection: project.tags,
1613
- sourceCount: prepared.sources.length,
1614
- selectedCaseCount: prepared.selectedCaseCount,
1615
- filteredCaseCount: prepared.filteredCaseCount,
1616
- ...lifecycle ? {
1617
- lifecycle
1618
- } : {},
1619
- cases,
1620
- plans: prepared.plans,
1621
- documents,
1622
- collectionErrors: prepared.collectionErrors
1623
- };
1624
- };
1625
2770
  const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
1626
2771
  announceProject(prepared, projectIndex);
1627
2772
  return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
@@ -1722,7 +2867,7 @@ async function runPreparedTestProject(plan) {
1722
2867
  };
1723
2868
  const projectResults = new Array(preparedProjects.length);
1724
2869
  try {
1725
- await runConcurrentJobs(preparedProjects, {
2870
+ if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await runConcurrentJobs(preparedProjects, {
1726
2871
  concurrency: effectiveConcurrency,
1727
2872
  shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
1728
2873
  }, async (prepared, index)=>{
@@ -1733,6 +2878,20 @@ async function runPreparedTestProject(plan) {
1733
2878
  throw error;
1734
2879
  }
1735
2880
  });
2881
+ else {
2882
+ const results = await runCaseExecution({
2883
+ projects: preparedProjects,
2884
+ test: definition.test,
2885
+ executor: definition.executor,
2886
+ runDir,
2887
+ signal: rootController.signal,
2888
+ progress,
2889
+ onInfrastructureError: recordInfrastructureError
2890
+ });
2891
+ results.forEach((result, index)=>{
2892
+ projectResults[index] = result;
2893
+ });
2894
+ }
1736
2895
  } catch (error) {
1737
2896
  recordInfrastructureError(error);
1738
2897
  } finally{
@@ -1845,6 +3004,10 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
1845
3004
  let configPath;
1846
3005
  let resultDir;
1847
3006
  const projectNames = [];
3007
+ const paths = [];
3008
+ const caseIds = [];
3009
+ const includeTags = [];
3010
+ const excludeTags = [];
1848
3011
  for(let index = commandOffset; index < args.length; index += 1){
1849
3012
  const token = args[index];
1850
3013
  const equal = token.startsWith('-') ? token.indexOf('=') : -1;
@@ -1855,17 +3018,22 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
1855
3018
  projectRoot = (0, external_node_path_.resolve)(cwd, arg);
1856
3019
  continue;
1857
3020
  }
1858
- if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
3021
+ if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--file' === arg || '--case-id' === arg || '--tag' === arg || '--exclude-tag' === arg) {
1859
3022
  const value = inlineValue ?? args[index + 1];
1860
3023
  if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
1861
3024
  if ('--config' === arg) configPath = value;
1862
3025
  else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
1863
- else projectNames.push(value);
3026
+ else if ('--project' === arg) projectNames.push(value);
3027
+ else if ('--file' === arg) paths.push(value);
3028
+ else if ('--case-id' === arg) caseIds.push(value);
3029
+ else if ('--tag' === arg) includeTags.push(value);
3030
+ else excludeTags.push(value);
1864
3031
  if (void 0 === inlineValue) index += 1;
1865
3032
  } else throw new Error(`Unknown option: ${token}`);
1866
3033
  }
1867
3034
  if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
1868
3035
  if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
3036
+ if ('nodes' === command && (paths.length > 0 || caseIds.length > 0 || includeTags.length > 0 || excludeTags.length > 0)) throw new Error('nodes does not support --file, --case-id, --tag, or --exclude-tag.');
1869
3037
  return {
1870
3038
  ...command ? {
1871
3039
  command
@@ -1876,6 +3044,22 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
1876
3044
  resultDir,
1877
3045
  ...projectNames.length > 0 ? {
1878
3046
  projectNames
3047
+ } : {},
3048
+ ...paths.length > 0 ? {
3049
+ paths
3050
+ } : {},
3051
+ ...caseIds.length > 0 ? {
3052
+ caseIds
3053
+ } : {},
3054
+ ...includeTags.length > 0 || excludeTags.length > 0 ? {
3055
+ tags: {
3056
+ ...includeTags.length > 0 ? {
3057
+ include: includeTags
3058
+ } : {},
3059
+ ...excludeTags.length > 0 ? {
3060
+ exclude: excludeTags
3061
+ } : {}
3062
+ }
1879
3063
  } : {}
1880
3064
  };
1881
3065
  };