@midscene/test 1.15.4-beta-20261013083552.0 → 1.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/es/{1~214.mjs → 1~4.mjs} +96 -174
  2. package/dist/es/1~4.mjs.map +1 -0
  3. package/dist/es/cli/index.mjs +741 -1899
  4. package/dist/es/cli/index.mjs.map +1 -1
  5. package/dist/es/index.mjs +1 -20
  6. package/dist/es/index.mjs.map +1 -1
  7. package/dist/es/midscene/index.mjs +2 -2
  8. package/dist/es/midscene/index.mjs.map +1 -1
  9. package/dist/es/test-cli.mjs +70 -1254
  10. package/dist/es/test-cli.mjs.map +1 -1
  11. package/dist/lib/{0~160.js → 0~593.js} +95 -173
  12. package/dist/lib/0~593.js.map +1 -0
  13. package/dist/lib/cli/index.js +740 -1913
  14. package/dist/lib/cli/index.js.map +1 -1
  15. package/dist/lib/index.js +1 -20
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/midscene/index.js +0 -3
  18. package/dist/lib/midscene/index.js.map +1 -1
  19. package/dist/lib/test-cli.js +70 -1254
  20. package/dist/lib/test-cli.js.map +1 -1
  21. package/dist/templates/manifest.json +60 -0
  22. package/dist/templates/test/android/.env.example +15 -0
  23. package/dist/templates/test/android/README.md +19 -0
  24. package/dist/templates/test/android/README.zh.md +11 -0
  25. package/dist/templates/test/android/cases/todo.yaml +26 -0
  26. package/dist/templates/test/android/gitignore +4 -0
  27. package/dist/templates/test/android/midscene.config.ts +69 -0
  28. package/dist/templates/test/android/package.json +20 -0
  29. package/dist/templates/test/android/tsconfig.json +12 -0
  30. package/dist/templates/test/computer/.env.example +17 -0
  31. package/dist/templates/test/computer/README.md +19 -0
  32. package/dist/templates/test/computer/README.zh.md +11 -0
  33. package/dist/templates/test/computer/cases/todo.yaml +31 -0
  34. package/dist/templates/test/computer/gitignore +4 -0
  35. package/dist/templates/test/computer/midscene.config.ts +74 -0
  36. package/dist/templates/test/computer/package.json +20 -0
  37. package/dist/templates/test/computer/tsconfig.json +12 -0
  38. package/dist/templates/test/harmony/.env.example +15 -0
  39. package/dist/templates/test/harmony/README.md +19 -0
  40. package/dist/templates/test/harmony/README.zh.md +11 -0
  41. package/dist/templates/test/harmony/cases/todo.yaml +26 -0
  42. package/dist/templates/test/harmony/gitignore +4 -0
  43. package/dist/templates/test/harmony/midscene.config.ts +69 -0
  44. package/dist/templates/test/harmony/package.json +20 -0
  45. package/dist/templates/test/harmony/tsconfig.json +12 -0
  46. package/dist/templates/test/ios/.env.example +18 -0
  47. package/dist/templates/test/ios/README.md +19 -0
  48. package/dist/templates/test/ios/README.zh.md +11 -0
  49. package/dist/templates/test/ios/cases/todo.yaml +26 -0
  50. package/dist/templates/test/ios/gitignore +4 -0
  51. package/dist/templates/test/ios/midscene.config.ts +95 -0
  52. package/dist/templates/test/ios/package.json +20 -0
  53. package/dist/templates/test/ios/tsconfig.json +12 -0
  54. package/dist/templates/test/web/.env.example +14 -0
  55. package/dist/templates/test/web/README.md +19 -0
  56. package/dist/templates/test/web/README.zh.md +13 -0
  57. package/dist/templates/test/web/cases/todo.yaml +62 -0
  58. package/dist/templates/test/web/gitignore +4 -0
  59. package/dist/templates/test/web/midscene.config.ts +56 -0
  60. package/dist/templates/test/web/nodes/todo.ts +215 -0
  61. package/dist/templates/test/web/package.json +21 -0
  62. package/dist/templates/test/web/tsconfig.json +12 -0
  63. package/dist/types/cli/index.d.ts +0 -550
  64. package/dist/types/index.d.ts +0 -20
  65. package/dist/types/midscene/index.d.ts +2 -6
  66. package/package.json +16 -15
  67. package/dist/es/1~214.mjs.map +0 -1
  68. package/dist/lib/0~160.js.map +0 -1
@@ -20,7 +20,9 @@ var __webpack_modules__ = {
20
20
  module.exports = __rspack_external_node_path_c5b9b54f;
21
21
  },
22
22
  "./package.json" (module) {
23
- module.exports = JSON.parse('{"rE":"1.15.4-beta-20261013083552.0"}');
23
+ module.exports = {
24
+ rE: "1.15.4"
25
+ };
24
26
  }
25
27
  };
26
28
  var __webpack_module_cache__ = {};
@@ -267,7 +269,7 @@ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = fal
267
269
  return pattern;
268
270
  });
269
271
  };
270
- const validateTestFileSelection = (value, label = 'files')=>{
272
+ const test_project_validateTestFileSelection = (value, label = 'files')=>{
271
273
  if (void 0 === value) return;
272
274
  if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
273
275
  rejectUnknownKeys(value, [
@@ -391,7 +393,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
391
393
  if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
392
394
  if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
393
395
  names.add(candidate.name);
394
- const files = validateTestFileSelection(candidate.files, `${label}.files`);
396
+ const files = test_project_validateTestFileSelection(candidate.files, `${label}.files`);
395
397
  if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
396
398
  const localNodes = new registry_NodeRegistry(candidate.nodes);
397
399
  const nodes = new registry_NodeRegistry([
@@ -421,27 +423,12 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
421
423
  const validateTestOptions = (value)=>{
422
424
  if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
423
425
  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'),
430
- executionUnit,
431
- executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
429
+ testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
432
430
  });
433
431
  };
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
- };
445
432
  const validateOutputPath = (value, fallback, label)=>{
446
433
  if (void 0 === value) return fallback;
447
434
  if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
@@ -470,22 +457,15 @@ const validateTestProjectDefinition = (definition)=>{
470
457
  'projects',
471
458
  'test',
472
459
  'output',
473
- 'nodes',
474
- 'executor'
460
+ 'nodes'
475
461
  ], 'root');
476
462
  const nodes = new registry_NodeRegistry(definition.nodes);
477
463
  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".');
481
464
  return {
482
465
  ...resolvedProjects,
483
- test,
466
+ test: validateTestOptions(definition.test),
484
467
  output: validateOutput(definition.output),
485
468
  nodes,
486
- ...executor ? {
487
- executor
488
- } : {},
489
469
  resolveNode: (name)=>nodes.get(name)
490
470
  };
491
471
  };
@@ -530,7 +510,6 @@ async function loadTestProject(configPath) {
530
510
  }
531
511
  return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
532
512
  }
533
- const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
534
513
  const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
535
514
  projectId,
536
515
  sourcePath,
@@ -750,15 +729,10 @@ function collectWorkflowDocument(source, options) {
750
729
  caseIndex
751
730
  });
752
731
  collect_rejectUnknownKeys(definition, [
753
- 'id',
754
732
  'name',
755
733
  'tags',
756
- 'resources',
757
734
  'steps'
758
735
  ], `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
- });
762
736
  if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
763
737
  caseIndex
764
738
  });
@@ -769,13 +743,6 @@ function collectWorkflowDocument(source, options) {
769
743
  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.`, {
770
744
  caseIndex
771
745
  });
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
- });
779
746
  const steps = normalize_normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
780
747
  if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
781
748
  caseIndex,
@@ -784,7 +751,7 @@ function collectWorkflowDocument(source, options) {
784
751
  });
785
752
  return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
786
753
  });
787
- const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
754
+ const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
788
755
  if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
789
756
  caseId
790
757
  });
@@ -795,14 +762,8 @@ function collectWorkflowDocument(source, options) {
795
762
  sourcePath,
796
763
  caseIndex,
797
764
  definition: {
798
- ...'string' == typeof definition.id ? {
799
- id: definition.id
800
- } : {},
801
765
  name: definition.name,
802
766
  tags: tags,
803
- ...resources.length > 0 ? {
804
- resources: resources
805
- } : {},
806
767
  steps
807
768
  }
808
769
  };
@@ -942,38 +903,6 @@ const writeTestProjectRunResult = (options)=>{
942
903
  ...outcome.notRunReason ? {
943
904
  notRunReason: outcome.notRunReason
944
905
  } : {},
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
- } : {},
977
906
  ...outcome.attempts ? {
978
907
  attempts: outcome.attempts.map((attempt)=>({
979
908
  attemptId: attempt.runId,
@@ -1051,27 +980,6 @@ const DEFAULT_TEST_FILE_SELECTION = {
1051
980
  ]
1052
981
  };
1053
982
  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
- };
1075
983
  const discoverResolvedTestFiles = (projectRoot, selection)=>{
1076
984
  const root = (0, external_node_path_.resolve)(projectRoot);
1077
985
  const match = (patterns)=>globSync(patterns, {
@@ -1096,11 +1004,6 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
1096
1004
  return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
1097
1005
  });
1098
1006
  };
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
- };
1104
1007
  const discoverTestConfig = (projectRoot)=>{
1105
1008
  const root = (0, external_node_path_.resolve)(projectRoot);
1106
1009
  const candidates = (0, external_node_fs_.readdirSync)(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
@@ -1131,14 +1034,8 @@ const matchesTags = (tags, selection)=>{
1131
1034
  if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
1132
1035
  return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
1133
1036
  };
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
- });
1037
+ const filterDocumentCases = (document, project)=>{
1038
+ const cases = document.cases.filter((item)=>matchesTags(item.definition.tags ?? [], project.tags));
1142
1039
  const filtered = document.cases.length - cases.length;
1143
1040
  return 0 === cases.length ? {
1144
1041
  filtered
@@ -1163,9 +1060,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1163
1060
  const platform = options.platform?.trim();
1164
1061
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
1165
1062
  const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_.resolve)(options.prerequisiteFile) : void 0;
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;
1063
+ const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
1169
1064
  const files = [
1170
1065
  ...prerequisiteFile ? [
1171
1066
  prerequisiteFile
@@ -1188,7 +1083,6 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1188
1083
  const invocations = [];
1189
1084
  const plans = [];
1190
1085
  let filteredCaseCount = 0;
1191
- const availableCaseIds = new Set();
1192
1086
  if (0 === sources.length) {
1193
1087
  const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
1194
1088
  projectName: project.name,
@@ -1219,16 +1113,10 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1219
1113
  variables: project.variables,
1220
1114
  env: process.env
1221
1115
  });
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);
1228
1116
  const filtered = source.absolutePath === prerequisiteFile ? {
1229
1117
  document,
1230
1118
  filtered: 0
1231
- } : filterDocumentCases(document, project, options.selection);
1119
+ } : filterDocumentCases(document, project);
1232
1120
  const plan = buildTestRunReportPlan(document, {
1233
1121
  selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
1234
1122
  defaultTimeoutMs,
@@ -1276,42 +1164,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
1276
1164
  plans,
1277
1165
  collectionErrors,
1278
1166
  selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
1279
- filteredCaseCount,
1280
- availableCaseIds: [
1281
- ...availableCaseIds
1282
- ]
1167
+ filteredCaseCount
1283
1168
  };
1284
1169
  }
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
- };
1315
1170
  const padDatePart = (value)=>String(value).padStart(2, '0');
1316
1171
  const createTestRunId = (date = new Date(), uuid = randomUUID())=>{
1317
1172
  const timestamp = [
@@ -1349,7 +1204,6 @@ async function prepareTestRun(options) {
1349
1204
  const startedAt = new Date();
1350
1205
  const runId = createTestRunId(startedAt);
1351
1206
  const input = resolveRunInput(options);
1352
- const selection = validateRunSelection(options);
1353
1207
  const configPath = options.configPath ? (0, external_node_path_.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
1354
1208
  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}\`.`);
1355
1209
  if (options.configPath && !(0, external_node_fs_.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
@@ -1363,20 +1217,11 @@ async function prepareTestRun(options) {
1363
1217
  });
1364
1218
  (0, external_node_fs_.mkdirSync)(runDir);
1365
1219
  const projects = [];
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.');
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));
1380
1225
  const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
1381
1226
  return {
1382
1227
  startedAt,
@@ -1472,1051 +1317,20 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
1472
1317
  }));
1473
1318
  return output;
1474
1319
  }
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
- });
1320
+ const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
1485
1321
  const formatStep = (info)=>{
1486
- const position = 'case' === info.scope ? info.case : info.document;
1322
+ const position = stepPosition(info);
1487
1323
  const phase = 'steps' === position.phase ? 'step' : position.phase;
1488
1324
  return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
1489
1325
  };
1490
- const formatStepResult = (info, result, indent = '')=>{
1326
+ const formatStepResult = (info, result)=>{
1327
+ const indent = 'case' === info.scope ? ' ' : ' ';
1491
1328
  if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
1492
1329
  const symbol = 'success' === result.status ? '✓' : '✗';
1493
1330
  const error = result.error ? ` — ${result.error.message}` : '';
1494
1331
  const continuation = result.continuedAfterError ? '; continuing' : '';
1495
1332
  return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
1496
1333
  };
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
- };
2520
1334
  const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
2521
1335
  ...attempt.beforeEach,
2522
1336
  ...attempt.steps,
@@ -2562,7 +1376,7 @@ async function executeDocumentInvocation(options) {
2562
1376
  },
2563
1377
  onStepResult: async (info, result)=>{
2564
1378
  await invocation.bindings.onStepResult?.(info, result);
2565
- options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
1379
+ options.onProgress(formatStepResult(info, result));
2566
1380
  },
2567
1381
  onCaseResult: async (attempt)=>{
2568
1382
  await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
@@ -2680,6 +1494,16 @@ function createOperationJournal(runDir) {
2680
1494
  }
2681
1495
  };
2682
1496
  }
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
+ });
2683
1507
  const summarize = (projects)=>{
2684
1508
  const healthCases = projects.flatMap((project)=>{
2685
1509
  const documentAttemptByRunId = new Map(project.documents.map((document)=>[
@@ -2713,6 +1537,12 @@ const summarize = (projects)=>{
2713
1537
  projectFailures: projects.filter((project)=>'failed' === project.status).length
2714
1538
  };
2715
1539
  };
1540
+ const latestById = (items, id)=>[
1541
+ ...new Map(items.map((item)=>[
1542
+ id(item),
1543
+ item
1544
+ ])).values()
1545
+ ];
2716
1546
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
2717
1547
  async function runPreparedTestProject(plan) {
2718
1548
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
@@ -2725,7 +1555,7 @@ async function runPreparedTestProject(plan) {
2725
1555
  await writeTestRunPlan(runDir, preparedProjects);
2726
1556
  const journal = createOperationJournal(runDir);
2727
1557
  const rootController = new AbortController();
2728
- setMaxListeners(Math.max(10, ('case' === definition.test.executionUnit ? Math.min(definition.test.maxConcurrency, totalCases) : effectiveConcurrency) + 1), rootController.signal);
1558
+ setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
2729
1559
  const handleSignal = (signal)=>{
2730
1560
  rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
2731
1561
  };
@@ -2767,6 +1597,31 @@ async function runPreparedTestProject(plan) {
2767
1597
  const { project } = prepared;
2768
1598
  runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
2769
1599
  };
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
+ };
2770
1625
  const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
2771
1626
  announceProject(prepared, projectIndex);
2772
1627
  return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
@@ -2867,7 +1722,7 @@ async function runPreparedTestProject(plan) {
2867
1722
  };
2868
1723
  const projectResults = new Array(preparedProjects.length);
2869
1724
  try {
2870
- if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await runConcurrentJobs(preparedProjects, {
1725
+ await runConcurrentJobs(preparedProjects, {
2871
1726
  concurrency: effectiveConcurrency,
2872
1727
  shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
2873
1728
  }, async (prepared, index)=>{
@@ -2878,20 +1733,6 @@ async function runPreparedTestProject(plan) {
2878
1733
  throw error;
2879
1734
  }
2880
1735
  });
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
- }
2895
1736
  } catch (error) {
2896
1737
  recordInfrastructureError(error);
2897
1738
  } finally{
@@ -3004,10 +1845,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3004
1845
  let configPath;
3005
1846
  let resultDir;
3006
1847
  const projectNames = [];
3007
- const paths = [];
3008
- const caseIds = [];
3009
- const includeTags = [];
3010
- const excludeTags = [];
3011
1848
  for(let index = commandOffset; index < args.length; index += 1){
3012
1849
  const token = args[index];
3013
1850
  const equal = token.startsWith('-') ? token.indexOf('=') : -1;
@@ -3018,22 +1855,17 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3018
1855
  projectRoot = (0, external_node_path_.resolve)(cwd, arg);
3019
1856
  continue;
3020
1857
  }
3021
- if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--file' === arg || '--case-id' === arg || '--tag' === arg || '--exclude-tag' === arg) {
1858
+ if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
3022
1859
  const value = inlineValue ?? args[index + 1];
3023
1860
  if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
3024
1861
  if ('--config' === arg) configPath = value;
3025
1862
  else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, 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);
1863
+ else projectNames.push(value);
3031
1864
  if (void 0 === inlineValue) index += 1;
3032
1865
  } else throw new Error(`Unknown option: ${token}`);
3033
1866
  }
3034
1867
  if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
3035
1868
  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.');
3037
1869
  return {
3038
1870
  ...command ? {
3039
1871
  command
@@ -3044,22 +1876,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3044
1876
  resultDir,
3045
1877
  ...projectNames.length > 0 ? {
3046
1878
  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
- }
3063
1879
  } : {}
3064
1880
  };
3065
1881
  };
@@ -3215,7 +2031,7 @@ async function runTestCli(args, io = defaultCliIO) {
3215
2031
  });
3216
2032
  }
3217
2033
  if ('create' === args[0]) {
3218
- const { runCreateCommand } = await __webpack_require__.e("214").then(__webpack_require__.bind(__webpack_require__, "./src/cli/create-command.ts"));
2034
+ const { runCreateCommand } = await __webpack_require__.e("4").then(__webpack_require__.bind(__webpack_require__, "./src/cli/create-command.ts"));
3219
2035
  await runCreateCommand(args.slice(1), io);
3220
2036
  return 0;
3221
2037
  }