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