@midscene/test 1.15.4-beta-20261013090705.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 -1910
  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 -1265
  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 -1927
  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 -1265
  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 -556
  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-20261013090705.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,1062 +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
- const executorErrorKey = Symbol.for('@midscene/test/executor-error');
1736
- let _executorErrorKey = executorErrorKey;
1737
- class TestExecutorError extends Error {
1738
- constructor(message, options = {}){
1739
- super(message, {
1740
- cause: options.cause
1741
- }), test_executor_define_property(this, _executorErrorKey, true), test_executor_define_property(this, "kind", void 0), test_executor_define_property(this, "retryable", void 0);
1742
- this.name = 'TestExecutorError';
1743
- this.kind = options.kind ?? 'unknown';
1744
- this.retryable = options.retryable ?? false;
1745
- }
1746
- }
1747
- function isTestExecutorError(value) {
1748
- return value instanceof Error && true === Reflect.get(value, executorErrorKey) && [
1749
- 'provision',
1750
- 'transport',
1751
- 'cleanup',
1752
- 'report',
1753
- 'unknown'
1754
- ].includes(Reflect.get(value, 'kind')) && 'boolean' == typeof Reflect.get(value, 'retryable');
1755
- }
1756
- const localTestExecutor = Object.freeze({
1757
- name: 'local',
1758
- execute: (_task, context)=>context.runLocal()
1759
- });
1760
- const jsonValueSchema = z.lazy(()=>z.union([
1761
- z["null"](),
1762
- z.string(),
1763
- z.number().finite(),
1764
- z.boolean(),
1765
- z.array(jsonValueSchema),
1766
- z.record(z.string(), jsonValueSchema)
1767
- ]));
1768
- const timestampSchema = z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
1769
- message: 'Expected an ISO-compatible timestamp.'
1770
- });
1771
- const errorSchema = z.strictObject({
1772
- format: z.literal('midscene-error').optional(),
1773
- cause: reportErrorSchema.optional(),
1774
- name: z.string().min(1),
1775
- message: z.string(),
1776
- code: z.string().min(1),
1777
- details: jsonValueSchema.optional()
1778
- });
1779
- const stepBaseSchema = z.strictObject({
1780
- phase: z["enum"]([
1781
- 'beforeAll',
1782
- 'beforeEach',
1783
- 'steps',
1784
- 'afterEach',
1785
- 'afterAll'
1786
- ]),
1787
- stepIndex: z.number().int().nonnegative(),
1788
- node: z.string().min(1),
1789
- input: jsonValueSchema,
1790
- meta: z.strictObject({
1791
- timeoutMs: z.number().positive().finite().optional(),
1792
- continueOnError: z.boolean()
1793
- }),
1794
- continuedAfterError: z.boolean(),
1795
- waitForPreviousOperationMs: z.number().nonnegative().finite().optional(),
1796
- operations: operationsSchema.optional(),
1797
- output: z.strictObject({
1798
- summary: z.string().optional(),
1799
- data: jsonValueSchema.optional()
1800
- }).optional(),
1801
- error: errorSchema.optional(),
1802
- report: z.strictObject({
1803
- traces: z.array(z.strictObject({
1804
- type: z.literal('midscene-execution'),
1805
- executionId: z.string().min(1),
1806
- metrics: metricsSchema.optional()
1807
- })),
1808
- collectionErrors: z.array(errorSchema).optional()
1809
- }).optional()
1810
- });
1811
- const stepSchema = z.discriminatedUnion('status', [
1812
- stepBaseSchema.extend({
1813
- status: z["enum"]([
1814
- 'success',
1815
- 'failed'
1816
- ]),
1817
- startedAt: timestampSchema,
1818
- endedAt: timestampSchema,
1819
- durationMs: z.number().nonnegative().finite()
1820
- }),
1821
- stepBaseSchema.extend({
1822
- status: z.literal('not-run'),
1823
- notRunReason: z.string().min(1),
1824
- blockedBy: z.string().optional()
1825
- })
1826
- ]);
1827
- const validatedStepSchema = stepSchema.superRefine((step, context)=>{
1828
- if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
1829
- code: 'custom',
1830
- message: 'Successful steps cannot contain failure evidence.'
1831
- });
1832
- if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
1833
- code: 'custom',
1834
- message: 'Continuation requires continueOnError.'
1835
- });
1836
- });
1837
- const attemptSchema = z.strictObject({
1838
- ...runtimeResultFields,
1839
- caseId: z.string().min(1),
1840
- runId: z.string().min(1),
1841
- projectName: z.string().min(1),
1842
- attemptIndex: z.number().int().nonnegative(),
1843
- name: z.string(),
1844
- sourcePath: z.string().min(1),
1845
- caseIndex: z.number().int().nonnegative(),
1846
- status: z["enum"]([
1847
- 'success',
1848
- 'failed'
1849
- ]),
1850
- beforeEach: z.array(validatedStepSchema),
1851
- steps: z.array(validatedStepSchema),
1852
- afterEach: z.array(validatedStepSchema),
1853
- executionErrors: z.array(errorSchema).optional(),
1854
- teardownErrors: z.array(errorSchema).optional(),
1855
- reportRefs: z.array(z.string().min(1)).optional(),
1856
- startedAt: timestampSchema,
1857
- endedAt: timestampSchema,
1858
- durationMs: z.number().nonnegative().finite()
1859
- });
1860
- const notRunReasonSchema = z["enum"]([
1861
- 'document-start-failed',
1862
- 'project-preflight-failed',
1863
- 'project-setup-failed',
1864
- 'interrupted',
1865
- 'bail',
1866
- 'fatal-error',
1867
- 'executor-failed',
1868
- 'cleanup-pending',
1869
- 'cleanup-failed',
1870
- 'operation-pending'
1871
- ]);
1872
- const resultSchema = z.strictObject({
1873
- case: z.strictObject({
1874
- documentId: z.string().min(1),
1875
- caseId: z.string().min(1),
1876
- projectName: z.string().min(1),
1877
- name: z.string(),
1878
- sourcePath: z.string().min(1),
1879
- caseIndex: z.number().int().nonnegative(),
1880
- status: z["enum"]([
1881
- 'success',
1882
- 'failed',
1883
- 'not-run'
1884
- ]),
1885
- run: attemptSchema.optional(),
1886
- attempts: z.array(attemptSchema).optional(),
1887
- notRunReason: notRunReasonSchema.optional()
1888
- }),
1889
- document: z.strictObject({
1890
- ...runtimeResultFields,
1891
- plan: documentPlanSchema.optional(),
1892
- attemptIndex: z.number().int().nonnegative().optional(),
1893
- hostErrors: z.array(z.strictObject({
1894
- phase: z["enum"]([
1895
- 'setup',
1896
- 'execution',
1897
- 'cleanup',
1898
- 'report',
1899
- 'publication',
1900
- 'observer'
1901
- ]),
1902
- error: reportErrorSchema
1903
- })).optional(),
1904
- documentId: z.string().min(1),
1905
- documentRunId: z.string().min(1),
1906
- projectId: z.string().min(1),
1907
- projectName: z.string().min(1),
1908
- sourcePath: z.string().min(1),
1909
- status: z["enum"]([
1910
- 'success',
1911
- 'failed'
1912
- ]),
1913
- startedAt: timestampSchema,
1914
- endedAt: timestampSchema,
1915
- durationMs: z.number().nonnegative().finite(),
1916
- beforeAll: z.array(validatedStepSchema),
1917
- afterAll: z.array(validatedStepSchema),
1918
- executionErrors: z.array(errorSchema).optional(),
1919
- teardownErrors: z.array(errorSchema).optional(),
1920
- reportRefs: z.array(z.string().min(1)).optional()
1921
- }).optional(),
1922
- lifecycle: z.strictObject({
1923
- cleanup: cleanupSchema.optional(),
1924
- pendingOperations: pendingOperationsSchema.optional(),
1925
- projectName: z.string().min(1),
1926
- status: z["enum"]([
1927
- 'success',
1928
- 'failed'
1929
- ]),
1930
- setupError: errorSchema.optional(),
1931
- teardownErrors: z.array(errorSchema).optional(),
1932
- startedAt: timestampSchema,
1933
- endedAt: timestampSchema,
1934
- durationMs: z.number().nonnegative().finite()
1935
- }).optional(),
1936
- artifacts: z.array(z.strictObject({
1937
- name: z.string().min(1),
1938
- uri: z.string().min(1),
1939
- mediaType: z.string().min(1).optional()
1940
- })).optional(),
1941
- metadata: z.record(z.string(), z.union([
1942
- z.string(),
1943
- z.number().finite(),
1944
- z.boolean(),
1945
- z["null"]()
1946
- ])).optional()
1947
- });
1948
- 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);
1949
- const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
1950
- 'completed',
1951
- 'not-required'
1952
- ].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
1953
- function assertTestCaseTaskRunResult(task, value) {
1954
- const parsed = resultSchema.safeParse(value);
1955
- if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${z.prettifyError(parsed.error)}`);
1956
- const outcome = parsed.data.case;
1957
- 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}.`);
1958
- const attempts = outcome.attempts ?? [];
1959
- for (const attempt of attempts){
1960
- const steps = [
1961
- ...attempt.beforeEach,
1962
- ...attempt.steps,
1963
- ...attempt.afterEach
1964
- ];
1965
- 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}.`);
1966
- }
1967
- if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
1968
- if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
1969
- 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}.`);
1970
- const finalAttempt = attempts.at(-1);
1971
- if ('not-run' === outcome.status) {
1972
- if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
1973
- } 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}.`);
1974
- const reportRefs = [
1975
- ...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
1976
- ...parsed.data.document?.reportRefs ?? []
1977
- ];
1978
- if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
1979
- const document = parsed.data.document;
1980
- if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
1981
- ...document.beforeAll,
1982
- ...document.afterAll
1983
- ].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
1984
- const lifecycle = parsed.data.lifecycle;
1985
- 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}.`);
1986
- 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}.`);
1987
- if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
1988
- }
1989
- function createExecutorReports(outputDir) {
1990
- const paths = new Map();
1991
- const sources = new Map();
1992
- return {
1993
- materializeSource (source) {
1994
- if (!source.dumpPath) {
1995
- const reference = this.materialize(source.sourcePath);
1996
- sources.set(reference, {
1997
- sourcePath: this.resolve(reference),
1998
- collection: source.collection
1999
- });
2000
- return reference;
2001
- }
2002
- const dump = (0, external_node_fs_.realpathSync)(source.dumpPath);
2003
- const reference = randomUUID();
2004
- const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
2005
- (0, external_node_fs_.mkdirSync)(destination, {
2006
- recursive: true
2007
- });
2008
- const dumpPath = (0, external_node_path_.join)(destination, 'snapshot.json');
2009
- (0, external_node_fs_.cpSync)(dump, dumpPath);
2010
- const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(dump), 'screenshots');
2011
- if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
2012
- recursive: true,
2013
- dereference: true
2014
- });
2015
- const sourcePath = (0, external_node_path_.join)(destination, 'index.html');
2016
- sources.set(reference, {
2017
- sourcePath,
2018
- dumpPath,
2019
- collection: source.collection
2020
- });
2021
- paths.set(reference, sourcePath);
2022
- return reference;
2023
- },
2024
- resolveSource (reference) {
2025
- return sources.get(reference);
2026
- },
2027
- materialize (sourcePath) {
2028
- const source = (0, external_node_fs_.realpathSync)(sourcePath);
2029
- const entry = (0, external_node_fs_.statSync)(source).isDirectory() ? (0, external_node_path_.join)(source, 'index.html') : source;
2030
- if (!(0, external_node_fs_.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
2031
- kind: 'report'
2032
- });
2033
- const reference = randomUUID();
2034
- const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
2035
- (0, external_node_fs_.mkdirSync)(destination, {
2036
- recursive: true
2037
- });
2038
- const isDirectoryReport = 'index.html' === (0, external_node_path_.basename)(entry);
2039
- const reportPath = (0, external_node_path_.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
2040
- (0, external_node_fs_.cpSync)(isDirectoryReport ? (0, external_node_path_.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
2041
- recursive: isDirectoryReport,
2042
- dereference: true
2043
- });
2044
- if (!isDirectoryReport) {
2045
- const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(entry), 'screenshots');
2046
- if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
2047
- recursive: true,
2048
- dereference: true
2049
- });
2050
- }
2051
- paths.set(reference, reportPath);
2052
- return reference;
2053
- },
2054
- resolve (reference) {
2055
- const reportPath = paths.get(reference);
2056
- if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
2057
- kind: 'report'
2058
- });
2059
- return reportPath;
2060
- }
2061
- };
2062
- }
2063
- async function runTaskPool(options) {
2064
- if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
2065
- const pending = options.tasks.map((_, index)=>index);
2066
- const results = new Array(options.tasks.length);
2067
- const activeResources = new Set();
2068
- let active = 0;
2069
- let firstError;
2070
- let hasError = false;
2071
- return await new Promise((resolve, reject)=>{
2072
- let settled = false;
2073
- const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
2074
- const finish = ()=>{
2075
- if (settled) return;
2076
- settled = true;
2077
- options.signal?.removeEventListener('abort', pump);
2078
- if (hasError) reject(firstError);
2079
- else resolve(results);
2080
- };
2081
- const release = (resources)=>{
2082
- for (const resource of resources)activeResources.delete(resource);
2083
- };
2084
- const claimRunnable = ()=>{
2085
- const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
2086
- if (pendingIndex < 0) return;
2087
- const [taskIndex] = pending.splice(pendingIndex, 1);
2088
- return taskIndex;
2089
- };
2090
- function pump() {
2091
- if (settled) return;
2092
- while(active < options.maxConcurrency && !stopRequested()){
2093
- const taskIndex = claimRunnable();
2094
- if (void 0 === taskIndex) break;
2095
- const task = options.tasks[taskIndex];
2096
- for (const resource of task.resources)activeResources.add(resource);
2097
- active += 1;
2098
- Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
2099
- results[taskIndex] = result;
2100
- }).catch((error)=>{
2101
- if (!hasError) {
2102
- hasError = true;
2103
- firstError = error;
2104
- }
2105
- }).finally(()=>{
2106
- release(task.resources);
2107
- active -= 1;
2108
- pump();
2109
- });
2110
- }
2111
- if (0 === active && (0 === pending.length || stopRequested())) finish();
2112
- }
2113
- options.signal?.addEventListener('abort', pump, {
2114
- once: true
2115
- });
2116
- pump();
2117
- });
2118
- }
2119
- const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
2120
- const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
2121
- const documentId = isolatedDocumentId(document, collectedCase);
2122
- return {
2123
- projectIndex,
2124
- prepared,
2125
- document,
2126
- collectedCase,
2127
- task: Object.freeze({
2128
- taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
2129
- projectId: prepared.project.projectId,
2130
- projectName: prepared.project.name,
2131
- documentId,
2132
- sourcePath: collectedCase.sourcePath,
2133
- caseId: collectedCase.caseId,
2134
- caseName: collectedCase.definition.name,
2135
- caseIndex: collectedCase.caseIndex,
2136
- tags: Object.freeze([
2137
- ...collectedCase.definition.tags ?? []
2138
- ]),
2139
- resources: Object.freeze([
2140
- ...collectedCase.definition.resources ?? []
2141
- ]),
2142
- retry: prepared.project.retry
2143
- })
2144
- };
2145
- })));
2146
- const reviveError = (error)=>{
2147
- const revived = new WorkflowError(error.message, {
2148
- code: error.code,
2149
- details: error.details,
2150
- cause: error.cause
2151
- });
2152
- revived.name = error.name;
2153
- return revived;
2154
- };
2155
- const reviveStep = (step)=>{
2156
- const { error, output, report, ...base } = step;
2157
- return {
2158
- ...base,
2159
- input: step.input,
2160
- meta: {
2161
- ...step.meta
2162
- },
2163
- ...output ? {
2164
- output: {
2165
- ...output
2166
- }
2167
- } : {},
2168
- ...error ? {
2169
- error: reviveError(error)
2170
- } : {},
2171
- ...report ? {
2172
- report: {
2173
- traces: report.traces.map((trace)=>({
2174
- ...trace
2175
- })),
2176
- ...report.collectionErrors ? {
2177
- collectionErrors: report.collectionErrors.map(reviveError)
2178
- } : {}
2179
- }
2180
- } : {}
2181
- };
2182
- };
2183
- const reviveAttempt = (attempt)=>{
2184
- const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
2185
- return {
2186
- ...base,
2187
- beforeEach: beforeEach.map(reviveStep),
2188
- steps: steps.map(reviveStep),
2189
- afterEach: afterEach.map(reviveStep),
2190
- ...executionErrors ? {
2191
- executionErrors: executionErrors.map(reviveError)
2192
- } : {},
2193
- ...teardownErrors ? {
2194
- teardownErrors: teardownErrors.map(reviveError)
2195
- } : {}
2196
- };
2197
- };
2198
- const reviveDocument = (document)=>{
2199
- const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
2200
- return {
2201
- ...base,
2202
- ...hostErrors ? {
2203
- hostErrors: hostErrors.map((error)=>({
2204
- ...error
2205
- }))
2206
- } : {},
2207
- beforeAll: beforeAll.map(reviveStep),
2208
- afterAll: afterAll.map(reviveStep),
2209
- ...executionErrors ? {
2210
- executionErrors: executionErrors.map(reviveError)
2211
- } : {},
2212
- ...teardownErrors ? {
2213
- teardownErrors: teardownErrors.map(reviveError)
2214
- } : {}
2215
- };
2216
- };
2217
- const reviveLifecycle = (lifecycle)=>{
2218
- const { setupError, teardownErrors, ...base } = lifecycle;
2219
- return {
2220
- ...base,
2221
- ...setupError ? {
2222
- setupError: reviveError(setupError)
2223
- } : {},
2224
- ...teardownErrors ? {
2225
- teardownErrors: teardownErrors.map(reviveError)
2226
- } : {}
2227
- };
2228
- };
2229
- const toTransportResult = (result, materializeReport, materializeSource)=>{
2230
- const transportReports = (scope)=>[
2231
- ...(scope.reportSources ?? []).map(materializeSource),
2232
- ...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
2233
- ];
2234
- const mapStep = (step)=>{
2235
- if (!step.report) return step;
2236
- const { sources: _sources, ...report } = step.report;
2237
- return {
2238
- ...step,
2239
- report
2240
- };
2241
- };
2242
- const mapAttempt = (attempt)=>{
2243
- const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
2244
- const reportRefs = transportReports(attempt);
2245
- return {
2246
- ...transportAttempt,
2247
- beforeEach: attempt.beforeEach.map(mapStep),
2248
- steps: attempt.steps.map(mapStep),
2249
- afterEach: attempt.afterEach.map(mapStep),
2250
- ...reportRefs.length ? {
2251
- reportRefs
2252
- } : {}
2253
- };
2254
- };
2255
- const attempts = result.case.attempts?.map(mapAttempt);
2256
- const run = attempts?.at(-1);
2257
- const { run: _run, attempts: _attempts, ...transportCase } = result.case;
2258
- const document = result.document ? (()=>{
2259
- const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
2260
- const reportRefs = transportReports(result.document);
2261
- return {
2262
- ...transportDocument,
2263
- ...result.document.hostErrors ? {
2264
- hostErrors: result.document.hostErrors.map(({ phase, error })=>({
2265
- phase,
2266
- error: sanitizeReportError(error)
2267
- }))
2268
- } : {},
2269
- beforeAll: result.document.beforeAll.map(mapStep),
2270
- afterAll: result.document.afterAll.map(mapStep),
2271
- ...reportRefs.length ? {
2272
- reportRefs
2273
- } : {}
2274
- };
2275
- })() : void 0;
2276
- return JSON.parse(JSON.stringify({
2277
- case: {
2278
- ...transportCase,
2279
- ...run ? {
2280
- run
2281
- } : {},
2282
- ...attempts ? {
2283
- attempts
2284
- } : {}
2285
- },
2286
- ...document ? {
2287
- document
2288
- } : {},
2289
- ...result.lifecycle ? {
2290
- lifecycle: result.lifecycle
2291
- } : {}
2292
- }));
2293
- };
2294
- const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
2295
- const resolveReports = (refs)=>{
2296
- const reportPaths = [];
2297
- const reportSources = [];
2298
- for (const ref of refs ?? []){
2299
- const path = resolveReportReference(ref);
2300
- const source = resolveSource(ref);
2301
- if (source) reportSources.push(source);
2302
- else reportPaths.push(path);
2303
- }
2304
- return {
2305
- ...reportPaths.length ? {
2306
- reportPaths
2307
- } : {},
2308
- ...reportSources.length ? {
2309
- reportSources
2310
- } : {}
2311
- };
2312
- };
2313
- const attempts = result.case.attempts?.map((attempt)=>({
2314
- ...reviveAttempt(attempt),
2315
- ...resolveReports(attempt.reportRefs)
2316
- }));
2317
- const run = attempts?.at(-1);
2318
- const { run: _run, attempts: _attempts, ...caseBase } = result.case;
2319
- return {
2320
- case: {
2321
- ...caseBase,
2322
- ...run ? {
2323
- run
2324
- } : {},
2325
- ...attempts ? {
2326
- attempts
2327
- } : {}
2328
- },
2329
- ...result.document ? {
2330
- document: {
2331
- ...reviveDocument(result.document),
2332
- ...resolveReports(result.document.reportRefs)
2333
- }
2334
- } : {},
2335
- ...result.lifecycle ? {
2336
- lifecycle: reviveLifecycle(result.lifecycle)
2337
- } : {}
2338
- };
2339
- };
2340
- const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
2341
- const { prepared, document, collectedCase, task } = preparedTask;
2342
- const { project } = prepared;
2343
- const isolatedDocument = {
2344
- ...document,
2345
- documentId: task.documentId,
2346
- cases: [
2347
- collectedCase
2348
- ]
2349
- };
2350
- const runtime = createProjectRuntime({
2351
- project,
2352
- setup: project.setup,
2353
- signal: options.signal
2354
- });
2355
- let lifecycle;
2356
- let execution;
2357
- let executionError;
2358
- try {
2359
- await runtime.start();
2360
- if (runtime.canRun) execution = await runWorkflowDocument(isolatedDocument, {
2361
- resolveNode: project.nodes.require.bind(project.nodes),
2362
- project,
2363
- projectContext: runtime.context,
2364
- retry: project.retry,
2365
- signal: runtime.signal,
2366
- defaultTimeoutMs: options.test.testTimeout,
2367
- shouldStop: ()=>options.signal.aborted,
2368
- stopReason: ()=>'interrupted',
2369
- isFatalError: (run)=>[
2370
- ...run.beforeEach,
2371
- ...run.steps,
2372
- ...run.afterEach
2373
- ].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
2374
- onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
2375
- onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
2376
- onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
2377
- });
2378
- } catch (error) {
2379
- executionError = error;
2380
- if (error instanceof WorkflowExecutionFailure) execution = error.result;
2381
- } finally{
2382
- const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
2383
- lifecycle = await runtime.finish(failed ? 'failed' : 'success');
2384
- }
2385
- if (executionError) {
2386
- if (!execution) throw executionError;
2387
- options.onInfrastructureError(executionError);
2388
- }
2389
- if (!execution) return toTransportResult({
2390
- case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
2391
- lifecycle
2392
- }, materializeReport, materializeSource);
2393
- const outcome = execution.cases[0];
2394
- if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
2395
- return toTransportResult({
2396
- case: {
2397
- ...outcome,
2398
- documentId: task.documentId
2399
- },
2400
- document: execution.document,
2401
- lifecycle
2402
- }, materializeReport, materializeSource);
2403
- };
2404
- const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
2405
- const { prepared, task } = preparedTask;
2406
- const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
2407
- taskProgress('started');
2408
- const outputDir = (0, external_node_path_.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
2409
- (0, external_node_fs_.mkdirSync)(outputDir, {
2410
- recursive: true
2411
- });
2412
- const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
2413
- if (prepared.collectionErrors.length > 0) return {
2414
- case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
2415
- };
2416
- const executor = options.executor ?? localTestExecutor;
2417
- let result;
2418
- let executorFailure;
2419
- let executorAttempts = 0;
2420
- for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
2421
- executorAttempts = attemptIndex + 1;
2422
- try {
2423
- result = await executor.execute(task, {
2424
- signal: options.signal,
2425
- onProgress: taskProgress,
2426
- outputDir,
2427
- materializeReport,
2428
- runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
2429
- });
2430
- executorFailure = void 0;
2431
- break;
2432
- } catch (error) {
2433
- if (!isTestExecutorError(error)) throw error;
2434
- executorFailure = error;
2435
- if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
2436
- taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
2437
- }
2438
- }
2439
- if (!result) {
2440
- if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
2441
- taskProgress(`executor-failed: ${executorFailure.message}`);
2442
- return {
2443
- case: {
2444
- ...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
2445
- execution: {
2446
- executor: executor.name,
2447
- resources: task.resources,
2448
- attempts: executorAttempts,
2449
- failure: {
2450
- kind: executorFailure.kind,
2451
- message: executorFailure.message,
2452
- retryable: executorFailure.retryable
2453
- }
2454
- }
2455
- }
2456
- };
2457
- }
2458
- assertTestCaseTaskRunResult(task, result);
2459
- const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
2460
- const completed = {
2461
- ...internalResult,
2462
- case: {
2463
- ...internalResult.case,
2464
- execution: {
2465
- executor: executor.name,
2466
- resources: task.resources,
2467
- attempts: executorAttempts,
2468
- ...internalResult.lifecycle ? {
2469
- lifecycle: internalResult.lifecycle
2470
- } : {},
2471
- ...result.artifacts ? {
2472
- artifacts: result.artifacts
2473
- } : {},
2474
- ...result.metadata ? {
2475
- metadata: result.metadata
2476
- } : {}
2477
- }
2478
- }
2479
- };
2480
- onCompleted(completed);
2481
- for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
2482
- if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
2483
- taskProgress(internalResult.case.status);
2484
- return completed;
2485
- };
2486
- const runCaseExecution = async (options)=>{
2487
- const tasks = prepareCaseTasks(options.projects);
2488
- let failedCaseCount = 0;
2489
- const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
2490
- const results = new Array(tasks.length);
2491
- if (tasks.length > 0) try {
2492
- await runTaskPool({
2493
- tasks: tasks.map((preparedTask)=>preparedTask.task),
2494
- maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
2495
- signal: options.signal,
2496
- shouldStop: bailReached,
2497
- run: async (_task, taskIndex)=>{
2498
- try {
2499
- const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
2500
- results[taskIndex] = completed;
2501
- });
2502
- results[taskIndex] = result;
2503
- if ('failed' === result.case.status) failedCaseCount += 1;
2504
- return result;
2505
- } catch (error) {
2506
- options.onInfrastructureError(error);
2507
- throw error;
2508
- }
2509
- }
2510
- });
2511
- } catch (error) {
2512
- options.onInfrastructureError(error);
2513
- }
2514
- return options.projects.map((prepared, projectIndex)=>{
2515
- const projectTasks = tasks.map((task, taskIndex)=>({
2516
- task,
2517
- result: results[taskIndex]
2518
- })).filter(({ task })=>task.projectIndex === projectIndex);
2519
- const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
2520
- const cases = projectTasks.map(({ task, result })=>{
2521
- if (result) return result.case;
2522
- if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
2523
- return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
2524
- });
2525
- const documents = projectTasks.flatMap(({ result })=>result?.document ? [
2526
- result.document
2527
- ] : []);
2528
- return buildProjectResult(prepared, cases, documents);
2529
- });
2530
- };
2531
1334
  const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
2532
1335
  ...attempt.beforeEach,
2533
1336
  ...attempt.steps,
@@ -2573,7 +1376,7 @@ async function executeDocumentInvocation(options) {
2573
1376
  },
2574
1377
  onStepResult: async (info, result)=>{
2575
1378
  await invocation.bindings.onStepResult?.(info, result);
2576
- options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
1379
+ options.onProgress(formatStepResult(info, result));
2577
1380
  },
2578
1381
  onCaseResult: async (attempt)=>{
2579
1382
  await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
@@ -2691,6 +1494,16 @@ function createOperationJournal(runDir) {
2691
1494
  }
2692
1495
  };
2693
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
+ });
2694
1507
  const summarize = (projects)=>{
2695
1508
  const healthCases = projects.flatMap((project)=>{
2696
1509
  const documentAttemptByRunId = new Map(project.documents.map((document)=>[
@@ -2724,6 +1537,12 @@ const summarize = (projects)=>{
2724
1537
  projectFailures: projects.filter((project)=>'failed' === project.status).length
2725
1538
  };
2726
1539
  };
1540
+ const latestById = (items, id)=>[
1541
+ ...new Map(items.map((item)=>[
1542
+ id(item),
1543
+ item
1544
+ ])).values()
1545
+ ];
2727
1546
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
2728
1547
  async function runPreparedTestProject(plan) {
2729
1548
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
@@ -2736,7 +1555,7 @@ async function runPreparedTestProject(plan) {
2736
1555
  await writeTestRunPlan(runDir, preparedProjects);
2737
1556
  const journal = createOperationJournal(runDir);
2738
1557
  const rootController = new AbortController();
2739
- 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);
2740
1559
  const handleSignal = (signal)=>{
2741
1560
  rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
2742
1561
  };
@@ -2778,6 +1597,31 @@ async function runPreparedTestProject(plan) {
2778
1597
  const { project } = prepared;
2779
1598
  runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
2780
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
+ };
2781
1625
  const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
2782
1626
  announceProject(prepared, projectIndex);
2783
1627
  return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
@@ -2878,7 +1722,7 @@ async function runPreparedTestProject(plan) {
2878
1722
  };
2879
1723
  const projectResults = new Array(preparedProjects.length);
2880
1724
  try {
2881
- if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await runConcurrentJobs(preparedProjects, {
1725
+ await runConcurrentJobs(preparedProjects, {
2882
1726
  concurrency: effectiveConcurrency,
2883
1727
  shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
2884
1728
  }, async (prepared, index)=>{
@@ -2889,20 +1733,6 @@ async function runPreparedTestProject(plan) {
2889
1733
  throw error;
2890
1734
  }
2891
1735
  });
2892
- else {
2893
- const results = await runCaseExecution({
2894
- projects: preparedProjects,
2895
- test: definition.test,
2896
- executor: definition.executor,
2897
- runDir,
2898
- signal: rootController.signal,
2899
- progress,
2900
- onInfrastructureError: recordInfrastructureError
2901
- });
2902
- results.forEach((result, index)=>{
2903
- projectResults[index] = result;
2904
- });
2905
- }
2906
1736
  } catch (error) {
2907
1737
  recordInfrastructureError(error);
2908
1738
  } finally{
@@ -3015,10 +1845,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3015
1845
  let configPath;
3016
1846
  let resultDir;
3017
1847
  const projectNames = [];
3018
- const paths = [];
3019
- const caseIds = [];
3020
- const includeTags = [];
3021
- const excludeTags = [];
3022
1848
  for(let index = commandOffset; index < args.length; index += 1){
3023
1849
  const token = args[index];
3024
1850
  const equal = token.startsWith('-') ? token.indexOf('=') : -1;
@@ -3029,22 +1855,17 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3029
1855
  projectRoot = (0, external_node_path_.resolve)(cwd, arg);
3030
1856
  continue;
3031
1857
  }
3032
- 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) {
3033
1859
  const value = inlineValue ?? args[index + 1];
3034
1860
  if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
3035
1861
  if ('--config' === arg) configPath = value;
3036
1862
  else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
3037
- else if ('--project' === arg) projectNames.push(value);
3038
- else if ('--file' === arg) paths.push(value);
3039
- else if ('--case-id' === arg) caseIds.push(value);
3040
- else if ('--tag' === arg) includeTags.push(value);
3041
- else excludeTags.push(value);
1863
+ else projectNames.push(value);
3042
1864
  if (void 0 === inlineValue) index += 1;
3043
1865
  } else throw new Error(`Unknown option: ${token}`);
3044
1866
  }
3045
1867
  if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
3046
1868
  if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
3047
- 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.');
3048
1869
  return {
3049
1870
  ...command ? {
3050
1871
  command
@@ -3055,22 +1876,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
3055
1876
  resultDir,
3056
1877
  ...projectNames.length > 0 ? {
3057
1878
  projectNames
3058
- } : {},
3059
- ...paths.length > 0 ? {
3060
- paths
3061
- } : {},
3062
- ...caseIds.length > 0 ? {
3063
- caseIds
3064
- } : {},
3065
- ...includeTags.length > 0 || excludeTags.length > 0 ? {
3066
- tags: {
3067
- ...includeTags.length > 0 ? {
3068
- include: includeTags
3069
- } : {},
3070
- ...excludeTags.length > 0 ? {
3071
- exclude: excludeTags
3072
- } : {}
3073
- }
3074
1879
  } : {}
3075
1880
  };
3076
1881
  };
@@ -3226,7 +2031,7 @@ async function runTestCli(args, io = defaultCliIO) {
3226
2031
  });
3227
2032
  }
3228
2033
  if ('create' === args[0]) {
3229
- 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"));
3230
2035
  await runCreateCommand(args.slice(1), io);
3231
2036
  return 0;
3232
2037
  }