agent-scenario-loop 0.1.3 → 0.1.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 (52) hide show
  1. package/app/profile-session.ts +263 -17
  2. package/dist/core/artifact-contract.d.ts +6 -4
  3. package/dist/core/artifact-contract.js +164 -15
  4. package/dist/core/artifact-layout.d.ts +2 -0
  5. package/dist/core/artifact-layout.js +2 -0
  6. package/dist/core/planner.js +4 -3
  7. package/dist/core/schema-validator.d.ts +1 -0
  8. package/dist/core/schema-validator.js +1 -0
  9. package/dist/runner/android-adb-driver.d.ts +7 -2
  10. package/dist/runner/android-adb-driver.js +7 -1
  11. package/dist/runner/android-adb.d.ts +40 -5
  12. package/dist/runner/android-adb.js +1046 -664
  13. package/dist/runner/ios-simctl.d.ts +1 -0
  14. package/dist/runner/ios-simctl.js +1 -0
  15. package/dist/runner/profile-android.d.ts +11 -1
  16. package/dist/runner/profile-android.js +266 -25
  17. package/dist/runner/profile-ios.d.ts +3 -2
  18. package/dist/runner/profile-ios.js +252 -22
  19. package/dist/runner/profile-mobile.d.ts +63 -4
  20. package/dist/runner/profile-mobile.js +1002 -20
  21. package/dist/runner/validate-project.js +3 -0
  22. package/dist/scripts/consumer-rehearsal.d.ts +127 -0
  23. package/dist/scripts/consumer-rehearsal.js +774 -0
  24. package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
  25. package/dist/scripts/downstream-local-package-gate.js +264 -0
  26. package/dist/scripts/package-smoke.d.ts +104 -0
  27. package/dist/scripts/package-smoke.js +2304 -0
  28. package/dist/scripts/release-check.d.ts +47 -0
  29. package/dist/scripts/release-check.js +117 -0
  30. package/dist/scripts/release-readiness.d.ts +2 -0
  31. package/dist/scripts/release-readiness.js +539 -0
  32. package/docs/adapters.md +3 -1
  33. package/docs/api.md +2 -2
  34. package/docs/authoring.md +34 -2
  35. package/docs/consumer-rehearsal.md +33 -1
  36. package/docs/contracts.md +16 -2
  37. package/docs/live-proofs.md +12 -4
  38. package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
  39. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
  40. package/examples/runners/README.md +3 -3
  41. package/examples/runners/axe-accessibility-provider.json +2 -2
  42. package/examples/runners/script-accessibility-provider.json +2 -2
  43. package/examples/runners/script-memory-provider.json +2 -2
  44. package/examples/runners/script-network-provider.json +2 -2
  45. package/examples/runners/script-profiler-provider.json +2 -2
  46. package/package.json +12 -4
  47. package/schemas/manifest.schema.json +73 -3
  48. package/schemas/profiler.schema.json +243 -0
  49. package/schemas/runner-capabilities.schema.json +8 -2
  50. package/schemas/scenario.schema.json +18 -2
  51. package/templates/evidence-provider.json +3 -3
  52. package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
@@ -50,6 +50,7 @@ type IosProfileSessionStorageCommand = {
50
50
  sequence?: number;
51
51
  timestamp?: number;
52
52
  waitForMilestone?: string;
53
+ waitMs?: number;
53
54
  waitTimeoutMs?: number;
54
55
  };
55
56
  type IosProfileSessionStorageSeed = {
@@ -462,6 +462,7 @@ async function seedProfileSessionStorage({ bundleId, commands = [], dataContaine
462
462
  ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
463
463
  timestamp: typeof profileCommand.timestamp === 'number' ? profileCommand.timestamp : startedAt + index + 1,
464
464
  ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
465
+ ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
465
466
  ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
466
467
  }));
467
468
  manifest[profileStorageKeys.session] = JSON.stringify(session);
@@ -35,6 +35,16 @@ declare function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnable
35
35
  profileSessionEnabled: boolean;
36
36
  scenario: Record<string, any>;
37
37
  }): number;
38
+ /**
39
+ * Derives a bounded logcat tail large enough to keep command-session evidence.
40
+ *
41
+ * @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
42
+ * @returns {number}
43
+ */
44
+ declare function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }: {
45
+ commands: AndroidAdbProfileCommand[];
46
+ profileSessionEnabled: boolean;
47
+ }): number;
38
48
  /**
39
49
  * Expands normalized scenario evidence steps into Android adb driver actions.
40
50
  *
@@ -84,4 +94,4 @@ declare function runProfileAndroid(args: import('./profile-mobile').CliArgs, opt
84
94
  * @returns {Promise<void>}
85
95
  */
86
96
  declare function main(): Promise<void>;
87
- export { deriveProfileSessionCaptureWaitMs, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
97
+ export { deriveProfileSessionCaptureWaitMs, deriveProfileSessionLogcatLines, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.usage = exports.parseArgs = void 0;
5
5
  exports.deriveProfileSessionCaptureWaitMs = deriveProfileSessionCaptureWaitMs;
6
+ exports.deriveProfileSessionLogcatLines = deriveProfileSessionLogcatLines;
6
7
  exports.main = main;
7
8
  exports.resolveAndroidAdbProfileCommands = resolveAndroidAdbProfileCommands;
8
9
  exports.resolveAndroidAdbDriverSteps = resolveAndroidAdbDriverSteps;
@@ -16,7 +17,7 @@ const fs = require('node:fs');
16
17
  const path = require('node:path');
17
18
  const { hasHelpFlag } = require('./cli');
18
19
  const { ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, parsePositiveInteger, runAndroidAdbPreflight, } = require('./android-adb');
19
- const { parseArgs, readScalarArg, runProfileMobile, usage, } = require('./profile-mobile');
20
+ const { parseArgs, readScalarArg, resolveArtifactRoot, resolveProfileScenarioName, runProfileCompatibilityPreflight, runProfileMobile, usage, } = require('./profile-mobile');
20
21
  exports.parseArgs = parseArgs;
21
22
  exports.usage = usage;
22
23
  const { buildScenarioExecutionPlan } = require('../core/execution-plan');
@@ -24,8 +25,12 @@ const { runAgentDeviceCapture } = require('./agent-device');
24
25
  const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
25
26
  const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
26
27
  const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
28
+ const PROFILE_SESSION_LOGCAT_MIN_LINES = 1000;
29
+ const PROFILE_SESSION_LOGCAT_MAX_LINES = 20000;
30
+ const PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND = 300;
27
31
  const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
28
32
  const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
33
+ const DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN = 'Running "main"';
29
34
  const MANIFEST_LIFECYCLE_PHASES = new Set([
30
35
  'cold-launch',
31
36
  'warm-launch',
@@ -41,6 +46,17 @@ const MANIFEST_LIFECYCLE_PHASES = new Set([
41
46
  'reboot',
42
47
  'relaunch',
43
48
  ]);
49
+ const ANDROID_PROFILE_RUNNER_CAPABILITIES = {
50
+ schemaVersion: '1.0.0',
51
+ runnerId: 'android-adb-profile-runner',
52
+ kind: 'primary',
53
+ platforms: ['android'],
54
+ capabilities: ['launch', 'sessionControl', 'command', 'logCapture', 'artifactWrite'],
55
+ driverActions: ['tap', 'scroll', 'assertVisible', 'inspectTree', 'screenshot', 'record', 'readLogs'],
56
+ artifactOutputs: ['logs', 'signals', 'screenshot', 'video', 'uiTree'],
57
+ uiContexts: ['app'],
58
+ lifecycle: ['prepare', 'launch', 'startSession', 'executeStep', 'waitForTruthEvent', 'captureEvidence', 'stopSession', 'finalize'],
59
+ };
44
60
  /**
45
61
  * Reads and parses a JSON object from disk.
46
62
  *
@@ -150,7 +166,7 @@ function resolveAndroidPackageName({ args, config, }) {
150
166
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
151
167
  * @returns {string}
152
168
  */
153
- function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
169
+ function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }) {
154
170
  const scheme = typeof config.app?.profileSessionScheme === 'string'
155
171
  ? config.app.profileSessionScheme
156
172
  : typeof config.app?.scheme === 'string'
@@ -171,6 +187,9 @@ function buildProfileSessionUrl({ action, command, commandId, config, queueId, r
171
187
  if (waitForMilestone) {
172
188
  params.set('waitForMilestone', waitForMilestone);
173
189
  }
190
+ if (typeof waitMs === 'number') {
191
+ params.set('waitMs', String(waitMs));
192
+ }
174
193
  if (typeof waitTimeoutMs === 'number') {
175
194
  params.set('waitTimeoutMs', String(waitTimeoutMs));
176
195
  }
@@ -184,11 +203,10 @@ function buildProfileSessionUrl({ action, command, commandId, config, queueId, r
184
203
  * @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
185
204
  */
186
205
  function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
187
- const timestampBase = Date.now();
188
206
  const storedCommands = commands.map((profileCommand, index) => {
189
- const timestamp = timestampBase + index + 1;
207
+ const timestampPlaceholder = `${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}+${index + 1}`;
190
208
  return {
191
- id: `${timestamp}-${scenario}-${profileCommand.command}`,
209
+ id: `${scenario}-${index + 1}-${profileCommand.command}`,
192
210
  scenario,
193
211
  runId,
194
212
  command: profileCommand.command,
@@ -196,8 +214,9 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
196
214
  ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
197
215
  ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
198
216
  ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
217
+ ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
199
218
  ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
200
- timestamp,
219
+ timestamp: timestampPlaceholder,
201
220
  };
202
221
  });
203
222
  const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
@@ -274,6 +293,32 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
274
293
  }
275
294
  return profileSessionEnabled ? deriveProfileSessionCaptureWaitMs(scenario) : 0;
276
295
  }
296
+ /**
297
+ * Derives a bounded logcat tail large enough to keep command-session evidence.
298
+ *
299
+ * @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
300
+ * @returns {number}
301
+ */
302
+ function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }) {
303
+ if (!profileSessionEnabled || commands.length === 0) {
304
+ return PROFILE_SESSION_LOGCAT_MIN_LINES;
305
+ }
306
+ const derivedLines = PROFILE_SESSION_LOGCAT_MIN_LINES + (commands.length * PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND);
307
+ return Math.min(Math.max(derivedLines, PROFILE_SESSION_LOGCAT_MIN_LINES), PROFILE_SESSION_LOGCAT_MAX_LINES);
308
+ }
309
+ /**
310
+ * Resolves Android logcat capture lines, keeping explicit CLI input authoritative.
311
+ *
312
+ * @param {{args: import('./profile-mobile').CliArgs, commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
313
+ * @returns {number}
314
+ */
315
+ function resolveProfileSessionLogcatLines({ args, commands, profileSessionEnabled, }) {
316
+ const explicitLogcatLines = readScalarArg(args['logcat-lines']);
317
+ if (explicitLogcatLines !== undefined) {
318
+ return parsePositiveInteger(explicitLogcatLines, PROFILE_SESSION_LOGCAT_MIN_LINES);
319
+ }
320
+ return deriveProfileSessionLogcatLines({ commands, profileSessionEnabled });
321
+ }
277
322
  /**
278
323
  * Reads Android adb adapter metadata from a normalized scenario step.
279
324
  *
@@ -341,16 +386,182 @@ function resolveExecutionPlanProfileCommands(scenario) {
341
386
  waitMs: readStepWaitMs(step),
342
387
  ...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
343
388
  ? {
344
- waitForMilestone: nextStep.milestone,
389
+ waitForMilestone: resolveMilestoneEventName(scenario, nextStep.milestone),
345
390
  waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
346
391
  }
347
392
  : {}),
348
393
  });
349
394
  }
350
- return Array.from({ length: repeat }).flatMap((_, iteration) => commands.map((command, commandIndex) => ({
395
+ return expandProfileCommandCycles(scenario, commands, repeat);
396
+ }
397
+ /**
398
+ * Returns true when a command is part of the setup prefix that establishes app readiness before repeated cycle work.
399
+ *
400
+ * @param {Record<string, unknown>} scenario
401
+ * @param {AndroidAdbProfileCommand} command
402
+ * @returns {boolean}
403
+ */
404
+ function isReadinessSetupProfileCommand(scenario, command) {
405
+ if (typeof command.waitForMilestone !== 'string') {
406
+ return false;
407
+ }
408
+ const readyEvent = resolveScenarioReadinessEvent(scenario);
409
+ return typeof readyEvent === 'string' && command.waitForMilestone === readyEvent;
410
+ }
411
+ /**
412
+ * Reads a string id list from scenario cycles metadata.
413
+ *
414
+ * @param {unknown} value
415
+ * @returns {Set<string>}
416
+ */
417
+ function readCycleStepIdSet(value) {
418
+ return new Set(Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : []);
419
+ }
420
+ /**
421
+ * Resolves the milestone ids that represent measured cycle boundaries.
422
+ *
423
+ * @param {Record<string, unknown>} scenario
424
+ * @returns {Set<string>}
425
+ */
426
+ function resolveMeasuredCycleMilestoneEvents(scenario) {
427
+ const milestones = new Set();
428
+ for (const budget of Array.isArray(scenario.budgets) ? scenario.budgets : []) {
429
+ if (!budget || typeof budget !== 'object' || budget.source !== 'milestone') {
430
+ continue;
431
+ }
432
+ if (typeof budget.fromMilestone === 'string') {
433
+ milestones.add(resolveMilestoneEventName(scenario, budget.fromMilestone));
434
+ }
435
+ if (typeof budget.toMilestone === 'string') {
436
+ milestones.add(resolveMilestoneEventName(scenario, budget.toMilestone));
437
+ }
438
+ }
439
+ return milestones;
440
+ }
441
+ /**
442
+ * Resolves how many leading commands are setup-only before repeated cycle work.
443
+ *
444
+ * @param {Record<string, unknown>} scenario
445
+ * @param {AndroidAdbProfileCommand[]} commands
446
+ * @returns {number}
447
+ */
448
+ function resolveSetupCommandCount(scenario, commands) {
449
+ const explicitSetupStepIds = readCycleStepIdSet(scenario.cycles?.setupStepIds);
450
+ if (explicitSetupStepIds.size > 0) {
451
+ let count = 0;
452
+ for (const command of commands) {
453
+ if (!command.commandId || !explicitSetupStepIds.has(command.commandId)) {
454
+ break;
455
+ }
456
+ count += 1;
457
+ }
458
+ return count;
459
+ }
460
+ const explicitBodyStepIds = readCycleStepIdSet(scenario.cycles?.bodyStepIds);
461
+ if (explicitBodyStepIds.size > 0) {
462
+ const firstBodyIndex = commands.findIndex((command) => (typeof command.commandId === 'string' && explicitBodyStepIds.has(command.commandId)));
463
+ return firstBodyIndex > 0 ? firstBodyIndex : 0;
464
+ }
465
+ let readinessSetupCommandCount = 0;
466
+ for (const command of commands) {
467
+ if (!isReadinessSetupProfileCommand(scenario, command)) {
468
+ break;
469
+ }
470
+ readinessSetupCommandCount += 1;
471
+ }
472
+ if (readinessSetupCommandCount > 0) {
473
+ return readinessSetupCommandCount;
474
+ }
475
+ const measuredMilestones = resolveMeasuredCycleMilestoneEvents(scenario);
476
+ if (measuredMilestones.size === 0) {
477
+ return 0;
478
+ }
479
+ const firstMeasuredCommandIndex = commands.findIndex((command) => (typeof command.waitForMilestone === 'string' && measuredMilestones.has(command.waitForMilestone)));
480
+ return firstMeasuredCommandIndex > 0 ? firstMeasuredCommandIndex : 0;
481
+ }
482
+ /**
483
+ * Expands commands so setup/readiness commands execute once while cycle-body commands repeat.
484
+ *
485
+ * @param {Record<string, unknown>} scenario
486
+ * @param {AndroidAdbProfileCommand[]} commands
487
+ * @param {number} repeat
488
+ * @returns {AndroidAdbProfileCommand[]}
489
+ */
490
+ function expandProfileCommandCycles(scenario, commands, repeat) {
491
+ const setupCommandCount = resolveSetupCommandCount(scenario, commands);
492
+ const setupCommands = commands.slice(0, setupCommandCount);
493
+ const cycleCommands = commands.slice(setupCommandCount);
494
+ const expandedCommands = cycleCommands.length === 0
495
+ ? setupCommands
496
+ : [
497
+ ...setupCommands,
498
+ ...Array.from({ length: repeat }).flatMap(() => cycleCommands),
499
+ ];
500
+ return expandedCommands.map((command, index) => ({
351
501
  ...command,
352
- sequence: (iteration * commands.length) + commandIndex + 1,
353
- })));
502
+ sequence: index + 1,
503
+ }));
504
+ }
505
+ /**
506
+ * Resolves a portable milestone id to the app truth event that releases command sequencing.
507
+ *
508
+ * @param {Record<string, unknown>} scenario
509
+ * @param {string} milestone
510
+ * @returns {string}
511
+ */
512
+ function resolveMilestoneEventName(scenario, milestone) {
513
+ const milestoneEntry = Array.isArray(scenario.milestones)
514
+ ? scenario.milestones.find((entry) => entry?.id === milestone)
515
+ : undefined;
516
+ if (typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0) {
517
+ return milestoneEntry.event;
518
+ }
519
+ const metricEvent = scenario.metricEvents?.[milestone];
520
+ return typeof metricEvent === 'string' && metricEvent.length > 0 ? metricEvent : milestone;
521
+ }
522
+ /**
523
+ * Resolves the scenario truth event that represents initial app readiness.
524
+ *
525
+ * @param {Record<string, unknown>} scenario
526
+ * @returns {string | null}
527
+ */
528
+ function resolveScenarioReadinessEvent(scenario) {
529
+ const explicitReadyEvent = scenario.truthEvents?.ready?.event;
530
+ if (typeof explicitReadyEvent === 'string' && explicitReadyEvent.length > 0) {
531
+ return explicitReadyEvent;
532
+ }
533
+ const milestoneEntry = Array.isArray(scenario.milestones)
534
+ ? scenario.milestones.find((entry) => (String(entry?.event ?? '').includes('ready')))
535
+ : undefined;
536
+ return typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0
537
+ ? milestoneEntry.event
538
+ : null;
539
+ }
540
+ /**
541
+ * Applies wait gates from the normalized execution plan to platform-declared commands.
542
+ *
543
+ * @param {Record<string, unknown>} scenario
544
+ * @param {AndroidAdbProfileCommand[]} commands
545
+ * @returns {AndroidAdbProfileCommand[]}
546
+ */
547
+ function applyExecutionPlanCommandGates(scenario, commands) {
548
+ const planCommands = resolveExecutionPlanProfileCommands(scenario);
549
+ if (planCommands.length === 0) {
550
+ return commands;
551
+ }
552
+ return commands.map((command, index) => {
553
+ const planCommand = planCommands[index];
554
+ if (!planCommand || typeof planCommand.waitForMilestone !== 'string' || typeof command.waitForMilestone === 'string') {
555
+ return command;
556
+ }
557
+ return {
558
+ ...command,
559
+ waitForMilestone: planCommand.waitForMilestone,
560
+ ...(typeof command.waitTimeoutMs === 'number'
561
+ ? {}
562
+ : { waitTimeoutMs: readPositiveInteger(planCommand.waitTimeoutMs, 0) }),
563
+ };
564
+ });
354
565
  }
355
566
  /**
356
567
  * Expands normalized scenario evidence steps into Android adb driver actions.
@@ -478,7 +689,7 @@ function resolveAndroidAdbProfileCommands(scenario) {
478
689
  });
479
690
  }
480
691
  }
481
- return commands;
692
+ return applyExecutionPlanCommandGates(scenario, commands);
482
693
  }
483
694
  /**
484
695
  * Summarizes failed adb capture checks for CLI errors.
@@ -553,8 +764,32 @@ async function runProfileAndroid(args, options = {}) {
553
764
  const config = readJson(path.resolve(args.config));
554
765
  const scenario = readJson(path.resolve(args.scenario));
555
766
  const runId = typeof args['run-id'] === 'string' ? args['run-id'] : createRunId();
767
+ const scenarioName = resolveProfileScenarioName({ scenario, scenarioPath: path.resolve(args.scenario) });
768
+ const artifactRoot = resolveArtifactRoot({
769
+ args,
770
+ config,
771
+ configPath: path.resolve(args.config),
772
+ platform: 'android',
773
+ });
556
774
  const adbCaptureEnabled = isEnabled(args['adb-capture']);
557
775
  const agentDeviceCaptureEnabled = isEnabled(args['agent-device-capture']);
776
+ const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
777
+ if (adbCaptureEnabled) {
778
+ const driverStepErrors = validateAndroidAdbDriverSteps(driverSteps);
779
+ if (driverStepErrors.length > 0) {
780
+ throw new Error(`Invalid Android adb driver step metadata: ${driverStepErrors.join(' ')}`);
781
+ }
782
+ }
783
+ await runProfileCompatibilityPreflight({
784
+ args,
785
+ artifactRoot,
786
+ platform: 'android',
787
+ primaryRunner: ANDROID_PROFILE_RUNNER_CAPABILITIES,
788
+ runDir: path.join(artifactRoot, scenarioName, runId),
789
+ runId,
790
+ scenario,
791
+ scenarioName,
792
+ });
558
793
  const profileSessionEnabled = isEnabled(args['profile-session']);
559
794
  const profileSessionStorageEnabled = isEnabled(args['android-profile-session-storage']);
560
795
  const profileSessionStorageKey = readStringArgOrEnv(args['android-profile-session-storage-key'], [
@@ -573,10 +808,14 @@ async function runProfileAndroid(args, options = {}) {
573
808
  'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
574
809
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
575
810
  ]), 1000);
576
- const androidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
811
+ const configuredAndroidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
577
812
  'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
578
813
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
579
814
  ]);
815
+ const androidDevClientReadyPattern = configuredAndroidDevClientReadyPattern
816
+ ?? (androidDevClientUrl && profileSessionEnabled && profileSessionStorageEnabled
817
+ ? DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN
818
+ : undefined);
580
819
  const androidDevClientReadyQuietMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
581
820
  'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
582
821
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
@@ -585,14 +824,10 @@ async function runProfileAndroid(args, options = {}) {
585
824
  'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
586
825
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
587
826
  ]), 60000);
588
- const scenarioName = typeof scenario.name === 'string' ? scenario.name : path.basename(args.scenario, '.json');
589
- const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
590
- if (adbCaptureEnabled) {
591
- const driverStepErrors = validateAndroidAdbDriverSteps(driverSteps);
592
- if (driverStepErrors.length > 0) {
593
- throw new Error(`Invalid Android adb driver step metadata: ${driverStepErrors.join(' ')}`);
594
- }
595
- }
827
+ const adbCommandTimeoutMs = parsePositiveInteger(readStringArgOrEnv(args['adb-command-timeout-ms'], [
828
+ 'ASL_ANDROID_ADB_COMMAND_TIMEOUT_MS',
829
+ 'ASL_EXAMPLE_ANDROID_ADB_COMMAND_TIMEOUT_MS',
830
+ ]), 30000);
596
831
  const profileSessionCommands = profileSessionEnabled ? resolveAndroidAdbProfileCommands(scenario) : [];
597
832
  const commandWaitMs = parsePositiveInteger(readScalarArg(args['command-wait-ms']), 250);
598
833
  const profileSessionDeepLinks = profileSessionEnabled && !profileSessionStorageEnabled
@@ -619,6 +854,7 @@ async function runProfileAndroid(args, options = {}) {
619
854
  ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
620
855
  ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
621
856
  ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
857
+ ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
622
858
  ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
623
859
  }),
624
860
  waitMs: profileCommand.waitMs,
@@ -652,13 +888,18 @@ async function runProfileAndroid(args, options = {}) {
652
888
  ...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
653
889
  captureLogcat: true,
654
890
  clearLogcat: isEnabled(args['clear-logcat']),
891
+ commandTimeoutMs: adbCommandTimeoutMs,
655
892
  deepLinks: profileSessionDeepLinks,
656
893
  ...(options.delay ? { delay: options.delay } : {}),
657
894
  ...(options.executor ? { executor: options.executor } : {}),
658
895
  driverSteps,
659
896
  launch: isEnabled(args.launch),
660
897
  launchWaitMs: parsePositiveInteger(readScalarArg(args['launch-wait-ms']), 0),
661
- logcatLines: parsePositiveInteger(readScalarArg(args['logcat-lines']), 1000),
898
+ logcatLines: resolveProfileSessionLogcatLines({
899
+ args,
900
+ commands: profileSessionCommands,
901
+ profileSessionEnabled,
902
+ }),
662
903
  outputDir: resolveAdbCaptureOutputDir({ args, runId }),
663
904
  packageName: resolveAndroidPackageName({ args, config }),
664
905
  ...(typeof args['react-native-debug-host'] === 'string'
@@ -724,7 +965,7 @@ async function runProfileAndroid(args, options = {}) {
724
965
  : baseProfileArgs;
725
966
  const lifecyclePhase = resolveManifestLifecyclePhase(args);
726
967
  const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
727
- const lifecycleArtifact = adbCapture ? 'raw/adb-logcat.txt' : 'raw/interaction.log';
968
+ const copiedAdbLogArtifact = adbCapture ? 'raw/adb-logcat.txt' : undefined;
728
969
  return runProfileMobile(profileArgs, {
729
970
  commandTransport: profileSessionStorageEnabled
730
971
  ? 'profile-session-storage'
@@ -740,13 +981,13 @@ async function runProfileAndroid(args, options = {}) {
740
981
  value: 'foreground',
741
982
  evidence: 'asserted',
742
983
  source: environmentSource,
743
- artifact: lifecycleArtifact,
984
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
744
985
  },
745
986
  lifecyclePhase: {
746
987
  value: 'foreground',
747
988
  evidence: 'asserted',
748
989
  source: environmentSource,
749
- artifact: lifecycleArtifact,
990
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
750
991
  },
751
992
  },
752
993
  environmentPreconditions: {
@@ -759,7 +1000,7 @@ async function runProfileAndroid(args, options = {}) {
759
1000
  value: lifecyclePhase,
760
1001
  evidence: 'asserted',
761
1002
  source: environmentSource,
762
- artifact: lifecycleArtifact,
1003
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
763
1004
  },
764
1005
  },
765
1006
  interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
@@ -49,7 +49,7 @@ declare function resolveIosConflictingBundleIds(config: Record<string, any>): st
49
49
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
50
50
  * @returns {string}
51
51
  */
52
- declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }: {
52
+ declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }: {
53
53
  action: 'start' | 'command';
54
54
  command?: string;
55
55
  commandId?: string;
@@ -59,10 +59,11 @@ declare function buildProfileSessionUrl({ action, command, commandId, config, qu
59
59
  scenario: string;
60
60
  sequence?: number;
61
61
  waitForMilestone?: string;
62
+ waitMs?: number;
62
63
  waitTimeoutMs?: number;
63
64
  }): string;
64
65
  /**
65
- * Derives a storage-backed profile capture window from scenario waits and cycles.
66
+ * Derives a storage-backed profile capture window from scenario waits, command gates, and cycles.
66
67
  *
67
68
  * @param {Record<string, unknown>} scenario
68
69
  * @returns {number}