@midscene/test 1.15.5-beta-20261015030228.0 → 1.15.5

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 (32) hide show
  1. package/dist/es/cli/index.mjs +27 -365
  2. package/dist/es/cli/index.mjs.map +1 -1
  3. package/dist/es/midscene/index.mjs +2 -2
  4. package/dist/es/midscene/index.mjs.map +1 -1
  5. package/dist/es/test-cli.mjs +18 -353
  6. package/dist/es/test-cli.mjs.map +1 -1
  7. package/dist/lib/cli/index.js +16 -357
  8. package/dist/lib/cli/index.js.map +1 -1
  9. package/dist/lib/midscene/index.js +3 -0
  10. package/dist/lib/midscene/index.js.map +1 -1
  11. package/dist/lib/test-cli.js +17 -352
  12. package/dist/lib/test-cli.js.map +1 -1
  13. package/dist/templates/test/android/README.md +0 -2
  14. package/dist/templates/test/android/README.zh.md +0 -2
  15. package/dist/templates/test/android/midscene.config.ts +31 -28
  16. package/dist/templates/test/computer/README.md +0 -2
  17. package/dist/templates/test/computer/README.zh.md +0 -2
  18. package/dist/templates/test/computer/midscene.config.ts +34 -31
  19. package/dist/templates/test/harmony/README.md +0 -2
  20. package/dist/templates/test/harmony/README.zh.md +0 -2
  21. package/dist/templates/test/harmony/midscene.config.ts +31 -28
  22. package/dist/templates/test/ios/README.md +0 -2
  23. package/dist/templates/test/ios/README.zh.md +0 -2
  24. package/dist/templates/test/ios/midscene.config.ts +31 -28
  25. package/dist/templates/test/web/README.md +0 -2
  26. package/dist/templates/test/web/README.zh.md +0 -2
  27. package/dist/templates/test/web/cases/todo.yaml +6 -3
  28. package/dist/templates/test/web/midscene.config.ts +12 -31
  29. package/dist/types/cli/index.d.ts +9 -30
  30. package/dist/types/index.d.ts +3 -3
  31. package/dist/types/midscene/index.d.ts +6 -2
  32. package/package.json +13 -13
@@ -1,7 +1,7 @@
1
- import { dirname, isAbsolute, join, relative, resolve as external_node_path_resolve, sep } from "node:path";
1
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { require as api_require } from "tsx/cjs/api";
4
- import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, getPendingScopeOperations, getResourceCleanupCompletion, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText, trackResourceOperation, waitForScopeOperations } from "@midscene/core/internal/test-runner";
4
+ import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText } from "@midscene/core/internal/test-runner";
5
5
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
6
  import { globSync } from "tinyglobby";
7
7
  import { createHash, randomUUID } from "node:crypto";
@@ -14,7 +14,6 @@ const registry_NodeRegistry = NodeRegistry;
14
14
  const projectIdFromIndex = (index)=>`project-${index}`;
15
15
  const defineTestProject = (definition)=>definition;
16
16
  const defineProjectSetup = (definition)=>definition;
17
- const defineFileSetup = (definition)=>definition;
18
17
  const getDefaultExport = (loaded, absolutePath)=>{
19
18
  if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
20
19
  const defaultExport = loaded.default;
@@ -131,14 +130,9 @@ const validateNonNegativeInteger = (value, fallback, label)=>{
131
130
  if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
132
131
  return value;
133
132
  };
134
- const validateFileConcurrency = (maxConcurrentFiles, fileSetup, label)=>{
135
- if (void 0 !== maxConcurrentFiles && maxConcurrentFiles > 1 && !fileSetup) throw new TypeError(`Midscene config ${label}.fileSetup is required when the effective maxConcurrentFiles is greater than 1.`);
136
- };
137
- const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, globalNodes, test)=>{
133
+ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
138
134
  if (void 0 === value) {
139
135
  const setup = validateSetup(defaultSetup, 'setup');
140
- const fileSetup = validateSetup(defaultFileSetup, 'fileSetup');
141
- validateFileConcurrency(test.maxConcurrentFiles, fileSetup, 'default');
142
136
  return {
143
137
  hasExplicitProjects: false,
144
138
  projects: Object.freeze([
@@ -148,12 +142,6 @@ const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, global
148
142
  ...setup ? {
149
143
  setup
150
144
  } : {},
151
- ...fileSetup ? {
152
- fileSetup
153
- } : {},
154
- ...void 0 === test.maxConcurrentFiles ? {} : {
155
- maxConcurrentFiles: test.maxConcurrentFiles
156
- },
157
145
  tags: Object.freeze({
158
146
  include: [],
159
147
  exclude: []
@@ -166,7 +154,6 @@ const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, global
166
154
  };
167
155
  }
168
156
  if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
169
- if (void 0 !== defaultFileSetup) throw new TypeError('Midscene config fileSetup cannot be used together with projects. Move fileSetup to projects[].fileSetup.');
170
157
  if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
171
158
  const names = new Set();
172
159
  const projects = value.map((candidate, index)=>{
@@ -175,8 +162,6 @@ const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, global
175
162
  rejectUnknownKeys(candidate, [
176
163
  'name',
177
164
  'setup',
178
- 'fileSetup',
179
- 'maxConcurrentFiles',
180
165
  'nodes',
181
166
  'files',
182
167
  'tags',
@@ -193,20 +178,10 @@ const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, global
193
178
  ...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
194
179
  ...localNodes.definitions()
195
180
  ]);
196
- const fileSetup = validateSetup(candidate.fileSetup, `${label}.fileSetup`);
197
- if (void 0 === test.maxConcurrentFiles && (void 0 !== fileSetup || void 0 !== candidate.maxConcurrentFiles)) throw new TypeError(`Midscene config ${label}.fileSetup/maxConcurrentFiles requires test.maxConcurrentFiles.`);
198
- const maxConcurrentFiles = void 0 === test.maxConcurrentFiles ? void 0 : Math.min(test.maxConcurrentFiles, validatePositiveInteger(candidate.maxConcurrentFiles, test.maxConcurrentFiles, `${label}.maxConcurrentFiles`));
199
- validateFileConcurrency(maxConcurrentFiles, fileSetup, label);
200
181
  return Object.freeze({
201
182
  projectId: projectIdFromIndex(index),
202
183
  name: candidate.name,
203
184
  nodes,
204
- ...fileSetup ? {
205
- fileSetup
206
- } : {},
207
- ...void 0 === maxConcurrentFiles ? {} : {
208
- maxConcurrentFiles
209
- },
210
185
  ...files ? {
211
186
  files
212
187
  } : {},
@@ -226,15 +201,7 @@ const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, global
226
201
  const validateTestOptions = (value)=>{
227
202
  if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
228
203
  const candidate = value ?? {};
229
- const fileMode = void 0 !== candidate.maxConcurrentFiles;
230
- if (fileMode && void 0 !== candidate.maxConcurrency) throw new TypeError('Midscene config test.maxConcurrentFiles cannot be combined with test.maxConcurrency.');
231
- if (!fileMode && (void 0 !== candidate.setupTimeout || void 0 !== candidate.teardownTimeout)) throw new TypeError('Midscene config test.setupTimeout/teardownTimeout requires test.maxConcurrentFiles.');
232
204
  return Object.freeze({
233
- ...fileMode ? {
234
- maxConcurrentFiles: validatePositiveInteger(candidate.maxConcurrentFiles, 1, 'test.maxConcurrentFiles'),
235
- setupTimeout: validatePositiveInteger(candidate.setupTimeout, 120000, 'test.setupTimeout'),
236
- teardownTimeout: validatePositiveInteger(candidate.teardownTimeout, 30000, 'test.teardownTimeout')
237
- } : {},
238
205
  maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
239
206
  bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
240
207
  testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
@@ -265,19 +232,16 @@ const validateTestProjectDefinition = (definition)=>{
265
232
  if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
266
233
  rejectUnknownKeys(definition, [
267
234
  'setup',
268
- 'fileSetup',
269
235
  'projects',
270
236
  'test',
271
237
  'output',
272
238
  'nodes'
273
239
  ], 'root');
274
240
  const nodes = new registry_NodeRegistry(definition.nodes);
275
- const test = validateTestOptions(definition.test);
276
- if (void 0 !== definition.fileSetup && void 0 === test.maxConcurrentFiles) throw new TypeError('Midscene config fileSetup requires test.maxConcurrentFiles.');
277
- const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, definition.fileSetup, nodes, test);
241
+ const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
278
242
  return {
279
243
  ...resolvedProjects,
280
- test,
244
+ test: validateTestOptions(definition.test),
281
245
  output: validateOutput(definition.output),
282
246
  nodes,
283
247
  resolveNode: (name)=>nodes.get(name)
@@ -299,7 +263,7 @@ async function loadTestProject(configPath) {
299
263
  if (!configPath) return validateTestProjectDefinition({
300
264
  nodes: []
301
265
  });
302
- const absolutePath = external_node_path_resolve(configPath);
266
+ const absolutePath = resolve(configPath);
303
267
  const extension = assertSupportedConfig(absolutePath);
304
268
  let loaded;
305
269
  try {
@@ -795,7 +759,7 @@ const DEFAULT_TEST_FILE_SELECTION = {
795
759
  };
796
760
  const project_preparation_toPosix = (value)=>value.split(sep).join('/');
797
761
  const discoverResolvedTestFiles = (projectRoot, selection)=>{
798
- const root = external_node_path_resolve(projectRoot);
762
+ const root = resolve(projectRoot);
799
763
  const match = (patterns)=>globSync(patterns, {
800
764
  absolute: true,
801
765
  caseSensitiveMatch: false,
@@ -811,7 +775,7 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
811
775
  }).filter((file)=>/\.ya?ml$/i.test(file));
812
776
  const files = match(selection.include);
813
777
  return [
814
- ...new Set(files.map((file)=>external_node_path_resolve(file)))
778
+ ...new Set(files.map((file)=>resolve(file)))
815
779
  ].sort((a, b)=>{
816
780
  const relativeA = project_preparation_toPosix(relative(root, a));
817
781
  const relativeB = project_preparation_toPosix(relative(root, b));
@@ -824,7 +788,7 @@ const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)
824
788
  return discoverResolvedTestFiles(projectRoot, normalized);
825
789
  };
826
790
  const discoverTestConfig = (projectRoot)=>{
827
- const root = external_node_path_resolve(projectRoot);
791
+ const root = resolve(projectRoot);
828
792
  const candidates = readdirSync(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
829
793
  const supported = candidates.filter((name)=>CONFIG_NAMES.includes(name));
830
794
  const unsupported = candidates.filter((name)=>!CONFIG_NAMES.includes(name));
@@ -878,7 +842,7 @@ const selectProjects = (projects, names)=>{
878
842
  async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, options = {}) {
879
843
  const platform = options.platform?.trim();
880
844
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
881
- const prerequisiteFile = options.prerequisiteFile ? external_node_path_resolve(options.prerequisiteFile) : void 0;
845
+ const prerequisiteFile = options.prerequisiteFile ? resolve(options.prerequisiteFile) : void 0;
882
846
  const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
883
847
  const files = [
884
848
  ...prerequisiteFile ? [
@@ -956,24 +920,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
956
920
  defaultTimeoutMs,
957
921
  reportEnabled: true,
958
922
  bindings: {
959
- resolveNode: project.nodes.require.bind(project.nodes),
960
- ...project.fileSetup ? {
961
- documentSetup: {
962
- name: project.fileSetup.name,
963
- setup ({ projectContext, document, signal, onTeardown }) {
964
- return project.fileSetup.setup({
965
- project,
966
- projectContext,
967
- file: document,
968
- env: Object.freeze({
969
- ...process.env
970
- }),
971
- signal,
972
- onTeardown
973
- });
974
- }
975
- }
976
- } : {}
923
+ resolveNode: project.nodes.require.bind(project.nodes)
977
924
  }
978
925
  });
979
926
  } catch (error) {
@@ -1019,9 +966,9 @@ const assertDirectory = (path, label)=>{
1019
966
  if (!existsSync(path) || !statSync(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
1020
967
  };
1021
968
  const resolveRunInput = (options)=>{
1022
- const cwd = external_node_path_resolve(options.cwd ?? process.cwd());
969
+ const cwd = resolve(options.cwd ?? process.cwd());
1023
970
  assertDirectory(cwd, 'Test working directory');
1024
- const inputPath = options.projectRoot ? external_node_path_resolve(cwd, options.projectRoot) : void 0;
971
+ const inputPath = options.projectRoot ? resolve(cwd, options.projectRoot) : void 0;
1025
972
  const singleFile = inputPath && existsSync(inputPath) && statSync(inputPath).isFile() ? inputPath : void 0;
1026
973
  if (singleFile && !/\.ya?ml$/i.test(singleFile)) throw new Error(`Test input must be a YAML file or project directory: ${singleFile}`);
1027
974
  const cliProjectRoot = singleFile ? dirname(singleFile) : inputPath;
@@ -1040,14 +987,14 @@ async function prepareTestRun(options) {
1040
987
  const startedAt = new Date();
1041
988
  const runId = createTestRunId(startedAt);
1042
989
  const input = resolveRunInput(options);
1043
- const configPath = options.configPath ? external_node_path_resolve(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
990
+ const configPath = options.configPath ? resolve(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
1044
991
  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}\`.`);
1045
992
  if (options.configPath && !existsSync(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
1046
993
  const definition = await loadTestProject(configPath);
1047
- const resultDir = options.resultDir ? external_node_path_resolve(input.cwd, options.resultDir) : join(input.projectRoot, '.midscene', 'test-results');
994
+ const resultDir = options.resultDir ? resolve(input.cwd, options.resultDir) : join(input.projectRoot, '.midscene', 'test-results');
1048
995
  const runDir = join(resultDir, runId);
1049
996
  const summaryPath = join(runDir, 'summary.json');
1050
- const reportDir = external_node_path_resolve(input.projectRoot, definition.output.reportDir);
997
+ const reportDir = resolve(input.projectRoot, definition.output.reportDir);
1051
998
  mkdirSync(resultDir, {
1052
999
  recursive: true
1053
1000
  });
@@ -1176,40 +1123,11 @@ const documentHasFatalError = (result)=>[
1176
1123
  ...result.beforeAll,
1177
1124
  ...result.afterAll
1178
1125
  ].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError) || !!result.executionErrors?.some(isResourceRecoveryError);
1179
- const isSharedResourceFailure = (error)=>{
1180
- const seen = new Set();
1181
- const pending = [
1182
- error
1183
- ];
1184
- while(pending.length && seen.size < 50){
1185
- const item = pending.pop();
1186
- if ('object' != typeof item || null === item || seen.has(item)) continue;
1187
- seen.add(item);
1188
- const failure = item;
1189
- const message = 'string' == typeof failure.message ? failure.message.trim() : '';
1190
- if ('PROJECT_RESOURCE_ERROR' === failure.code || /^(?:browser\.[\w]+:.*(?:closed|disconnected)|browser (?:has been |was |is )?(?:closed|disconnected)|browser connection.*closed)/i.test(message)) return true;
1191
- if (failure.cause) pending.push(failure.cause);
1192
- if (Array.isArray(failure.errors)) pending.push(...failure.errors.slice(0, 50));
1193
- }
1194
- return false;
1195
- };
1196
1126
  async function executeDocumentInvocation(options) {
1197
1127
  const { invocation, project, sinks } = options;
1198
1128
  const { document } = invocation;
1199
1129
  const documentRetry = 'document' === invocation.retry.scope;
1200
1130
  const attempts = [];
1201
- const fileSetup = invocation.bindings.documentSetup;
1202
- const documentSetup = options.onUnresolvedResources && fileSetup ? {
1203
- ...fileSetup,
1204
- async setup (ctx) {
1205
- try {
1206
- return await fileSetup.setup(ctx);
1207
- } catch (error) {
1208
- if (isSharedResourceFailure(error)) options.markProjectFatal();
1209
- throw error;
1210
- }
1211
- }
1212
- } : fileSetup;
1213
1131
  await runDocumentAttempts({
1214
1132
  retry: documentRetry ? invocation.retry.count : 0,
1215
1133
  signal: options.signal,
@@ -1220,9 +1138,6 @@ async function executeDocumentInvocation(options) {
1220
1138
  plan: invocation.plan,
1221
1139
  documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
1222
1140
  ...invocation.bindings,
1223
- documentSetup,
1224
- setupTimeoutMs: options.setupTimeoutMs,
1225
- teardownTimeoutMs: options.teardownTimeoutMs,
1226
1141
  project,
1227
1142
  projectContext: options.projectContext,
1228
1143
  retry: documentRetry ? 0 : invocation.retry.count,
@@ -1234,7 +1149,7 @@ async function executeDocumentInvocation(options) {
1234
1149
  ...run.beforeEach,
1235
1150
  ...run.steps,
1236
1151
  ...run.afterEach
1237
- ].some((step)=>step.error && (isFatalDeviceError(step.error) || !!options.onUnresolvedResources && isSharedResourceFailure(step.error))) || (run.teardownErrors ?? []).some((error)=>isFatalDeviceError(error) || !!options.onUnresolvedResources && isSharedResourceFailure(error)),
1152
+ ].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
1238
1153
  onCaseStart: (collectedCase)=>{
1239
1154
  options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
1240
1155
  },
@@ -1243,7 +1158,6 @@ async function executeDocumentInvocation(options) {
1243
1158
  options.onProgress(`${indent}→ ${formatStep(info)}`);
1244
1159
  },
1245
1160
  onStepResult: async (info, result)=>{
1246
- if (options.onUnresolvedResources && isSharedResourceFailure(result.error)) options.markProjectFatal();
1247
1161
  await invocation.bindings.onStepResult?.(info, result);
1248
1162
  options.onProgress(formatStepResult(info, result));
1249
1163
  },
@@ -1252,12 +1166,7 @@ async function executeDocumentInvocation(options) {
1252
1166
  options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
1253
1167
  },
1254
1168
  onCaseOutcome: (outcome)=>{
1255
- if (options.onUnresolvedResources && outcome.attempts?.some((attempt)=>[
1256
- ...attempt.beforeEach,
1257
- ...attempt.steps,
1258
- ...attempt.afterEach
1259
- ].some((step)=>isSharedResourceFailure(step.error)) || attempt.teardownErrors?.some(isSharedResourceFailure))) options.markProjectFatal();
1260
- if (!options.isolatedContext && caseHasFatalError(outcome)) options.markProjectFatal();
1169
+ if (caseHasFatalError(outcome)) options.markProjectFatal();
1261
1170
  if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
1262
1171
  },
1263
1172
  onDocumentResult: options.onDocumentResult
@@ -1266,7 +1175,7 @@ async function executeDocumentInvocation(options) {
1266
1175
  const partial = error.result;
1267
1176
  const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>isResourceRecoveryError(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
1268
1177
  if (resourceOnly) {
1269
- if (!options.isolatedContext && error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
1178
+ if (error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
1270
1179
  return partial;
1271
1180
  }
1272
1181
  if (documentRetry) partial.document.retryDecision = decideRetry({
@@ -1295,17 +1204,7 @@ async function executeDocumentInvocation(options) {
1295
1204
  }
1296
1205
  throw error;
1297
1206
  });
1298
- if (!options.isolatedContext && documentHasFatalError(execution.document)) options.markProjectFatal();
1299
- if (options.onUnresolvedResources && [
1300
- ...execution.document.beforeAll,
1301
- ...execution.document.afterAll,
1302
- ...(execution.document.hostErrors ?? []).map(({ error })=>({
1303
- error
1304
- })),
1305
- ...(execution.document.teardownErrors ?? []).map((error)=>({
1306
- error
1307
- }))
1308
- ].some(({ error })=>isSharedResourceFailure(error))) options.markProjectFatal();
1207
+ if (documentHasFatalError(execution.document)) options.markProjectFatal();
1309
1208
  const owners = [
1310
1209
  execution.document,
1311
1210
  ...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
@@ -1313,10 +1212,7 @@ async function executeDocumentInvocation(options) {
1313
1212
  const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
1314
1213
  const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
1315
1214
  const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
1316
- if (cleanupPending || operationPending) {
1317
- options.markProjectFatal();
1318
- options.onUnresolvedResources?.();
1319
- } else if (cleanupFailed && !options.isolatedContext) options.markProjectFatal();
1215
+ if (cleanupPending || cleanupFailed || operationPending) options.markProjectFatal();
1320
1216
  const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
1321
1217
  const decision = decideRetry({
1322
1218
  scope: 'document',
@@ -1369,212 +1265,6 @@ async function executeDocumentInvocation(options) {
1369
1265
  if (documentRetry && failedCases) options.addFailedCases(failedCases);
1370
1266
  else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
1371
1267
  }
1372
- async function runPreparedFiles(options) {
1373
- const states = options.projects.map((prepared)=>({
1374
- prepared,
1375
- cases: [],
1376
- documents: [],
1377
- cursor: 0,
1378
- active: 0,
1379
- initializing: false,
1380
- finished: false,
1381
- fatal: false,
1382
- setupFailed: false
1383
- }));
1384
- let nextProject = 0;
1385
- let resourcesUnresolved = false;
1386
- let occupied = 0;
1387
- const cleanupQueue = [];
1388
- const release = ()=>{
1389
- occupied--;
1390
- while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
1391
- occupied++;
1392
- cleanupQueue.shift()();
1393
- }
1394
- wake?.();
1395
- };
1396
- let wake;
1397
- const running = new Set();
1398
- const stopped = (state)=>options.signal.aborted || options.shouldBail() || state.fatal;
1399
- const invalid = (state)=>options.preflightFailed || state.prepared.collectionErrors.length > 0;
1400
- const unresolved = ()=>{
1401
- if (resourcesUnresolved) return;
1402
- resourcesUnresolved = true;
1403
- options.onInfrastructureError(new WorkflowError('Resources are still running or cleanup is pending. No new test files will start.', {
1404
- code: 'UNRESOLVED_FILE_RESOURCES'
1405
- }));
1406
- };
1407
- const finishProject = async (state)=>{
1408
- if (!state.runtime || state.finished) return;
1409
- state.finished = true;
1410
- const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
1411
- state.lifecycle = await state.runtime.finish(failed ? 'failed' : 'success');
1412
- if (state.lifecycle.cleanup?.status === 'pending' || state.lifecycle.pendingOperations?.length) unresolved();
1413
- };
1414
- const admit = ()=>{
1415
- for(let offset = 0; offset < states.length; offset++){
1416
- const index = (nextProject + offset) % states.length;
1417
- const state = states[index];
1418
- if (invalid(state) || state.finished || state.initializing) continue;
1419
- const cleanupOnly = !!state.runtime && 0 === state.active && (stopped(state) || state.cursor === state.prepared.invocations.length);
1420
- if (!cleanupOnly && (stopped(state) || state.cursor >= state.prepared.invocations.length || state.active >= (state.prepared.project.maxConcurrentFiles ?? options.maxConcurrentFiles))) continue;
1421
- nextProject = (index + 1) % states.length;
1422
- occupied++;
1423
- state.active++;
1424
- if (!state.runtime && !cleanupOnly) state.initializing = true;
1425
- const invocationIndex = cleanupOnly ? -1 : state.cursor++;
1426
- return {
1427
- state,
1428
- index,
1429
- invocationIndex
1430
- };
1431
- }
1432
- };
1433
- const run = async ({ state, index, invocationIndex })=>{
1434
- const { prepared } = state;
1435
- const { project } = prepared;
1436
- let fileResourcesPending = false;
1437
- try {
1438
- if (invocationIndex < 0) return void await finishProject(state);
1439
- if (!state.runtime) {
1440
- state.runtime = createProjectRuntime({
1441
- project,
1442
- setup: project.setup,
1443
- signal: options.signal,
1444
- setupTimeoutMs: options.setupTimeoutMs,
1445
- teardownTimeoutMs: options.teardownTimeoutMs
1446
- });
1447
- setMaxListeners(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
1448
- options.announceProject(prepared, index);
1449
- await state.runtime.start();
1450
- state.initializing = false;
1451
- if (!state.runtime.canRun) {
1452
- state.fatal = true;
1453
- state.setupFailed = true;
1454
- }
1455
- wake?.();
1456
- }
1457
- if (stopped(state)) return;
1458
- const invocation = prepared.invocations[invocationIndex];
1459
- options.onProgress(prepared, ` [document ${invocationIndex + 1}/${prepared.invocations.length}] ${invocation.document.sourcePath}`);
1460
- await executeDocumentInvocation({
1461
- invocation,
1462
- project,
1463
- projectContext: state.runtime.context,
1464
- runDir: options.runDir,
1465
- signal: state.runtime.signal,
1466
- rootSignal: options.signal,
1467
- shouldBail: options.shouldBail,
1468
- isProjectFatal: ()=>state.fatal,
1469
- markProjectFatal: ()=>{
1470
- if (state.fatal) return;
1471
- state.fatal = true;
1472
- state.runtime?.abort(new WorkflowError(`Project "${project.name}" shared execution environment is unavailable.`, {
1473
- code: 'PROJECT_RESOURCE_ERROR'
1474
- }));
1475
- },
1476
- addFailedCases: options.addFailedCases,
1477
- onProgress: (message)=>options.onProgress(prepared, message),
1478
- sinks: state,
1479
- isolatedContext: void 0 !== project.fileSetup,
1480
- setupTimeoutMs: options.setupTimeoutMs,
1481
- teardownTimeoutMs: options.teardownTimeoutMs,
1482
- onUnresolvedResources: ()=>{
1483
- fileResourcesPending = true;
1484
- unresolved();
1485
- }
1486
- });
1487
- } catch (error) {
1488
- state.fatal = true;
1489
- fileResourcesPending = !!state.runtime && getPendingScopeOperations(state.runtime.signal).length > 0;
1490
- options.onInfrastructureError(error);
1491
- } finally{
1492
- state.initializing = false;
1493
- try {
1494
- if (1 === state.active && (stopped(state) || state.cursor === prepared.invocations.length)) await finishProject(state);
1495
- } catch (error) {
1496
- options.onInfrastructureError(error);
1497
- }
1498
- state.active--;
1499
- if (state.runtime && (fileResourcesPending || state.lifecycle?.cleanup?.status === 'pending')) waitForScopeOperations(state.runtime.signal).then(release, (error)=>{
1500
- options.onInfrastructureError(error);
1501
- release();
1502
- });
1503
- else release();
1504
- wake?.();
1505
- }
1506
- };
1507
- while(true){
1508
- const changed = new Promise((resolve)=>{
1509
- wake = resolve;
1510
- });
1511
- while(occupied < options.maxConcurrentFiles){
1512
- const job = admit();
1513
- if (!job) break;
1514
- const task = run(job);
1515
- running.add(task);
1516
- const settled = ()=>{
1517
- running.delete(task);
1518
- wake?.();
1519
- };
1520
- task.then(settled, (error)=>{
1521
- options.onInfrastructureError(error);
1522
- settled();
1523
- });
1524
- }
1525
- if (!running.size) break;
1526
- await changed;
1527
- }
1528
- wake = void 0;
1529
- await Promise.all(states.map(async (state)=>{
1530
- if (!state.runtime || state.finished) return;
1531
- const admission = new Promise((resolve)=>{
1532
- cleanupQueue.push(resolve);
1533
- });
1534
- trackResourceOperation({}, admission, state.runtime.signal);
1535
- while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
1536
- occupied++;
1537
- cleanupQueue.shift()();
1538
- }
1539
- try {
1540
- await finishProject(state);
1541
- } catch (error) {
1542
- options.onInfrastructureError(error);
1543
- } finally{
1544
- Promise.all([
1545
- admission,
1546
- getResourceCleanupCompletion(state.runtime.signal)
1547
- ]).then(release, (error)=>{
1548
- options.onInfrastructureError(error);
1549
- release();
1550
- });
1551
- }
1552
- }));
1553
- for (const state of states){
1554
- const { prepared } = state;
1555
- if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
1556
- const completed = new Set(state.cases.map((item)=>item.caseId));
1557
- const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
1558
- for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
1559
- documentId: document.documentId,
1560
- caseId: item.caseId,
1561
- projectName: prepared.project.name,
1562
- name: item.definition.name,
1563
- sourcePath: item.sourcePath,
1564
- caseIndex: item.caseIndex,
1565
- status: 'not-run',
1566
- notRunReason: reason
1567
- });
1568
- const order = new Map(prepared.documents.map((document, i)=>[
1569
- document.documentId,
1570
- i
1571
- ]));
1572
- const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
1573
- state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
1574
- state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
1575
- }
1576
- return states;
1577
- }
1578
1268
  function createOperationJournal(runDir) {
1579
1269
  const path = join(runDir, 'operations.jsonl');
1580
1270
  writeFileSync(path, '', {
@@ -1644,7 +1334,7 @@ async function runPreparedTestProject(plan) {
1644
1334
  const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
1645
1335
  const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
1646
1336
  progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
1647
- const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? definition.test.maxConcurrency, void 0 === definition.test.maxConcurrentFiles ? preparedProjects.length : Math.max(1, totalDocuments));
1337
+ const effectiveConcurrency = Math.min(definition.test.maxConcurrency, preparedProjects.length);
1648
1338
  await writeTestRunPlan(runDir, preparedProjects);
1649
1339
  const journal = createOperationJournal(runDir);
1650
1340
  const rootController = new AbortController();
@@ -1658,18 +1348,6 @@ async function runPreparedTestProject(plan) {
1658
1348
  process.on('SIGTERM', sigterm);
1659
1349
  let failedCaseCount = 0;
1660
1350
  const bailReached = ()=>definition.test.bail > 0 && failedCaseCount >= definition.test.bail;
1661
- let shutdownDeadline;
1662
- const beginShutdown = ()=>{
1663
- if (void 0 !== definition.test.maxConcurrentFiles) shutdownDeadline ??= Date.now() + definition.test.teardownTimeout;
1664
- };
1665
- rootController.signal.addEventListener('abort', beginShutdown, {
1666
- once: true
1667
- });
1668
- const addFailedCases = (count)=>{
1669
- failedCaseCount += count;
1670
- if (bailReached()) beginShutdown();
1671
- };
1672
- const teardownTimeoutMs = ()=>Math.max(0, Math.min(definition.test.teardownTimeout, void 0 === shutdownDeadline ? 1 / 0 : shutdownDeadline - Date.now()));
1673
1351
  let hasInfrastructureError = false;
1674
1352
  const infrastructureErrors = [];
1675
1353
  const recordInfrastructureError = (error)=>{
@@ -1772,7 +1450,9 @@ async function runPreparedTestProject(plan) {
1772
1450
  markProjectFatal: ()=>{
1773
1451
  projectFatal = true;
1774
1452
  },
1775
- addFailedCases,
1453
+ addFailedCases: (count)=>{
1454
+ failedCaseCount += count;
1455
+ },
1776
1456
  onProgress: projectProgress,
1777
1457
  sinks: {
1778
1458
  cases,
@@ -1825,25 +1505,7 @@ async function runPreparedTestProject(plan) {
1825
1505
  };
1826
1506
  const projectResults = new Array(preparedProjects.length);
1827
1507
  try {
1828
- if (void 0 !== definition.test.maxConcurrentFiles) {
1829
- const states = await runPreparedFiles({
1830
- projects: preparedProjects,
1831
- maxConcurrentFiles: definition.test.maxConcurrentFiles,
1832
- setupTimeoutMs: definition.test.setupTimeout,
1833
- teardownTimeoutMs,
1834
- signal: rootController.signal,
1835
- runDir,
1836
- preflightFailed: 'run' === plan.preflightScope && totalErrors > 0,
1837
- shouldBail: bailReached,
1838
- addFailedCases,
1839
- announceProject,
1840
- onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
1841
- onInfrastructureError: recordInfrastructureError
1842
- });
1843
- states.forEach((state, index)=>{
1844
- projectResults[index] = buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle);
1845
- });
1846
- } else await runConcurrentJobs(preparedProjects, {
1508
+ await runConcurrentJobs(preparedProjects, {
1847
1509
  concurrency: effectiveConcurrency,
1848
1510
  shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
1849
1511
  }, async (prepared, index)=>{
@@ -1959,6 +1621,6 @@ async function runPreparedTestProject(plan) {
1959
1621
  async function runTestProject(options = {}) {
1960
1622
  return runPreparedTestProject(await prepareTestRun(options));
1961
1623
  }
1962
- export { DEFAULT_TEST_FILE_SELECTION, createTestRunId, defineFileSetup, defineProjectSetup, defineTestProject, discoverTestConfig, discoverTestFiles, loadTestProject, runTestProject, validateTestFileSelection };
1624
+ export { DEFAULT_TEST_FILE_SELECTION, createTestRunId, defineProjectSetup, defineTestProject, discoverTestConfig, discoverTestFiles, loadTestProject, runTestProject, validateTestFileSelection };
1963
1625
 
1964
1626
  //# sourceMappingURL=index.mjs.map