agent-scenario-loop 0.1.3 → 0.1.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 (47) 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/schema-validator.d.ts +1 -0
  5. package/dist/core/schema-validator.js +1 -0
  6. package/dist/runner/android-adb-driver.d.ts +7 -2
  7. package/dist/runner/android-adb-driver.js +7 -1
  8. package/dist/runner/android-adb.d.ts +40 -5
  9. package/dist/runner/android-adb.js +1046 -664
  10. package/dist/runner/ios-simctl.d.ts +1 -0
  11. package/dist/runner/ios-simctl.js +1 -0
  12. package/dist/runner/profile-android.d.ts +11 -1
  13. package/dist/runner/profile-android.js +230 -16
  14. package/dist/runner/profile-ios.d.ts +3 -2
  15. package/dist/runner/profile-ios.js +223 -20
  16. package/dist/runner/profile-mobile.d.ts +31 -3
  17. package/dist/runner/profile-mobile.js +793 -20
  18. package/dist/runner/validate-project.js +3 -0
  19. package/dist/scripts/consumer-rehearsal.d.ts +119 -0
  20. package/dist/scripts/consumer-rehearsal.js +757 -0
  21. package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
  22. package/dist/scripts/downstream-local-package-gate.js +264 -0
  23. package/dist/scripts/package-smoke.d.ts +96 -0
  24. package/dist/scripts/package-smoke.js +2282 -0
  25. package/dist/scripts/release-readiness.d.ts +2 -0
  26. package/dist/scripts/release-readiness.js +520 -0
  27. package/docs/adapters.md +3 -1
  28. package/docs/api.md +2 -2
  29. package/docs/authoring.md +34 -2
  30. package/docs/consumer-rehearsal.md +27 -1
  31. package/docs/contracts.md +16 -2
  32. package/docs/live-proofs.md +5 -3
  33. package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
  34. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
  35. package/examples/runners/README.md +3 -3
  36. package/examples/runners/axe-accessibility-provider.json +2 -2
  37. package/examples/runners/script-accessibility-provider.json +2 -2
  38. package/examples/runners/script-memory-provider.json +2 -2
  39. package/examples/runners/script-network-provider.json +2 -2
  40. package/examples/runners/script-profiler-provider.json +2 -2
  41. package/package.json +11 -3
  42. package/schemas/manifest.schema.json +73 -3
  43. package/schemas/profiler.schema.json +243 -0
  44. package/schemas/runner-capabilities.schema.json +8 -2
  45. package/schemas/scenario.schema.json +18 -2
  46. package/templates/evidence-provider.json +3 -3
  47. package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
@@ -169,6 +169,7 @@ function parseKeyValueProfileSessionEntry(payload) {
169
169
  const timestamp = coerceNumber(entry.timestamp);
170
170
  const atMs = coerceNumber(entry.atMs);
171
171
  const sequence = coerceNumber(entry.sequence);
172
+ const waitMs = coerceNumber(entry.waitMs);
172
173
  const waitTimeoutMs = coerceNumber(entry.waitTimeoutMs);
173
174
  if (atMs !== null) {
174
175
  entry.atMs = atMs;
@@ -179,6 +180,9 @@ function parseKeyValueProfileSessionEntry(payload) {
179
180
  if (sequence !== null) {
180
181
  entry.sequence = sequence;
181
182
  }
183
+ if (waitMs !== null) {
184
+ entry.waitMs = waitMs;
185
+ }
182
186
  if (waitTimeoutMs !== null) {
183
187
  entry.waitTimeoutMs = waitTimeoutMs;
184
188
  }
@@ -364,10 +368,10 @@ function extractProfileSessionEntries(logText, filters = {}) {
364
368
  /**
365
369
  * Builds timing metrics from app-emitted profile events.
366
370
  *
367
- * @param {{scenario: string, runId: string, events: Record<string, unknown>[], expectedIterations: number, timeoutCount?: number, artifacts?: Record<string, unknown>, cycleEventNames?: Record<string, string> | null, budgets?: Record<string, unknown> | null}} options
371
+ * @param {{scenario: string, runId: string, events: Record<string, unknown>[], expectedIterations: number, timeoutCount?: number, artifacts?: Record<string, unknown>, cycleEventNames?: Record<string, string> | null, milestoneEventsPerIteration?: number, budgets?: Record<string, unknown> | null}} options
368
372
  * @returns {Record<string, unknown>}
369
373
  */
370
- function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterations, timeoutCount = 0, artifacts = {}, cycleEventNames = null, budgets = null, }) {
374
+ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterations, timeoutCount = 0, artifacts = {}, cycleEventNames = null, milestoneEventsPerIteration = 1, budgets = null, }) {
371
375
  const resolvedCycleEventNames = {
372
376
  openRequested: cycleEventNames?.openRequested ?? 'surface_open_requested',
373
377
  opened: cycleEventNames?.opened ?? 'surface_opened',
@@ -376,17 +380,35 @@ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterat
376
380
  milestone: cycleEventNames?.milestone,
377
381
  };
378
382
  const usesMilestoneOnlyCycle = typeof resolvedCycleEventNames.milestone === 'string';
383
+ const requiredMilestoneEventsPerIteration = usesMilestoneOnlyCycle &&
384
+ Number.isInteger(milestoneEventsPerIteration) &&
385
+ milestoneEventsPerIteration > 1
386
+ ? milestoneEventsPerIteration
387
+ : 1;
379
388
  const iterations = new Map();
389
+ let nextImplicitMilestoneIteration = 1;
390
+ let nextImplicitMilestoneCount = 0;
380
391
  for (const event of [...events].sort((left, right) => {
381
392
  const leftAt = typeof left.atMs === 'number' ? left.atMs : Number.POSITIVE_INFINITY;
382
393
  const rightAt = typeof right.atMs === 'number' ? right.atMs : Number.POSITIVE_INFINITY;
383
394
  return leftAt - rightAt;
384
395
  })) {
385
- const eventIteration = typeof event.iteration === 'number'
396
+ let eventIteration = typeof event.iteration === 'number'
386
397
  ? event.iteration
387
398
  : expectedIterations === 1
388
399
  ? 1
389
400
  : null;
401
+ if (eventIteration === null &&
402
+ usesMilestoneOnlyCycle &&
403
+ event.event === resolvedCycleEventNames.milestone &&
404
+ nextImplicitMilestoneIteration <= expectedIterations) {
405
+ eventIteration = nextImplicitMilestoneIteration;
406
+ nextImplicitMilestoneCount += 1;
407
+ if (nextImplicitMilestoneCount >= requiredMilestoneEventsPerIteration) {
408
+ nextImplicitMilestoneIteration += 1;
409
+ nextImplicitMilestoneCount = 0;
410
+ }
411
+ }
390
412
  if (eventIteration === null) {
391
413
  continue;
392
414
  }
@@ -416,8 +438,10 @@ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterat
416
438
  event.atMs >= current.closeRequestedAt) {
417
439
  current.dismissedAt = event.atMs;
418
440
  }
419
- if (event.event === resolvedCycleEventNames.milestone &&
420
- typeof current.milestoneAt !== 'number') {
441
+ if (event.event === resolvedCycleEventNames.milestone) {
442
+ current.milestoneCount = typeof current.milestoneCount === 'number'
443
+ ? current.milestoneCount + 1
444
+ : 1;
421
445
  current.milestoneAt = event.atMs;
422
446
  }
423
447
  iterations.set(eventIteration, current);
@@ -445,6 +469,9 @@ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterat
445
469
  record.dismissedAt >= record.closeRequestedAt;
446
470
  const hasMilestoneDuration = usesMilestoneOnlyCycle &&
447
471
  typeof record.milestoneAt === 'number' &&
472
+ (requiredMilestoneEventsPerIteration <= 1 ||
473
+ (typeof record.milestoneCount === 'number' &&
474
+ record.milestoneCount >= requiredMilestoneEventsPerIteration)) &&
448
475
  record.milestoneAt >= 0;
449
476
  if (hasMilestoneDuration) {
450
477
  durationsMs.push(roundMs(record.milestoneAt));
@@ -482,7 +509,8 @@ function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterat
482
509
  incompleteIterations,
483
510
  artifacts: sortValue(artifacts),
484
511
  };
485
- const budgetEvaluation = evaluateProfileBudgets({ metrics, budgets });
512
+ const intervalBudgetChecks = evaluateIntervalBudgetChecks({ events, expectedIterations, budgets });
513
+ const budgetEvaluation = evaluateProfileBudgets({ metrics, budgets, extraChecks: intervalBudgetChecks });
486
514
  if (budgetEvaluation) {
487
515
  metrics.budgetEvaluation = sortValue(budgetEvaluation);
488
516
  }
@@ -507,15 +535,111 @@ function evaluateBudgetCheck({ name, actual, limit }) {
507
535
  unit: 'ms',
508
536
  };
509
537
  }
538
+ /**
539
+ * Collects duration samples for one named interval budget.
540
+ *
541
+ * @param {{events: ProfileEvent[], fromEvent: string, toEvent: string, expectedIterations: number}} options
542
+ * @returns {number[]}
543
+ */
544
+ function collectIntervalDurations({ events, fromEvent, toEvent, expectedIterations, }) {
545
+ const sortedEvents = [...events].sort((left, right) => {
546
+ const leftAt = typeof left.atMs === 'number' ? left.atMs : Number.POSITIVE_INFINITY;
547
+ const rightAt = typeof right.atMs === 'number' ? right.atMs : Number.POSITIVE_INFINITY;
548
+ return leftAt - rightAt;
549
+ });
550
+ const hasIterationPairs = sortedEvents.some((event) => (typeof event.iteration === 'number' && (event.event === fromEvent || event.event === toEvent)));
551
+ if (hasIterationPairs) {
552
+ const durations = [];
553
+ for (let iteration = 1; iteration <= expectedIterations; iteration += 1) {
554
+ const from = sortedEvents.find((event) => (event.event === fromEvent &&
555
+ event.iteration === iteration &&
556
+ typeof event.atMs === 'number'));
557
+ const to = sortedEvents.find((event) => (event.event === toEvent &&
558
+ event.iteration === iteration &&
559
+ typeof event.atMs === 'number' &&
560
+ typeof from?.atMs === 'number' &&
561
+ event.atMs >= from.atMs));
562
+ if (typeof from?.atMs === 'number' && typeof to?.atMs === 'number') {
563
+ durations.push(roundMs(to.atMs - from.atMs));
564
+ }
565
+ }
566
+ return durations;
567
+ }
568
+ const durations = [];
569
+ let pendingFrom = null;
570
+ for (const event of sortedEvents) {
571
+ if (typeof event.atMs !== 'number') {
572
+ continue;
573
+ }
574
+ if (event.event === fromEvent && pendingFrom === null) {
575
+ pendingFrom = event.atMs;
576
+ continue;
577
+ }
578
+ if (event.event === toEvent && pendingFrom !== null && event.atMs >= pendingFrom) {
579
+ durations.push(roundMs(event.atMs - pendingFrom));
580
+ pendingFrom = null;
581
+ if (durations.length >= expectedIterations) {
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ return durations;
587
+ }
588
+ /**
589
+ * Evaluates named interval budgets that should not define scenario health completeness.
590
+ *
591
+ * @param {{events: ProfileEvent[], expectedIterations: number, budgets?: Record<string, unknown> | null}} options
592
+ * @returns {BudgetCheck[]}
593
+ */
594
+ function evaluateIntervalBudgetChecks({ events, expectedIterations, budgets, }) {
595
+ if (!Array.isArray(budgets?.intervals)) {
596
+ return [];
597
+ }
598
+ return budgets.intervals
599
+ .map((budget) => {
600
+ if (typeof budget.name !== 'string' ||
601
+ typeof budget.metric !== 'string' ||
602
+ typeof budget.limit !== 'number' ||
603
+ typeof budget.fromEvent !== 'string' ||
604
+ typeof budget.toEvent !== 'string') {
605
+ return null;
606
+ }
607
+ const durations = collectIntervalDurations({
608
+ events,
609
+ fromEvent: budget.fromEvent,
610
+ toEvent: budget.toEvent,
611
+ expectedIterations,
612
+ });
613
+ const actual = budget.metric === 'p50'
614
+ ? percentile(durations, 50)
615
+ : budget.metric === 'p95'
616
+ ? percentile(durations, 95)
617
+ : null;
618
+ return evaluateBudgetCheck({
619
+ name: budget.name,
620
+ actual,
621
+ limit: budget.limit,
622
+ });
623
+ })
624
+ .filter((check) => Boolean(check));
625
+ }
510
626
  /**
511
627
  * Evaluates configured profile budgets against generated metrics.
512
628
  *
513
- * @param {{metrics: Record<string, unknown>, budgets?: Record<string, unknown> | null}} options
629
+ * @param {{metrics: Record<string, unknown>, budgets?: Record<string, unknown> | null, extraChecks?: BudgetCheck[]}} options
514
630
  * @returns {Record<string, unknown> | null}
515
631
  */
516
- function evaluateProfileBudgets({ metrics, budgets }) {
632
+ function evaluateProfileBudgets({ metrics, budgets, extraChecks = [], }) {
517
633
  if (!budgets?.pass || typeof budgets.pass !== 'object') {
518
- return null;
634
+ if (extraChecks.length === 0) {
635
+ return null;
636
+ }
637
+ return {
638
+ metric: budgets?.metric ?? metrics.measurement ?? 'profile budget',
639
+ pass: extraChecks.every((check) => check.pass),
640
+ checks: extraChecks,
641
+ failedChecks: extraChecks.filter((check) => !check.pass).map((check) => check.name),
642
+ };
519
643
  }
520
644
  const checks = [
521
645
  evaluateBudgetCheck({
@@ -589,7 +713,7 @@ function evaluateProfileBudgets({ metrics, budgets }) {
589
713
  }
590
714
  : null,
591
715
  ].filter((check) => Boolean(check));
592
- const allChecks = [...thresholdChecks, ...checks];
716
+ const allChecks = [...thresholdChecks, ...checks, ...extraChecks];
593
717
  if (allChecks.length === 0) {
594
718
  return null;
595
719
  }
@@ -846,6 +970,7 @@ function buildCommandAcknowledgementTimeline({ entries, startedAt, }) {
846
970
  ...(typeof entry.result === 'string' ? { result: entry.result } : {}),
847
971
  ...(typeof entry.reason === 'string' ? { reason: entry.reason } : {}),
848
972
  ...(typeof entry.waitForMilestone === 'string' ? { waitForMilestone: entry.waitForMilestone } : {}),
973
+ ...(typeof entry.waitMs === 'number' ? { waitMs: entry.waitMs } : {}),
849
974
  ...(typeof entry.waitTimeoutMs === 'number' ? { waitTimeoutMs: entry.waitTimeoutMs } : {}),
850
975
  };
851
976
  return sortValue({
@@ -1053,6 +1178,7 @@ function normalizeBudgetsForCausalRun(budgets) {
1053
1178
  */
1054
1179
  function buildCausalRun({ scenario, flowId, runId, platform = 'ios', buildFlavor = 'unknown', interactionDriver, trigger = null, budgets = null, timeline = [], artifacts, manifest, metrics, }) {
1055
1180
  const iterationSummary = buildIterationSummary(metrics);
1181
+ const videoPath = typeof artifacts.captures?.video === 'string' ? artifacts.captures.video : null;
1056
1182
  return sortValue({
1057
1183
  schemaVersion: '1.0.0',
1058
1184
  flowId,
@@ -1084,7 +1210,7 @@ function buildCausalRun({ scenario, flowId, runId, platform = 'ios', buildFlavor
1084
1210
  summary: artifacts.summary,
1085
1211
  metrics: artifacts.metrics,
1086
1212
  manifest: artifacts.manifest,
1087
- video: artifacts.captures?.video,
1213
+ ...(videoPath ? { video: videoPath } : {}),
1088
1214
  screenshot: Array.isArray(artifacts.captures?.screenshots)
1089
1215
  ? artifacts.captures.screenshots[0] ?? null
1090
1216
  : null,
@@ -1217,9 +1343,20 @@ function buildSummaryMarkdown({ manifest, metrics }) {
1217
1343
  const evidenceAttachments = Array.isArray(manifest.artifacts.evidenceAttachments)
1218
1344
  ? manifest.artifacts.evidenceAttachments
1219
1345
  : [];
1346
+ const diagnostics = Array.isArray(manifest.artifacts.diagnostics)
1347
+ ? manifest.artifacts.diagnostics
1348
+ : [];
1220
1349
  const evidenceAttachmentLines = evidenceAttachments.length > 0
1221
1350
  ? evidenceAttachments.map((attachment) => `- ${attachment.channel}/${attachment.kind}: \`${attachment.path}\` (${attachment.sizeBytes} bytes, sha256 ${attachment.sha256})`)
1222
1351
  : ['- none'];
1352
+ const diagnosticLines = diagnostics.length > 0
1353
+ ? diagnostics.map((diagnostic) => {
1354
+ const label = diagnostic.name ? `${diagnostic.kind}/${diagnostic.name}` : diagnostic.kind;
1355
+ const pathLabel = typeof diagnostic.path === 'string' ? ` \`${diagnostic.path}\`` : '';
1356
+ const reasonLabel = typeof diagnostic.reason === 'string' ? ` - ${diagnostic.reason}` : '';
1357
+ return `- ${label}: ${diagnostic.status}${diagnostic.required ? ' (required)' : ''}${pathLabel}${reasonLabel}`;
1358
+ })
1359
+ : ['- none'];
1223
1360
  const attempt = manifest.attempt && typeof manifest.attempt === 'object' ? manifest.attempt : {};
1224
1361
  const attemptLines = [
1225
1362
  `- Attempt ID: \`${attempt.attemptId ?? manifest.runId}\``,
@@ -1263,10 +1400,18 @@ function buildSummaryMarkdown({ manifest, metrics }) {
1263
1400
  `- Manifest: \`${manifest.artifacts.manifest}\``,
1264
1401
  `- Scenario: \`${manifest.artifacts.scenario}\``,
1265
1402
  `- Metrics: \`${manifest.artifacts.metrics}\``,
1266
- `- Interaction log: \`${manifest.artifacts.raw.interactionLog}\``,
1267
- `- Device log: \`${manifest.artifacts.raw.deviceLog}\``,
1268
- `- Video: \`${manifest.artifacts.captures.video}\``,
1269
- `- UI tree: \`${manifest.artifacts.captures.uiTree}\``,
1403
+ `- Interaction log: ${typeof manifest.artifacts.raw.interactionLog === 'string'
1404
+ ? `\`${manifest.artifacts.raw.interactionLog}\``
1405
+ : 'none'}`,
1406
+ `- Device log: ${typeof manifest.artifacts.raw.deviceLog === 'string'
1407
+ ? `\`${manifest.artifacts.raw.deviceLog}\``
1408
+ : 'none'}`,
1409
+ `- Video: ${typeof manifest.artifacts.captures.video === 'string'
1410
+ ? `\`${manifest.artifacts.captures.video}\``
1411
+ : 'none'}`,
1412
+ `- UI tree: ${typeof manifest.artifacts.captures.uiTree === 'string'
1413
+ ? `\`${manifest.artifacts.captures.uiTree}\``
1414
+ : 'none'}`,
1270
1415
  `- Screenshots: ${screenshots.length > 0
1271
1416
  ? screenshots.map((item) => `\`${item}\``).join(', ')
1272
1417
  : 'none'}`,
@@ -1278,6 +1423,10 @@ function buildSummaryMarkdown({ manifest, metrics }) {
1278
1423
  '## Evidence attachments',
1279
1424
  '',
1280
1425
  ...evidenceAttachmentLines,
1426
+ '',
1427
+ '## Diagnostic inventory',
1428
+ '',
1429
+ ...diagnosticLines,
1281
1430
  ];
1282
1431
  if (metrics.budgetEvaluation) {
1283
1432
  lines.push('', '## Budget', '', `- Metric: ${metrics.budgetEvaluation.metric}`, `- Status: ${metrics.budgetEvaluation.pass ? 'pass' : 'fail'}`);
@@ -52,6 +52,7 @@ declare const SCHEMAS: {
52
52
  liveProofSet: JsonSchema;
53
53
  manifest: JsonSchema;
54
54
  metrics: JsonSchema;
55
+ profiler: JsonSchema;
55
56
  projectValidation: JsonSchema;
56
57
  scenario: JsonSchema;
57
58
  runnerCapabilities: JsonSchema;
@@ -55,6 +55,7 @@ const SCHEMAS = {
55
55
  liveProofSet: loadSchema('live-proof-set.schema.json'),
56
56
  manifest: loadSchema('manifest.schema.json'),
57
57
  metrics: loadSchema('metrics.schema.json'),
58
+ profiler: loadSchema('profiler.schema.json'),
58
59
  projectValidation: loadSchema('project-validation.schema.json'),
59
60
  scenario: loadSchema('scenario.schema.json'),
60
61
  runnerCapabilities: loadSchema('runner-capabilities.schema.json'),
@@ -7,6 +7,7 @@ type AndroidAdbCommandResult = {
7
7
  rawFileName: string;
8
8
  stderr: string;
9
9
  stdout: string;
10
+ stdoutBuffer?: Uint8Array;
10
11
  };
11
12
  type AndroidAdbDriver = {
12
13
  assertVisible: (options: AndroidAdbAssertVisibleOptions) => Promise<AndroidAdbCommandResult>;
@@ -31,12 +32,15 @@ type AndroidAdbDriverOptions = {
31
32
  deviceSerial: string;
32
33
  executor: AndroidAdbCommandExecutor;
33
34
  };
34
- type AndroidAdbCommandExecutor = (command: string, args: string[]) => Promise<{
35
+ type AndroidAdbCommandExecutor = (command: string, args: string[], options?: {
36
+ encoding?: 'buffer' | 'utf8';
37
+ }) => Promise<{
35
38
  args: string[];
36
39
  command: string;
37
40
  exitCode: number;
38
41
  stderr: string;
39
42
  stdout: string;
43
+ stdoutBuffer?: Uint8Array;
40
44
  }>;
41
45
  type AndroidAdbDeepLinkOptions = {
42
46
  packageName?: string | null;
@@ -110,7 +114,8 @@ declare function quoteAndroidShellArg(value: string): string;
110
114
  declare function formatAndroidAdbRawOutput(result: {
111
115
  stdout: string;
112
116
  stderr: string;
113
- }): string;
117
+ stdoutBuffer?: Uint8Array;
118
+ }): string | Uint8Array;
114
119
  /**
115
120
  * Joins command output from a multi-command adb driver action.
116
121
  *
@@ -57,6 +57,7 @@ function buildDriverResult({ action, rawFileName, result, }) {
57
57
  rawFileName,
58
58
  stderr: result.stderr,
59
59
  stdout: result.stdout,
60
+ ...(result.stdoutBuffer ? { stdoutBuffer: result.stdoutBuffer } : {}),
60
61
  };
61
62
  }
62
63
  /**
@@ -66,6 +67,9 @@ function buildDriverResult({ action, rawFileName, result, }) {
66
67
  * @returns {string}
67
68
  */
68
69
  function formatAndroidAdbRawOutput(result) {
70
+ if (result.stdoutBuffer && !result.stderr) {
71
+ return result.stdoutBuffer;
72
+ }
69
73
  return [result.stdout, result.stderr].filter(Boolean).join('\n');
70
74
  }
71
75
  /**
@@ -373,7 +377,9 @@ function createAndroidAdbDriver({ adbPath, deviceSerial, executor, }) {
373
377
  };
374
378
  },
375
379
  async screenshot({ rawFileName = 'adb-screenshot.png', } = {}) {
376
- const result = await executor(adbPath, ['-s', deviceSerial, 'exec-out', 'screencap', '-p']);
380
+ const result = await executor(adbPath, ['-s', deviceSerial, 'exec-out', 'screencap', '-p'], {
381
+ encoding: 'buffer',
382
+ });
377
383
  return buildDriverResult({ action: 'screenshot', rawFileName, result });
378
384
  },
379
385
  async scroll({ durationMs = 300, endX, endY, rawFileName = 'adb-scroll.txt', startX, startY, }) {
@@ -3,6 +3,7 @@ type CliArgs = {
3
3
  adb?: string | boolean;
4
4
  'capture-logcat'?: string | boolean;
5
5
  'clear-logcat'?: string | boolean;
6
+ 'command-timeout-ms'?: string | boolean;
6
7
  launch?: string | boolean;
7
8
  'android-dev-client-url'?: string | boolean;
8
9
  'android-dev-client-wait-ms'?: string | boolean;
@@ -28,8 +29,12 @@ type CommandResult = {
28
29
  exitCode: number;
29
30
  stderr: string;
30
31
  stdout: string;
32
+ stdoutBuffer?: Uint8Array;
31
33
  };
32
- type CommandExecutor = (command: string, args: string[]) => Promise<CommandResult>;
34
+ type CommandExecutorOptions = {
35
+ encoding?: 'buffer' | 'utf8';
36
+ };
37
+ type CommandExecutor = (command: string, args: string[], options?: CommandExecutorOptions) => Promise<CommandResult>;
33
38
  type AndroidDevice = {
34
39
  serial: string;
35
40
  state: string;
@@ -40,7 +45,7 @@ type AndroidPreflightResult = {
40
45
  device: AndroidDevice | null;
41
46
  health: Record<string, unknown>;
42
47
  metadata: Record<string, unknown>;
43
- raw: Record<string, string>;
48
+ raw: Record<string, string | Uint8Array>;
44
49
  runDir: string;
45
50
  verdict: Record<string, unknown>;
46
51
  };
@@ -81,7 +86,9 @@ type AndroidAdbDriverStep = {
81
86
  type AndroidPreflightOptions = {
82
87
  adbPath?: string;
83
88
  captureLogcat?: boolean;
89
+ captureWatchdogMs?: number;
84
90
  clearLogcat?: boolean;
91
+ commandTimeoutMs?: number;
85
92
  deepLinks?: AndroidDeepLinkCommand[];
86
93
  delay?: (ms: number) => Promise<void>;
87
94
  driverSteps?: AndroidAdbDriverStep[];
@@ -129,7 +136,16 @@ declare function parsePositiveInteger(value: string | boolean | undefined, fallb
129
136
  * @param {string[]} args
130
137
  * @returns {Promise<CommandResult>}
131
138
  */
132
- declare function execFileCommand(command: string, args: string[]): Promise<CommandResult>;
139
+ declare function execFileCommand(command: string, args: string[], options?: CommandExecutorOptions): Promise<CommandResult>;
140
+ /**
141
+ * Runs a command with a bounded timeout and captures stdout, stderr, and exit code without throwing.
142
+ *
143
+ * @param {string} command
144
+ * @param {string[]} args
145
+ * @param {number} timeoutMs
146
+ * @returns {Promise<CommandResult>}
147
+ */
148
+ declare function execFileCommandWithTimeout(command: string, args: string[], timeoutMs?: number, options?: CommandExecutorOptions): Promise<CommandResult>;
133
149
  /**
134
150
  * Parses `adb devices -l` output into device rows.
135
151
  *
@@ -189,6 +205,25 @@ declare function buildAndroidVerdict({ runId, health }: {
189
205
  runId: string;
190
206
  health: Record<string, unknown>;
191
207
  }): Record<string, unknown>;
208
+ type AndroidAdbCaptureWatchdogBudget = {
209
+ ceilingMs: number;
210
+ commandBudgetMs: number;
211
+ commandUnits: number;
212
+ declaredWaitMs: number;
213
+ floorMs: number;
214
+ perCommandOverheadMs: number;
215
+ source: 'derived' | 'override';
216
+ timeoutMs: number;
217
+ };
218
+ /**
219
+ * Derives a whole-capture adb watchdog from declared runner waits and command bounds.
220
+ *
221
+ * @param {AndroidPreflightOptions & {commandTimeoutMs: number}} options
222
+ * @returns {AndroidAdbCaptureWatchdogBudget}
223
+ */
224
+ declare function deriveAndroidAdbCaptureWatchdogBudget({ captureLogcat, captureWatchdogMs, clearLogcat, commandTimeoutMs, deepLinks, driverSteps, launch, launchWaitMs, packageName, reactNativeDebugHost, startupDeepLinks, storageWrites, waitMs, }: AndroidPreflightOptions & {
225
+ commandTimeoutMs: number;
226
+ }): AndroidAdbCaptureWatchdogBudget;
192
227
  /**
193
228
  * Builds the driver steps for this adb capture window.
194
229
  *
@@ -243,12 +278,12 @@ declare function runAndroidAdbDriverStep({ capturesDir, driver, driverStep, logc
243
278
  * @param {AndroidPreflightOptions} options
244
279
  * @returns {Promise<AndroidPreflightResult>}
245
280
  */
246
- declare function runAndroidAdbPreflight({ adbPath, captureLogcat, clearLogcat, deepLinks, delay: wait, driverSteps, executor, launch, launchWaitMs, logcatLines, outputDir, packageName, reactNativeDebugHost, runId, serial, startupDeepLinks, storageWrites, waitMs, }?: AndroidPreflightOptions): Promise<AndroidPreflightResult>;
281
+ declare function runAndroidAdbPreflight({ adbPath, captureLogcat, captureWatchdogMs: captureWatchdogMsOverride, clearLogcat, commandTimeoutMs, deepLinks, delay: wait, driverSteps, executor, launch, launchWaitMs, logcatLines, outputDir, packageName, reactNativeDebugHost, runId, serial, startupDeepLinks, storageWrites, waitMs, }?: AndroidPreflightOptions): Promise<AndroidPreflightResult>;
247
282
  /**
248
283
  * Runs the android-adb preflight CLI.
249
284
  *
250
285
  * @returns {Promise<void>}
251
286
  */
252
287
  declare function main(): Promise<void>;
253
- export { ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, buildAndroidHealth, buildAndroidVerdict, buildReactNativeDebugHostPreferenceCommand, escapeAndroidPreferenceXml, execFileCommand, main, parseAdbDevices, parseArgs, parsePositiveInteger, parseReactNativeDebugHostPort, resolveAndroidAdbDriverSteps, applyAndroidSelectorResolution, buildAndroidSelectorHealthMetadata, needsAndroidSelectorResolution, runAndroidAdbDriverStep, runAndroidAdbPreflight, selectDevice, usage, };
288
+ export { ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, buildAndroidHealth, buildAndroidVerdict, buildReactNativeDebugHostPreferenceCommand, deriveAndroidAdbCaptureWatchdogBudget, escapeAndroidPreferenceXml, execFileCommand, execFileCommandWithTimeout, main, parseAdbDevices, parseArgs, parsePositiveInteger, parseReactNativeDebugHostPort, resolveAndroidAdbDriverSteps, applyAndroidSelectorResolution, buildAndroidSelectorHealthMetadata, needsAndroidSelectorResolution, runAndroidAdbDriverStep, runAndroidAdbPreflight, selectDevice, usage, };
254
289
  export type { AndroidDevice, AndroidAdbDriverStep, AndroidAsyncStorageWrite, AndroidDeepLinkCommand, AndroidPreflightOptions, AndroidPreflightResult, CliArgs, CommandExecutor, CommandResult, };