agent-scenario-loop 0.1.2 → 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.
- package/README.md +9 -9
- package/app/profile-session.ts +352 -12
- package/dist/core/agent-summary.d.ts +3 -2
- package/dist/core/agent-summary.js +44 -2
- package/dist/core/artifact-contract.d.ts +28 -8
- package/dist/core/artifact-contract.js +676 -26
- package/dist/core/comparison.d.ts +57 -3
- package/dist/core/comparison.js +113 -1
- package/dist/core/planner.d.ts +32 -1
- package/dist/core/planner.js +144 -0
- package/dist/core/run-index.d.ts +4 -0
- package/dist/core/run-index.js +55 -1
- package/dist/core/schema-validator.d.ts +2 -0
- package/dist/core/schema-validator.js +2 -0
- package/dist/runner/android-adb-driver.d.ts +7 -2
- package/dist/runner/android-adb-driver.js +7 -1
- package/dist/runner/android-adb.d.ts +40 -5
- package/dist/runner/android-adb.js +1046 -664
- package/dist/runner/compare-latest.d.ts +8 -4
- package/dist/runner/compare-latest.js +24 -5
- package/dist/runner/example-android-live.d.ts +10 -1
- package/dist/runner/example-android-live.js +55 -0
- package/dist/runner/example-ios-live.d.ts +10 -1
- package/dist/runner/example-ios-live.js +55 -0
- package/dist/runner/ios-simctl.d.ts +6 -0
- package/dist/runner/ios-simctl.js +7 -0
- package/dist/runner/live-comparison.d.ts +2 -2
- package/dist/runner/live-comparison.js +2 -1
- package/dist/runner/live-proof-summary.d.ts +5 -4
- package/dist/runner/live-proof-summary.js +12 -2
- package/dist/runner/live-proof.d.ts +3 -2
- package/dist/runner/live-proof.js +9 -2
- package/dist/runner/profile-android.d.ts +16 -1
- package/dist/runner/profile-android.js +364 -26
- package/dist/runner/profile-ios.d.ts +13 -2
- package/dist/runner/profile-ios.js +341 -19
- package/dist/runner/profile-mobile.d.ts +39 -3
- package/dist/runner/profile-mobile.js +1054 -42
- package/dist/runner/validate-project.js +3 -0
- package/dist/scripts/consumer-rehearsal.d.ts +119 -0
- package/dist/scripts/consumer-rehearsal.js +757 -0
- package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
- package/dist/scripts/downstream-local-package-gate.js +264 -0
- package/dist/scripts/package-smoke.d.ts +96 -0
- package/dist/scripts/package-smoke.js +2282 -0
- package/dist/scripts/release-readiness.d.ts +2 -0
- package/dist/scripts/release-readiness.js +520 -0
- package/docs/adapters.md +7 -1
- package/docs/api.md +2 -2
- package/docs/architecture.md +90 -0
- package/docs/authoring.md +39 -3
- package/docs/concepts.md +3 -24
- package/docs/consumer-rehearsal.md +31 -1
- package/docs/contracts.md +45 -101
- package/docs/external-adapter-protocol.md +219 -0
- package/docs/live-proofs.md +86 -3
- package/docs/principles.md +9 -15
- package/examples/mobile-app/README.md +12 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
- package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
- package/examples/runners/README.md +4 -3
- package/examples/runners/adb-android.json +1 -0
- package/examples/runners/agent-device-android.json +1 -0
- package/examples/runners/agent-device-ios.json +1 -0
- package/examples/runners/argent-android.json +1 -0
- package/examples/runners/argent-ios.json +1 -0
- package/examples/runners/axe-accessibility-provider.json +2 -2
- package/examples/runners/script-accessibility-provider.json +2 -2
- package/examples/runners/script-memory-provider.json +2 -2
- package/examples/runners/script-network-provider.json +2 -2
- package/examples/runners/script-profiler-provider.json +2 -2
- package/examples/runners/xcodebuildmcp-ios.json +1 -0
- package/package.json +12 -3
- package/schemas/causal-run.schema.json +85 -2
- package/schemas/comparison.schema.json +130 -2
- package/schemas/external-adapter-message.schema.json +693 -0
- package/schemas/health.schema.json +72 -0
- package/schemas/live-proof-set.schema.json +1 -1
- package/schemas/live-proof.schema.json +14 -6
- package/schemas/manifest.schema.json +515 -4
- package/schemas/profiler.schema.json +243 -0
- package/schemas/runner-capabilities.schema.json +28 -2
- package/schemas/scenario.schema.json +34 -2
- package/templates/evidence-provider.json +3 -3
- package/templates/primary-runner.json +1 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
|
@@ -5,8 +5,10 @@ exports.ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER = void 0;
|
|
|
5
5
|
exports.buildAndroidHealth = buildAndroidHealth;
|
|
6
6
|
exports.buildAndroidVerdict = buildAndroidVerdict;
|
|
7
7
|
exports.buildReactNativeDebugHostPreferenceCommand = buildReactNativeDebugHostPreferenceCommand;
|
|
8
|
+
exports.deriveAndroidAdbCaptureWatchdogBudget = deriveAndroidAdbCaptureWatchdogBudget;
|
|
8
9
|
exports.escapeAndroidPreferenceXml = escapeAndroidPreferenceXml;
|
|
9
10
|
exports.execFileCommand = execFileCommand;
|
|
11
|
+
exports.execFileCommandWithTimeout = execFileCommandWithTimeout;
|
|
10
12
|
exports.main = main;
|
|
11
13
|
exports.parseAdbDevices = parseAdbDevices;
|
|
12
14
|
exports.parseArgs = parseArgs;
|
|
@@ -32,6 +34,24 @@ const { hasHelpFlag, writeUsage } = require('./cli');
|
|
|
32
34
|
const { buildAndroidScrollCoordinatesFromBounds, createAndroidAdbDriver, formatAndroidAdbRawOutput, quoteAndroidShellArg, resolveAndroidSelectorFromUiTree, } = require('./android-adb-driver');
|
|
33
35
|
const ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER = '__ASL_ANDROID_DEVICE_EPOCH_MS__';
|
|
34
36
|
exports.ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER = ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER;
|
|
37
|
+
const DEFAULT_ADB_COMMAND_TIMEOUT_MS = 30000;
|
|
38
|
+
const ANDROID_ADB_CAPTURE_WATCHDOG_FLOOR_MS = 15000;
|
|
39
|
+
const ANDROID_ADB_CAPTURE_WATCHDOG_CEILING_MS = 120000;
|
|
40
|
+
const ANDROID_ADB_CAPTURE_COMMAND_OVERHEAD_MS = 3000;
|
|
41
|
+
const ANDROID_ADB_CAPTURE_COMMAND_OVERHEAD_CEILING_MS = 45000;
|
|
42
|
+
/**
|
|
43
|
+
* Replaces Android device-clock placeholders in JSON payload strings.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} value
|
|
46
|
+
* @param {number} epochMs
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function resolveAndroidDeviceEpochPlaceholders(value, epochMs) {
|
|
50
|
+
return value
|
|
51
|
+
.replace(new RegExp(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}\\+(\\d+)"`, 'gu'), (_match, offset) => String(epochMs + Number(offset)))
|
|
52
|
+
.replace(new RegExp(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}"`, 'gu'), String(epochMs))
|
|
53
|
+
.replaceAll(ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, String(epochMs));
|
|
54
|
+
}
|
|
35
55
|
const ANDROID_READY_LOG_POLL_MS = 1000;
|
|
36
56
|
/**
|
|
37
57
|
* Prints CLI usage to stderr.
|
|
@@ -43,6 +63,7 @@ function usage(output = process.stderr) {
|
|
|
43
63
|
'Usage: asl-android-adb [--adb <path>] [--serial <device>] [--package <name>] [--run-id <id>] [--out <dir>]',
|
|
44
64
|
'',
|
|
45
65
|
'Checks adb/device readiness and writes health.json, verdict.json, agent-summary.md, and raw adb evidence.',
|
|
66
|
+
'Use --command-timeout-ms <ms> to bound each adb invocation.',
|
|
46
67
|
'Use --capture-logcat [--logcat-lines <count>] to attach a bounded adb logcat snapshot under raw/adb-logcat.txt.',
|
|
47
68
|
'Use --clear-logcat --launch [--launch-wait-ms <ms>] --wait-ms <ms> with --package <name> to capture a bounded app launch window.',
|
|
48
69
|
'Use --react-native-debug-host <host:port> with --package <name> to set the app debug server and adb reverse for React Native dev builds.',
|
|
@@ -106,15 +127,32 @@ function parsePositiveInteger(value, fallback) {
|
|
|
106
127
|
* @param {string[]} args
|
|
107
128
|
* @returns {Promise<CommandResult>}
|
|
108
129
|
*/
|
|
109
|
-
function execFileCommand(command, args) {
|
|
130
|
+
function execFileCommand(command, args, options = {}) {
|
|
131
|
+
return execFileCommandWithTimeout(command, args, DEFAULT_ADB_COMMAND_TIMEOUT_MS, options);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Runs a command with a bounded timeout and captures stdout, stderr, and exit code without throwing.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} command
|
|
137
|
+
* @param {string[]} args
|
|
138
|
+
* @param {number} timeoutMs
|
|
139
|
+
* @returns {Promise<CommandResult>}
|
|
140
|
+
*/
|
|
141
|
+
function execFileCommandWithTimeout(command, args, timeoutMs = DEFAULT_ADB_COMMAND_TIMEOUT_MS, options = {}) {
|
|
110
142
|
return new Promise((resolve) => {
|
|
111
|
-
execFile(command, args, (error, stdout, stderr) => {
|
|
143
|
+
execFile(command, args, { encoding: options.encoding === 'buffer' ? 'buffer' : 'utf8', timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
144
|
+
const timedOut = Boolean(error?.killed || error?.signal === 'SIGTERM');
|
|
145
|
+
const stdoutText = Buffer.isBuffer(stdout) ? stdout.toString('utf8') : stdout;
|
|
146
|
+
const stderrText = Buffer.isBuffer(stderr) ? stderr.toString('utf8') : stderr;
|
|
112
147
|
resolve({
|
|
113
148
|
command,
|
|
114
149
|
args,
|
|
115
150
|
exitCode: error && typeof error.code === 'number' ? error.code : error ? 1 : 0,
|
|
116
|
-
stderr,
|
|
117
|
-
stdout,
|
|
151
|
+
stderr: [stderrText, timedOut ? `adb command timed out after ${timeoutMs}ms.` : ''].filter(Boolean).join('\n'),
|
|
152
|
+
stdout: stdoutText,
|
|
153
|
+
...(options.encoding === 'buffer'
|
|
154
|
+
? { stdoutBuffer: Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout, 'utf8') }
|
|
155
|
+
: {}),
|
|
118
156
|
});
|
|
119
157
|
});
|
|
120
158
|
});
|
|
@@ -192,7 +230,8 @@ function isAdbDaemonUnavailable(result) {
|
|
|
192
230
|
return (output.includes('cannot connect to daemon') ||
|
|
193
231
|
output.includes('failed to check server version') ||
|
|
194
232
|
output.includes('could not install *smartsocket* listener') ||
|
|
195
|
-
output.includes('adb server didn')
|
|
233
|
+
output.includes('adb server didn') ||
|
|
234
|
+
output.includes('adb command timed out'));
|
|
196
235
|
}
|
|
197
236
|
/**
|
|
198
237
|
* Builds the health check details for device selection failures.
|
|
@@ -482,6 +521,239 @@ function buildAndroidVerdict({ runId, health }) {
|
|
|
482
521
|
: 'Android adb preflight failed; runtime scenario execution is not ready.',
|
|
483
522
|
}, SCHEMAS.verdict, 'Verdict artifact');
|
|
484
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Converts an unexpected runner failure into artifact-safe metadata.
|
|
526
|
+
*
|
|
527
|
+
* @param {unknown} error
|
|
528
|
+
* @returns {Record<string, unknown>}
|
|
529
|
+
*/
|
|
530
|
+
function normalizeAndroidRunnerFailure(error) {
|
|
531
|
+
if (error instanceof Error) {
|
|
532
|
+
return {
|
|
533
|
+
name: error.name,
|
|
534
|
+
message: error.message,
|
|
535
|
+
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (error && typeof error === 'object') {
|
|
539
|
+
const record = error;
|
|
540
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => (value === null ||
|
|
541
|
+
['boolean', 'number', 'string'].includes(typeof value) ||
|
|
542
|
+
Array.isArray(value))));
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
message: String(error),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Formats unexpected runner failure metadata as a raw transcript.
|
|
550
|
+
*
|
|
551
|
+
* @param {Record<string, unknown>} failure
|
|
552
|
+
* @returns {string}
|
|
553
|
+
*/
|
|
554
|
+
function formatAndroidRunnerFailureRaw(failure) {
|
|
555
|
+
return Object.entries(failure)
|
|
556
|
+
.map(([key, value]) => {
|
|
557
|
+
const formatted = value && typeof value === 'object' && !Array.isArray(value)
|
|
558
|
+
? JSON.stringify(value)
|
|
559
|
+
: Array.isArray(value)
|
|
560
|
+
? value.join(' ')
|
|
561
|
+
: String(value);
|
|
562
|
+
return `${key}: ${formatted}`;
|
|
563
|
+
})
|
|
564
|
+
.join('\n');
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Sums only positive integer-ish durations.
|
|
568
|
+
*
|
|
569
|
+
* @param {Array<number | undefined>} values
|
|
570
|
+
* @returns {number}
|
|
571
|
+
*/
|
|
572
|
+
function sumPositiveDurations(values) {
|
|
573
|
+
let total = 0;
|
|
574
|
+
for (const value of values) {
|
|
575
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
576
|
+
total += value;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return total;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Derives a whole-capture adb watchdog from declared runner waits and command bounds.
|
|
583
|
+
*
|
|
584
|
+
* @param {AndroidPreflightOptions & {commandTimeoutMs: number}} options
|
|
585
|
+
* @returns {AndroidAdbCaptureWatchdogBudget}
|
|
586
|
+
*/
|
|
587
|
+
function deriveAndroidAdbCaptureWatchdogBudget({ captureLogcat = false, captureWatchdogMs, clearLogcat = false, commandTimeoutMs, deepLinks = [], driverSteps = [], launch = false, launchWaitMs = 0, packageName = null, reactNativeDebugHost = null, startupDeepLinks = [], storageWrites = [], waitMs = 0, }) {
|
|
588
|
+
if (typeof captureWatchdogMs === 'number' && Number.isFinite(captureWatchdogMs) && captureWatchdogMs > 0) {
|
|
589
|
+
return {
|
|
590
|
+
ceilingMs: ANDROID_ADB_CAPTURE_WATCHDOG_CEILING_MS,
|
|
591
|
+
commandBudgetMs: 0,
|
|
592
|
+
commandUnits: 0,
|
|
593
|
+
declaredWaitMs: captureWatchdogMs,
|
|
594
|
+
floorMs: ANDROID_ADB_CAPTURE_WATCHDOG_FLOOR_MS,
|
|
595
|
+
perCommandOverheadMs: 0,
|
|
596
|
+
source: 'override',
|
|
597
|
+
timeoutMs: Math.ceil(captureWatchdogMs),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const resolvedDriverSteps = resolveAndroidAdbDriverSteps({
|
|
601
|
+
captureLogcat,
|
|
602
|
+
driverSteps,
|
|
603
|
+
logcatLines: 1,
|
|
604
|
+
waitMs,
|
|
605
|
+
});
|
|
606
|
+
const declaredWaitMs = sumPositiveDurations([
|
|
607
|
+
launchWaitMs,
|
|
608
|
+
...startupDeepLinks.map((deepLink) => deepLink.waitMs),
|
|
609
|
+
...startupDeepLinks.map((deepLink) => deepLink.readyLogTimeoutMs),
|
|
610
|
+
...storageWrites.map((write) => write.waitMs),
|
|
611
|
+
...deepLinks.map((deepLink) => deepLink.waitMs),
|
|
612
|
+
...resolvedDriverSteps.map((step) => step.waitMs),
|
|
613
|
+
...resolvedDriverSteps.map((step) => step.durationMs),
|
|
614
|
+
...resolvedDriverSteps.map((step) => (typeof step.durationSeconds === 'number' ? step.durationSeconds * 1000 : undefined)),
|
|
615
|
+
]);
|
|
616
|
+
const startupReadyPolls = startupDeepLinks.filter((deepLink) => deepLink.readyLogPattern).length;
|
|
617
|
+
const storageClockReads = storageWrites.filter((write) => write.value.includes(ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER)).length;
|
|
618
|
+
const selectorInspections = resolvedDriverSteps.filter((step) => needsAndroidSelectorResolution(step)).length;
|
|
619
|
+
const lifecycleChecks = packageName && (launch || deepLinks.length > 0) ? 2 : 0;
|
|
620
|
+
const commandUnits = 6 +
|
|
621
|
+
(packageName ? 1 : 0) +
|
|
622
|
+
(reactNativeDebugHost ? 2 : 0) +
|
|
623
|
+
(clearLogcat ? 1 : 0) +
|
|
624
|
+
(launch ? 2 : 0) +
|
|
625
|
+
startupDeepLinks.length +
|
|
626
|
+
startupReadyPolls +
|
|
627
|
+
storageWrites.length +
|
|
628
|
+
storageClockReads +
|
|
629
|
+
deepLinks.length +
|
|
630
|
+
(packageName ? deepLinks.length : 0) +
|
|
631
|
+
resolvedDriverSteps.length +
|
|
632
|
+
selectorInspections +
|
|
633
|
+
lifecycleChecks;
|
|
634
|
+
const perCommandOverheadMs = Math.min(commandTimeoutMs, ANDROID_ADB_CAPTURE_COMMAND_OVERHEAD_MS);
|
|
635
|
+
const commandBudgetMs = Math.min(commandUnits * perCommandOverheadMs, ANDROID_ADB_CAPTURE_COMMAND_OVERHEAD_CEILING_MS);
|
|
636
|
+
const bufferedBudgetMs = commandBudgetMs + declaredWaitMs + 5000;
|
|
637
|
+
const ceilingMs = Math.max(ANDROID_ADB_CAPTURE_WATCHDOG_CEILING_MS, bufferedBudgetMs);
|
|
638
|
+
return {
|
|
639
|
+
ceilingMs,
|
|
640
|
+
commandBudgetMs,
|
|
641
|
+
commandUnits,
|
|
642
|
+
declaredWaitMs,
|
|
643
|
+
floorMs: ANDROID_ADB_CAPTURE_WATCHDOG_FLOOR_MS,
|
|
644
|
+
perCommandOverheadMs,
|
|
645
|
+
source: 'derived',
|
|
646
|
+
timeoutMs: Math.min(ceilingMs, Math.max(ANDROID_ADB_CAPTURE_WATCHDOG_FLOOR_MS, bufferedBudgetMs)),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Builds the immediate raw checkpoint written before the first adb call.
|
|
651
|
+
*
|
|
652
|
+
* @param {{adbPath: string, captureWatchdog: AndroidAdbCaptureWatchdogBudget, commandTimeoutMs: number, deadlineEpochMs: number, metadata: Record<string, unknown>, runId: string, startedAt: string}} options
|
|
653
|
+
* @returns {Record<string, unknown>}
|
|
654
|
+
*/
|
|
655
|
+
function buildAndroidAdbCaptureStartedCheckpoint({ adbPath, captureWatchdog, commandTimeoutMs, deadlineEpochMs, metadata, runId, startedAt, }) {
|
|
656
|
+
return {
|
|
657
|
+
status: 'started',
|
|
658
|
+
message: 'Android adb capture started and the whole-capture watchdog deadline was derived.',
|
|
659
|
+
runId,
|
|
660
|
+
adbPath,
|
|
661
|
+
commandTimeoutMs,
|
|
662
|
+
startedAt,
|
|
663
|
+
watchdog: {
|
|
664
|
+
...captureWatchdog,
|
|
665
|
+
armed: true,
|
|
666
|
+
deadlineEpochMs,
|
|
667
|
+
deadlineIso: new Date(deadlineEpochMs).toISOString(),
|
|
668
|
+
},
|
|
669
|
+
selectedInputs: {
|
|
670
|
+
captureLogcat: metadata.captureLogcat,
|
|
671
|
+
clearLogcat: metadata.clearLogcat,
|
|
672
|
+
deepLinks: metadata.deepLinks,
|
|
673
|
+
driverSteps: metadata.driverSteps,
|
|
674
|
+
launch: metadata.launch,
|
|
675
|
+
logcatLines: metadata.logcatLines,
|
|
676
|
+
packageName: metadata.packageName,
|
|
677
|
+
reactNativeDebugHost: metadata.reactNativeDebugHost,
|
|
678
|
+
startupDeepLinks: metadata.startupDeepLinks,
|
|
679
|
+
storageWrites: metadata.storageWrites,
|
|
680
|
+
waitMs: metadata.waitMs,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Creates the classified whole-capture watchdog error.
|
|
686
|
+
*
|
|
687
|
+
* @param {AndroidAdbCaptureWatchdogBudget} watchdog
|
|
688
|
+
* @returns {AndroidAdbCaptureWatchdogError}
|
|
689
|
+
*/
|
|
690
|
+
function createAndroidAdbCaptureWatchdogError(watchdog) {
|
|
691
|
+
const error = new Error(`Android adb capture did not complete within ${watchdog.timeoutMs}ms.`);
|
|
692
|
+
return Object.assign(error, {
|
|
693
|
+
code: 'android_adb_runner_liveness_timeout',
|
|
694
|
+
watchdog,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Checks whether an error came from the capture watchdog.
|
|
699
|
+
*
|
|
700
|
+
* @param {unknown} error
|
|
701
|
+
* @returns {error is AndroidAdbCaptureWatchdogError}
|
|
702
|
+
*/
|
|
703
|
+
function isAndroidAdbCaptureWatchdogError(error) {
|
|
704
|
+
return error instanceof Error &&
|
|
705
|
+
error.code === 'android_adb_runner_liveness_timeout';
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Runs the mutable capture body with a single whole-capture timeout.
|
|
709
|
+
*
|
|
710
|
+
* @param {{body: () => Promise<void>, watchdog: AndroidAdbCaptureWatchdogBudget}} options
|
|
711
|
+
* @returns {Promise<void>}
|
|
712
|
+
*/
|
|
713
|
+
async function runAndroidAdbCaptureBodyWithWatchdog({ body, watchdog, }) {
|
|
714
|
+
let timer = null;
|
|
715
|
+
try {
|
|
716
|
+
await Promise.race([
|
|
717
|
+
body(),
|
|
718
|
+
new Promise((_resolve, reject) => {
|
|
719
|
+
timer = setTimeout(() => reject(createAndroidAdbCaptureWatchdogError(watchdog)), watchdog.timeoutMs);
|
|
720
|
+
}),
|
|
721
|
+
]);
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
if (timer) {
|
|
725
|
+
clearTimeout(timer);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Writes the Android adb artifact set.
|
|
731
|
+
*
|
|
732
|
+
* @param {{agentSummary: string, health: Record<string, unknown>, layout: ReturnType<typeof createArtifactLayout>, metadata: Record<string, unknown>, raw: Record<string, string | Uint8Array>, rawDir: string, verdict: Record<string, unknown>}} options
|
|
733
|
+
* @returns {Promise<void>}
|
|
734
|
+
*/
|
|
735
|
+
async function writeAndroidAdbArtifacts({ agentSummary, health, layout, metadata, raw, rawDir, verdict, }) {
|
|
736
|
+
await Promise.all(Object.entries(raw).map(([fileName, content]) => (content instanceof Uint8Array
|
|
737
|
+
? fsp.writeFile(path.join(rawDir, fileName), content)
|
|
738
|
+
: fsp.writeFile(path.join(rawDir, fileName), `${content.trimEnd()}\n`, 'utf8'))));
|
|
739
|
+
await fsp.writeFile(path.join(rawDir, 'android-metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
740
|
+
await writeJsonArtifact({
|
|
741
|
+
filePath: layout.health,
|
|
742
|
+
value: health,
|
|
743
|
+
schema: SCHEMAS.health,
|
|
744
|
+
label: 'Health artifact',
|
|
745
|
+
});
|
|
746
|
+
await writeJsonArtifact({
|
|
747
|
+
filePath: layout.verdict,
|
|
748
|
+
value: verdict,
|
|
749
|
+
schema: SCHEMAS.verdict,
|
|
750
|
+
label: 'Verdict artifact',
|
|
751
|
+
});
|
|
752
|
+
await writeTextArtifact({
|
|
753
|
+
filePath: layout.agentSummary,
|
|
754
|
+
content: agentSummary,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
485
757
|
/**
|
|
486
758
|
* Builds the driver steps for this adb capture window.
|
|
487
759
|
*
|
|
@@ -771,68 +1043,39 @@ async function runAndroidAdbDriverStep({ capturesDir, driver, driverStep, logcat
|
|
|
771
1043
|
* @param {AndroidPreflightOptions} options
|
|
772
1044
|
* @returns {Promise<AndroidPreflightResult>}
|
|
773
1045
|
*/
|
|
774
|
-
async function runAndroidAdbPreflight({ adbPath = 'adb', captureLogcat = false, clearLogcat = false, deepLinks = [], delay: wait = delay, driverSteps = [], executor =
|
|
1046
|
+
async function runAndroidAdbPreflight({ adbPath = 'adb', captureLogcat = false, captureWatchdogMs: captureWatchdogMsOverride, clearLogcat = false, commandTimeoutMs = DEFAULT_ADB_COMMAND_TIMEOUT_MS, deepLinks = [], delay: wait = delay, driverSteps = [], executor = (command, args, options) => execFileCommandWithTimeout(command, args, commandTimeoutMs, options), launch = false, launchWaitMs = 0, logcatLines = 1000, outputDir = path.resolve('artifacts/android-adb-preflight'), packageName = null, reactNativeDebugHost = null, runId = createRunId(), serial = null, startupDeepLinks = [], storageWrites = [], waitMs = 0, } = {}) {
|
|
775
1047
|
const runDir = path.resolve(outputDir);
|
|
776
1048
|
const layout = createArtifactLayout({ outputDir: runDir });
|
|
777
1049
|
const rawDir = layout.raw;
|
|
778
1050
|
await fsp.mkdir(rawDir, { recursive: true });
|
|
779
1051
|
const raw = {};
|
|
780
1052
|
const checks = [];
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const devicesOutput = adbAvailable
|
|
797
|
-
? await executor(adbPath, ['devices', '-l'])
|
|
798
|
-
: {
|
|
799
|
-
command: adbPath,
|
|
800
|
-
args: ['devices', '-l'],
|
|
801
|
-
exitCode: 1,
|
|
802
|
-
stderr: 'adb unavailable',
|
|
803
|
-
stdout: '',
|
|
804
|
-
};
|
|
805
|
-
raw['adb-devices.txt'] = [devicesOutput.stdout, devicesOutput.stderr].filter(Boolean).join('\n');
|
|
806
|
-
const devices = parseAdbDevices(devicesOutput.stdout);
|
|
807
|
-
const device = selectDevice(devices, serial);
|
|
808
|
-
const deviceOnline = Boolean(device && device.state === 'device');
|
|
809
|
-
const deviceFailure = !deviceOnline
|
|
810
|
-
? buildAndroidDeviceFailure({ devicesOutput, serial })
|
|
811
|
-
: null;
|
|
812
|
-
checks.push({
|
|
813
|
-
name: 'android_device_connected',
|
|
814
|
-
status: deviceOnline ? 'passed' : 'failed',
|
|
815
|
-
source: 'runner',
|
|
816
|
-
code: deviceOnline ? 'android_device_connected' : deviceFailure?.code,
|
|
817
|
-
message: deviceOnline && device
|
|
818
|
-
? `Selected Android device ${device.serial}.`
|
|
819
|
-
: deviceFailure?.message,
|
|
820
|
-
...(!deviceOnline
|
|
821
|
-
? {
|
|
822
|
-
metadata: deviceFailure?.metadata,
|
|
823
|
-
}
|
|
824
|
-
: {}),
|
|
1053
|
+
let device = null;
|
|
1054
|
+
const captureWatchdog = deriveAndroidAdbCaptureWatchdogBudget({
|
|
1055
|
+
captureLogcat,
|
|
1056
|
+
...(typeof captureWatchdogMsOverride === 'number' ? { captureWatchdogMs: captureWatchdogMsOverride } : {}),
|
|
1057
|
+
clearLogcat,
|
|
1058
|
+
commandTimeoutMs,
|
|
1059
|
+
deepLinks,
|
|
1060
|
+
driverSteps,
|
|
1061
|
+
launch,
|
|
1062
|
+
launchWaitMs,
|
|
1063
|
+
packageName,
|
|
1064
|
+
reactNativeDebugHost,
|
|
1065
|
+
startupDeepLinks,
|
|
1066
|
+
storageWrites,
|
|
1067
|
+
waitMs,
|
|
825
1068
|
});
|
|
826
1069
|
const metadata = {
|
|
827
1070
|
adbPath,
|
|
828
1071
|
captureLogcat,
|
|
1072
|
+
captureWatchdog,
|
|
829
1073
|
clearLogcat,
|
|
1074
|
+
commandTimeoutMs,
|
|
830
1075
|
deepLinks,
|
|
831
|
-
devices,
|
|
832
1076
|
driverSteps,
|
|
833
1077
|
launch,
|
|
834
1078
|
logcatLines,
|
|
835
|
-
selectedDevice: device,
|
|
836
1079
|
packageName,
|
|
837
1080
|
reactNativeDebugHost,
|
|
838
1081
|
startupDeepLinks,
|
|
@@ -844,714 +1087,852 @@ async function runAndroidAdbPreflight({ adbPath = 'adb', captureLogcat = false,
|
|
|
844
1087
|
})),
|
|
845
1088
|
waitMs,
|
|
846
1089
|
};
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1090
|
+
const captureStartedAt = new Date();
|
|
1091
|
+
const captureDeadlineEpochMs = captureStartedAt.getTime() + captureWatchdog.timeoutMs;
|
|
1092
|
+
const captureStartedCheckpoint = buildAndroidAdbCaptureStartedCheckpoint({
|
|
1093
|
+
adbPath,
|
|
1094
|
+
captureWatchdog,
|
|
1095
|
+
commandTimeoutMs,
|
|
1096
|
+
deadlineEpochMs: captureDeadlineEpochMs,
|
|
1097
|
+
metadata,
|
|
1098
|
+
runId,
|
|
1099
|
+
startedAt: captureStartedAt.toISOString(),
|
|
852
1100
|
});
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1101
|
+
const captureStartedRawFileName = 'adb-capture-started.json';
|
|
1102
|
+
raw[captureStartedRawFileName] = JSON.stringify(captureStartedCheckpoint, null, 2);
|
|
1103
|
+
metadata.captureStarted = {
|
|
1104
|
+
rawPath: `raw/${captureStartedRawFileName}`,
|
|
1105
|
+
startedAt: captureStartedCheckpoint.startedAt,
|
|
1106
|
+
watchdog: captureStartedCheckpoint.watchdog,
|
|
1107
|
+
};
|
|
1108
|
+
await fsp.writeFile(path.join(rawDir, captureStartedRawFileName), `${JSON.stringify(captureStartedCheckpoint, null, 2)}\n`, 'utf8');
|
|
1109
|
+
const runCaptureBody = async () => {
|
|
1110
|
+
const version = await executor(adbPath, ['version']);
|
|
1111
|
+
const adbAvailable = version.exitCode === 0;
|
|
1112
|
+
raw['adb-version.txt'] = [version.stdout, version.stderr].filter(Boolean).join('\n');
|
|
1113
|
+
checks.push({
|
|
1114
|
+
name: 'adb_available',
|
|
1115
|
+
status: adbAvailable ? 'passed' : 'failed',
|
|
1116
|
+
source: 'runner',
|
|
1117
|
+
code: adbAvailable ? 'adb_available' : 'adb_unavailable',
|
|
1118
|
+
message: adbAvailable ? 'adb command is available.' : 'adb command could not be executed.',
|
|
1119
|
+
...(!adbAvailable
|
|
1120
|
+
? {
|
|
1121
|
+
metadata: nextActionHint('fix_adb_command', 'Install Android platform-tools or pass --adb with a working adb binary, then rerun the capture.'),
|
|
1122
|
+
}
|
|
1123
|
+
: {}),
|
|
859
1124
|
});
|
|
860
|
-
const
|
|
861
|
-
executor(adbPath, [
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1125
|
+
const devicesOutput = adbAvailable
|
|
1126
|
+
? await executor(adbPath, ['devices', '-l'])
|
|
1127
|
+
: {
|
|
1128
|
+
command: adbPath,
|
|
1129
|
+
args: ['devices', '-l'],
|
|
1130
|
+
exitCode: 1,
|
|
1131
|
+
stderr: 'adb unavailable',
|
|
1132
|
+
stdout: '',
|
|
1133
|
+
};
|
|
1134
|
+
raw['adb-devices.txt'] = [devicesOutput.stdout, devicesOutput.stderr].filter(Boolean).join('\n');
|
|
1135
|
+
const devices = parseAdbDevices(devicesOutput.stdout);
|
|
1136
|
+
metadata.devices = devices;
|
|
1137
|
+
device = selectDevice(devices, serial);
|
|
1138
|
+
metadata.selectedDevice = device;
|
|
1139
|
+
const deviceOnline = Boolean(device && device.state === 'device');
|
|
1140
|
+
const deviceFailure = !deviceOnline
|
|
1141
|
+
? buildAndroidDeviceFailure({ devicesOutput, serial })
|
|
1142
|
+
: null;
|
|
1143
|
+
checks.push({
|
|
1144
|
+
name: 'android_device_connected',
|
|
1145
|
+
status: deviceOnline ? 'passed' : 'failed',
|
|
1146
|
+
source: 'runner',
|
|
1147
|
+
code: deviceOnline ? 'android_device_connected' : deviceFailure?.code,
|
|
1148
|
+
message: deviceOnline && device
|
|
1149
|
+
? `Selected Android device ${device.serial}.`
|
|
1150
|
+
: deviceFailure?.message,
|
|
1151
|
+
...(!deviceOnline
|
|
1152
|
+
? {
|
|
1153
|
+
metadata: deviceFailure?.metadata,
|
|
1154
|
+
}
|
|
1155
|
+
: {}),
|
|
1156
|
+
});
|
|
1157
|
+
const resolvedDriverSteps = resolveAndroidAdbDriverSteps({
|
|
1158
|
+
captureLogcat,
|
|
1159
|
+
driverSteps,
|
|
1160
|
+
logcatLines,
|
|
1161
|
+
waitMs,
|
|
1162
|
+
});
|
|
1163
|
+
if (device && device.state === 'device') {
|
|
1164
|
+
const shellPrefix = ['-s', device.serial, 'shell'];
|
|
1165
|
+
const driver = createAndroidAdbDriver({
|
|
1166
|
+
adbPath,
|
|
1167
|
+
deviceSerial: device.serial,
|
|
1168
|
+
executor,
|
|
896
1169
|
});
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1170
|
+
const [model, release, sdk] = await Promise.all([
|
|
1171
|
+
executor(adbPath, [...shellPrefix, 'getprop', 'ro.product.model']),
|
|
1172
|
+
executor(adbPath, [...shellPrefix, 'getprop', 'ro.build.version.release']),
|
|
1173
|
+
executor(adbPath, [...shellPrefix, 'getprop', 'ro.build.version.sdk']),
|
|
1174
|
+
]);
|
|
1175
|
+
metadata.deviceProperties = {
|
|
1176
|
+
model: model.stdout.trim(),
|
|
1177
|
+
release: release.stdout.trim(),
|
|
1178
|
+
sdk: sdk.stdout.trim(),
|
|
1179
|
+
};
|
|
1180
|
+
raw['adb-device-properties.txt'] = [
|
|
1181
|
+
`model=${model.stdout.trim()}`,
|
|
1182
|
+
`release=${release.stdout.trim()}`,
|
|
1183
|
+
`sdk=${sdk.stdout.trim()}`,
|
|
1184
|
+
].join('\n');
|
|
1185
|
+
let selectedPackageInstalled = false;
|
|
1186
|
+
if (packageName) {
|
|
1187
|
+
const packageCheck = await executor(adbPath, [...shellPrefix, 'pm', 'path', packageName]);
|
|
1188
|
+
raw['adb-package.txt'] = [packageCheck.stdout, packageCheck.stderr].filter(Boolean).join('\n');
|
|
1189
|
+
const packageInstalled = packageCheck.exitCode === 0 && packageCheck.stdout.includes('package:');
|
|
1190
|
+
selectedPackageInstalled = packageInstalled;
|
|
901
1191
|
checks.push({
|
|
902
|
-
name: '
|
|
903
|
-
status: 'failed',
|
|
1192
|
+
name: 'android_package_installed',
|
|
1193
|
+
status: packageInstalled ? 'passed' : 'failed',
|
|
904
1194
|
source: 'runner',
|
|
905
|
-
code:
|
|
906
|
-
|
|
907
|
-
|
|
1195
|
+
code: packageInstalled
|
|
1196
|
+
? 'android_package_installed'
|
|
1197
|
+
: 'android_package_missing',
|
|
1198
|
+
message: packageInstalled
|
|
1199
|
+
? `Package ${packageName} is installed.`
|
|
1200
|
+
: `Package ${packageName} is not installed on ${device.serial}.`,
|
|
1201
|
+
...(!packageInstalled
|
|
1202
|
+
? {
|
|
1203
|
+
metadata: nextActionHint('install_android_package', 'Build and install the app on the selected device, or rerun with --package set to the installed application id.'),
|
|
1204
|
+
}
|
|
1205
|
+
: {}),
|
|
908
1206
|
});
|
|
909
1207
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1208
|
+
if (reactNativeDebugHost) {
|
|
1209
|
+
const reactNativeDebugPort = parseReactNativeDebugHostPort(reactNativeDebugHost);
|
|
1210
|
+
if (!packageName) {
|
|
1211
|
+
checks.push({
|
|
1212
|
+
name: 'android_react_native_debug_host_configured',
|
|
1213
|
+
status: 'failed',
|
|
1214
|
+
source: 'runner',
|
|
1215
|
+
code: 'android_react_native_debug_host_missing_package',
|
|
1216
|
+
message: 'React Native debug host setup was requested, but --package was not provided.',
|
|
1217
|
+
metadata: nextActionHint('provide_android_package', 'Rerun with --package set to the installed Android application id when --react-native-debug-host is enabled.'),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
else if (!selectedPackageInstalled) {
|
|
1221
|
+
checks.push({
|
|
1222
|
+
name: 'android_react_native_debug_host_configured',
|
|
1223
|
+
status: 'failed',
|
|
1224
|
+
source: 'runner',
|
|
1225
|
+
code: 'android_react_native_debug_host_package_missing',
|
|
1226
|
+
message: `React Native debug host setup requires installed package ${packageName}.`,
|
|
1227
|
+
metadata: nextActionHint('install_android_package', 'Build and install the app on the selected device before configuring the React Native debug host.'),
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
else if (!reactNativeDebugPort) {
|
|
1231
|
+
checks.push({
|
|
1232
|
+
name: 'android_react_native_debug_host_configured',
|
|
1233
|
+
status: 'failed',
|
|
1234
|
+
source: 'runner',
|
|
1235
|
+
code: 'android_react_native_debug_host_invalid',
|
|
1236
|
+
message: `React Native debug host ${reactNativeDebugHost} must be a host:port value without a URL scheme.`,
|
|
1237
|
+
metadata: nextActionHint('fix_react_native_debug_host', 'Pass a React Native debug host such as localhost:8097, not a full http:// URL.'),
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
const reverseResult = await executor(adbPath, [
|
|
1242
|
+
'-s',
|
|
1243
|
+
device.serial,
|
|
1244
|
+
'reverse',
|
|
1245
|
+
`tcp:${reactNativeDebugPort}`,
|
|
1246
|
+
`tcp:${reactNativeDebugPort}`,
|
|
1247
|
+
]);
|
|
1248
|
+
const preferenceCommand = buildReactNativeDebugHostPreferenceCommand({
|
|
1249
|
+
debugHost: reactNativeDebugHost,
|
|
1250
|
+
packageName,
|
|
1251
|
+
});
|
|
1252
|
+
const preferenceResult = await executor(adbPath, [
|
|
1253
|
+
'-s',
|
|
1254
|
+
device.serial,
|
|
1255
|
+
'shell',
|
|
1256
|
+
'run-as',
|
|
1257
|
+
packageName,
|
|
1258
|
+
'sh',
|
|
1259
|
+
'-c',
|
|
1260
|
+
quoteAndroidShellArg(preferenceCommand),
|
|
1261
|
+
]);
|
|
1262
|
+
const reversePassed = reverseResult.exitCode === 0;
|
|
1263
|
+
const preferencePassed = preferenceResult.exitCode === 0;
|
|
1264
|
+
raw['adb-react-native-reverse.txt'] = formatAndroidCommandRawOutput(reverseResult);
|
|
1265
|
+
raw['adb-react-native-debug-host.txt'] = formatAndroidCommandRawOutput(preferenceResult);
|
|
1266
|
+
checks.push({
|
|
1267
|
+
name: 'android_react_native_reverse_configured',
|
|
1268
|
+
status: reversePassed ? 'passed' : 'failed',
|
|
1269
|
+
source: 'runner',
|
|
1270
|
+
code: reversePassed
|
|
1271
|
+
? 'android_react_native_reverse_configured'
|
|
1272
|
+
: 'android_react_native_reverse_failed',
|
|
1273
|
+
message: reversePassed
|
|
1274
|
+
? `Configured adb reverse for React Native debug port ${reactNativeDebugPort}.`
|
|
1275
|
+
: `Failed to configure adb reverse for React Native debug port ${reactNativeDebugPort}.`,
|
|
1276
|
+
...(!reversePassed
|
|
1277
|
+
? {
|
|
1278
|
+
metadata: nextActionHint('inspect_android_react_native_reverse', 'Inspect raw/adb-react-native-reverse.txt, confirm the selected device supports adb reverse, then rerun the capture.'),
|
|
1279
|
+
}
|
|
1280
|
+
: {}),
|
|
1281
|
+
});
|
|
1282
|
+
checks.push({
|
|
1283
|
+
name: 'android_react_native_debug_host_configured',
|
|
1284
|
+
status: preferencePassed ? 'passed' : 'failed',
|
|
1285
|
+
source: 'runner',
|
|
1286
|
+
code: preferencePassed
|
|
1287
|
+
? 'android_react_native_debug_host_configured'
|
|
1288
|
+
: 'android_react_native_debug_host_failed',
|
|
1289
|
+
message: preferencePassed
|
|
1290
|
+
? `Configured React Native debug host ${reactNativeDebugHost} for ${packageName}.`
|
|
1291
|
+
: `Failed to configure React Native debug host ${reactNativeDebugHost} for ${packageName}.`,
|
|
1292
|
+
...(!preferencePassed
|
|
1293
|
+
? {
|
|
1294
|
+
metadata: nextActionHint('inspect_android_react_native_debug_host', 'Inspect raw/adb-react-native-debug-host.txt, confirm the app is debuggable and run-as works for the package, then rerun the capture.'),
|
|
1295
|
+
}
|
|
1296
|
+
: {}),
|
|
1297
|
+
});
|
|
1298
|
+
metadata.reactNativeDebugHostSetup = {
|
|
1299
|
+
debugHost: reactNativeDebugHost,
|
|
1300
|
+
port: reactNativeDebugPort,
|
|
1301
|
+
preferenceRawPath: 'raw/adb-react-native-debug-host.txt',
|
|
1302
|
+
reverseRawPath: 'raw/adb-react-native-reverse.txt',
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
919
1305
|
}
|
|
920
|
-
|
|
1306
|
+
if (clearLogcat) {
|
|
1307
|
+
const clear = await driver.clearLogs();
|
|
1308
|
+
const logcatCleared = clear.exitCode === 0;
|
|
1309
|
+
raw[clear.rawFileName] = formatAndroidAdbRawOutput(clear);
|
|
921
1310
|
checks.push({
|
|
922
|
-
name: '
|
|
923
|
-
status: 'failed',
|
|
1311
|
+
name: 'android_logcat_cleared',
|
|
1312
|
+
status: logcatCleared ? 'passed' : 'failed',
|
|
924
1313
|
source: 'runner',
|
|
925
|
-
code: '
|
|
926
|
-
message:
|
|
927
|
-
|
|
1314
|
+
code: logcatCleared ? 'android_logcat_cleared' : 'android_logcat_clear_failed',
|
|
1315
|
+
message: logcatCleared ? 'Cleared adb logcat before capture.' : 'adb logcat clear failed.',
|
|
1316
|
+
...(!logcatCleared
|
|
1317
|
+
? {
|
|
1318
|
+
metadata: nextActionHint('inspect_adb_logcat_clear', `Inspect raw/${clear.rawFileName}, confirm the selected device allows logcat access, then rerun the capture.`),
|
|
1319
|
+
}
|
|
1320
|
+
: {}),
|
|
928
1321
|
});
|
|
1322
|
+
metadata.logcatClear = {
|
|
1323
|
+
args: clear.args,
|
|
1324
|
+
exitCode: clear.exitCode,
|
|
1325
|
+
rawPath: `raw/${clear.rawFileName}`,
|
|
1326
|
+
};
|
|
929
1327
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1328
|
+
const appLifecycleMetadata = {};
|
|
1329
|
+
let lifecyclePackageName = null;
|
|
1330
|
+
let knownLifecyclePids = [];
|
|
1331
|
+
if (launch) {
|
|
1332
|
+
if (!packageName) {
|
|
1333
|
+
checks.push({
|
|
1334
|
+
name: 'android_package_launched',
|
|
1335
|
+
status: 'failed',
|
|
1336
|
+
source: 'runner',
|
|
1337
|
+
code: 'android_launch_missing_package',
|
|
1338
|
+
message: 'Package launch was requested, but --package was not provided.',
|
|
1339
|
+
metadata: nextActionHint('provide_android_package', 'Rerun with --package set to the installed Android application id when --launch is enabled.'),
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
const launchResult = await driver.launchPackage(packageName);
|
|
1344
|
+
const launchPassed = launchResult.exitCode === 0;
|
|
1345
|
+
raw[launchResult.rawFileName] = formatAndroidAdbRawOutput(launchResult);
|
|
1346
|
+
checks.push({
|
|
1347
|
+
name: 'android_package_launched',
|
|
1348
|
+
status: launchPassed ? 'passed' : 'failed',
|
|
1349
|
+
source: 'runner',
|
|
1350
|
+
code: launchPassed ? 'android_package_launched' : 'android_package_launch_failed',
|
|
1351
|
+
message: launchPassed
|
|
1352
|
+
? `Launched package ${packageName}.`
|
|
1353
|
+
: `Failed to launch package ${packageName}.`,
|
|
1354
|
+
...(!launchPassed
|
|
1355
|
+
? {
|
|
1356
|
+
metadata: nextActionHint('inspect_android_launch', `Inspect raw/${launchResult.rawFileName}, verify the package has a launcher activity, and confirm the app can open manually on the device.`),
|
|
1357
|
+
}
|
|
1358
|
+
: {}),
|
|
1359
|
+
});
|
|
1360
|
+
metadata.launchResult = {
|
|
1361
|
+
args: launchResult.args,
|
|
1362
|
+
exitCode: launchResult.exitCode,
|
|
1363
|
+
rawPath: `raw/${launchResult.rawFileName}`,
|
|
1364
|
+
};
|
|
1365
|
+
if (launchPassed && launchWaitMs > 0) {
|
|
1366
|
+
await wait(launchWaitMs);
|
|
1367
|
+
checks.push({
|
|
1368
|
+
name: 'android_launch_waited',
|
|
1369
|
+
status: 'passed',
|
|
1370
|
+
source: 'runner',
|
|
1371
|
+
code: 'android_launch_waited',
|
|
1372
|
+
message: `Waited ${launchWaitMs}ms after Android package launch.`,
|
|
1373
|
+
});
|
|
1374
|
+
metadata.launchWaitMs = launchWaitMs;
|
|
1375
|
+
}
|
|
1376
|
+
if (launchPassed) {
|
|
1377
|
+
lifecyclePackageName = packageName;
|
|
1378
|
+
const pidofAfterLaunch = await executor(adbPath, [
|
|
1379
|
+
'-s',
|
|
1380
|
+
device.serial,
|
|
1381
|
+
'shell',
|
|
1382
|
+
'pidof',
|
|
1383
|
+
packageName,
|
|
1384
|
+
]);
|
|
1385
|
+
const rawPath = 'raw/adb-app-pidof-after-launch.txt';
|
|
1386
|
+
raw['adb-app-pidof-after-launch.txt'] = formatAndroidCommandRawOutput(pidofAfterLaunch);
|
|
1387
|
+
const afterLaunchPids = parseAndroidPidofOutput(pidofAfterLaunch.stdout);
|
|
1388
|
+
knownLifecyclePids = afterLaunchPids;
|
|
1389
|
+
const runningAfterLaunch = pidofAfterLaunch.exitCode === 0 && afterLaunchPids.length > 0;
|
|
1390
|
+
checks.push({
|
|
1391
|
+
name: 'android_app_process_running_after_launch',
|
|
1392
|
+
status: runningAfterLaunch ? 'passed' : 'failed',
|
|
1393
|
+
source: 'runner',
|
|
1394
|
+
code: runningAfterLaunch
|
|
1395
|
+
? 'android_app_process_running_after_launch'
|
|
1396
|
+
: 'android_app_not_running_after_launch',
|
|
1397
|
+
message: runningAfterLaunch
|
|
1398
|
+
? `Package ${packageName} is running after launch with PID ${afterLaunchPids.join(', ')}.`
|
|
1399
|
+
: `Package ${packageName} is not running after launch.`,
|
|
1400
|
+
...(!runningAfterLaunch
|
|
1401
|
+
? {
|
|
1402
|
+
metadata: nextActionHint('inspect_android_app_launch', `Inspect ${rawPath} and the app's device logs to find why the launched process exited before evidence capture.`),
|
|
1403
|
+
}
|
|
1404
|
+
: {}),
|
|
1405
|
+
});
|
|
1406
|
+
Object.assign(appLifecycleMetadata, {
|
|
1407
|
+
afterLaunchPids,
|
|
1408
|
+
afterLaunchRawPath: rawPath,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
let startupReady = true;
|
|
1414
|
+
for (const [index, deepLink] of startupDeepLinks.entries()) {
|
|
1415
|
+
const rawFileName = `adb-startup-deep-link-${index + 1}.txt`;
|
|
1416
|
+
const deepLinkResult = await driver.openDeepLink({
|
|
940
1417
|
packageName,
|
|
1418
|
+
rawFileName,
|
|
1419
|
+
url: deepLink.url,
|
|
941
1420
|
});
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
packageName,
|
|
948
|
-
'sh',
|
|
949
|
-
'-c',
|
|
950
|
-
quoteAndroidShellArg(preferenceCommand),
|
|
951
|
-
]);
|
|
952
|
-
const reversePassed = reverseResult.exitCode === 0;
|
|
953
|
-
const preferencePassed = preferenceResult.exitCode === 0;
|
|
954
|
-
raw['adb-react-native-reverse.txt'] = formatAndroidCommandRawOutput(reverseResult);
|
|
955
|
-
raw['adb-react-native-debug-host.txt'] = formatAndroidCommandRawOutput(preferenceResult);
|
|
1421
|
+
const deepLinkOpened = deepLinkResult.exitCode === 0;
|
|
1422
|
+
if (!deepLinkOpened) {
|
|
1423
|
+
startupReady = false;
|
|
1424
|
+
}
|
|
1425
|
+
raw[deepLinkResult.rawFileName] = formatAndroidAdbRawOutput(deepLinkResult);
|
|
956
1426
|
checks.push({
|
|
957
|
-
name: '
|
|
958
|
-
status:
|
|
1427
|
+
name: 'android_startup_deep_link_opened',
|
|
1428
|
+
status: deepLinkOpened ? 'passed' : 'failed',
|
|
959
1429
|
source: 'runner',
|
|
960
|
-
code:
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
: `Failed to configure adb reverse for React Native debug port ${reactNativeDebugPort}.`,
|
|
966
|
-
...(!reversePassed
|
|
1430
|
+
code: deepLinkOpened ? 'android_startup_deep_link_opened' : 'android_startup_deep_link_failed',
|
|
1431
|
+
message: deepLinkOpened
|
|
1432
|
+
? `Opened Android startup deep link ${deepLink.label ?? index + 1}.`
|
|
1433
|
+
: `Failed to open Android startup deep link ${deepLink.label ?? index + 1}.`,
|
|
1434
|
+
...(!deepLinkOpened
|
|
967
1435
|
? {
|
|
968
|
-
metadata: nextActionHint('
|
|
1436
|
+
metadata: nextActionHint('inspect_android_startup_deep_link', `Inspect raw/${deepLinkResult.rawFileName}, verify the dev-client URL and package intent filter, then rerun the capture.`),
|
|
969
1437
|
}
|
|
970
1438
|
: {}),
|
|
971
1439
|
});
|
|
1440
|
+
if (deepLink.waitMs && deepLink.waitMs > 0) {
|
|
1441
|
+
await wait(deepLink.waitMs);
|
|
1442
|
+
checks.push({
|
|
1443
|
+
name: 'android_startup_deep_link_waited',
|
|
1444
|
+
status: 'passed',
|
|
1445
|
+
source: 'runner',
|
|
1446
|
+
code: 'android_startup_deep_link_waited',
|
|
1447
|
+
message: `Waited ${deepLink.waitMs}ms after Android startup deep link ${deepLink.label ?? index + 1}.`,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
if (deepLink.readyLogPattern) {
|
|
1451
|
+
const rawFileName = `adb-startup-deep-link-${index + 1}-ready-log.txt`;
|
|
1452
|
+
const readyLog = await waitForAndroidReadyLog({
|
|
1453
|
+
driver,
|
|
1454
|
+
logcatLines,
|
|
1455
|
+
pattern: deepLink.readyLogPattern,
|
|
1456
|
+
quietMs: deepLink.readyLogQuietMs ?? 0,
|
|
1457
|
+
rawFileName,
|
|
1458
|
+
timeoutMs: deepLink.readyLogTimeoutMs ?? 60000,
|
|
1459
|
+
wait,
|
|
1460
|
+
});
|
|
1461
|
+
if (!readyLog.ready) {
|
|
1462
|
+
startupReady = false;
|
|
1463
|
+
}
|
|
1464
|
+
raw[rawFileName] = formatAndroidAdbRawOutput(readyLog.result);
|
|
1465
|
+
checks.push({
|
|
1466
|
+
name: 'android_startup_deep_link_ready',
|
|
1467
|
+
status: readyLog.ready ? 'passed' : 'failed',
|
|
1468
|
+
source: 'runner',
|
|
1469
|
+
code: readyLog.ready
|
|
1470
|
+
? 'android_startup_deep_link_ready'
|
|
1471
|
+
: 'android_startup_deep_link_not_ready',
|
|
1472
|
+
message: readyLog.ready
|
|
1473
|
+
? `Android startup deep link ${deepLink.label ?? index + 1} emitted readiness log evidence.`
|
|
1474
|
+
: `Android startup deep link ${deepLink.label ?? index + 1} did not emit readiness log evidence before timeout.`,
|
|
1475
|
+
...(!readyLog.ready
|
|
1476
|
+
? {
|
|
1477
|
+
metadata: nextActionHint('inspect_android_startup_deep_link_ready_log', `Inspect raw/${rawFileName}, confirm the dev-client loaded the app bundle, and increase the ready timeout only if the app is still making progress.`),
|
|
1478
|
+
}
|
|
1479
|
+
: {}),
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (!startupReady && (storageWrites.length > 0 || deepLinks.length > 0)) {
|
|
972
1484
|
checks.push({
|
|
973
|
-
name: '
|
|
974
|
-
status:
|
|
1485
|
+
name: 'android_startup_ready_for_control',
|
|
1486
|
+
status: 'failed',
|
|
975
1487
|
source: 'runner',
|
|
976
|
-
code:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
message: preferencePassed
|
|
980
|
-
? `Configured React Native debug host ${reactNativeDebugHost} for ${packageName}.`
|
|
981
|
-
: `Failed to configure React Native debug host ${reactNativeDebugHost} for ${packageName}.`,
|
|
982
|
-
...(!preferencePassed
|
|
983
|
-
? {
|
|
984
|
-
metadata: nextActionHint('inspect_android_react_native_debug_host', 'Inspect raw/adb-react-native-debug-host.txt, confirm the app is debuggable and run-as works for the package, then rerun the capture.'),
|
|
985
|
-
}
|
|
986
|
-
: {}),
|
|
1488
|
+
code: 'android_startup_not_ready_for_control',
|
|
1489
|
+
message: 'Android startup readiness failed before profile-session control was delivered.',
|
|
1490
|
+
metadata: nextActionHint('verify_android_startup_readiness', 'Inspect startup deep-link and ready-log artifacts, confirm the app loaded the expected bundle, then rerun before delivering profile-session storage or command deep links.'),
|
|
987
1491
|
});
|
|
988
|
-
metadata.reactNativeDebugHostSetup = {
|
|
989
|
-
debugHost: reactNativeDebugHost,
|
|
990
|
-
port: reactNativeDebugPort,
|
|
991
|
-
preferenceRawPath: 'raw/adb-react-native-debug-host.txt',
|
|
992
|
-
reverseRawPath: 'raw/adb-react-native-reverse.txt',
|
|
993
|
-
};
|
|
994
1492
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1493
|
+
for (const [index, write] of storageWrites.entries()) {
|
|
1494
|
+
const rawFileName = `adb-async-storage-write-${index + 1}.txt`;
|
|
1495
|
+
if (!startupReady) {
|
|
1496
|
+
checks.push({
|
|
1497
|
+
name: 'android_async_storage_written',
|
|
1498
|
+
status: 'failed',
|
|
1499
|
+
source: 'runner',
|
|
1500
|
+
code: 'android_async_storage_skipped_startup_not_ready',
|
|
1501
|
+
message: `Skipped Android AsyncStorage value ${write.label ?? index + 1} because startup readiness failed.`,
|
|
1502
|
+
metadata: nextActionHint('verify_android_startup_readiness', 'Rerun after Android startup readiness passes; ASL will not write profile-session storage into an app that has not proven bundle readiness.'),
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (!packageName) {
|
|
1507
|
+
checks.push({
|
|
1508
|
+
name: 'android_async_storage_written',
|
|
1509
|
+
status: 'failed',
|
|
1510
|
+
source: 'runner',
|
|
1511
|
+
code: 'android_async_storage_missing_package',
|
|
1512
|
+
message: 'Android AsyncStorage write was requested, but --package was not provided.',
|
|
1513
|
+
metadata: nextActionHint('provide_android_package', 'Rerun with --package set to the installed Android application id when Android AsyncStorage profile-session storage is enabled.'),
|
|
1514
|
+
});
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
let resolvedWrite = write;
|
|
1518
|
+
if (write.value.includes(ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER)) {
|
|
1519
|
+
const deviceEpoch = await readAndroidDeviceEpochMs({
|
|
1520
|
+
adbPath,
|
|
1521
|
+
deviceSerial: device.serial,
|
|
1522
|
+
executor,
|
|
1523
|
+
});
|
|
1524
|
+
raw['adb-device-epoch-ms.txt'] = formatAndroidCommandRawOutput(deviceEpoch);
|
|
1525
|
+
const deviceClockRead = typeof deviceEpoch.epochMs === 'number';
|
|
1526
|
+
checks.push({
|
|
1527
|
+
name: 'android_device_clock_read',
|
|
1528
|
+
status: deviceClockRead ? 'passed' : 'failed',
|
|
1529
|
+
source: 'runner',
|
|
1530
|
+
code: deviceClockRead ? 'android_device_clock_read' : 'android_device_clock_unavailable',
|
|
1531
|
+
message: deviceClockRead
|
|
1532
|
+
? `Read Android device clock as ${deviceEpoch.epochMs}ms since epoch.`
|
|
1533
|
+
: 'Failed to read Android device clock for AsyncStorage timing evidence.',
|
|
1534
|
+
...(!deviceClockRead
|
|
1535
|
+
? {
|
|
1536
|
+
metadata: nextActionHint('inspect_android_device_clock', 'Inspect raw/adb-device-epoch-ms.txt and confirm the selected Android device supports `adb shell date +%s` before using storage-backed timing evidence.'),
|
|
1537
|
+
}
|
|
1538
|
+
: {}),
|
|
1539
|
+
});
|
|
1540
|
+
if (!deviceClockRead || deviceEpoch.epochMs === null) {
|
|
1541
|
+
continue;
|
|
1009
1542
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1543
|
+
resolvedWrite = {
|
|
1544
|
+
...write,
|
|
1545
|
+
value: resolveAndroidDeviceEpochPlaceholders(write.value, deviceEpoch.epochMs),
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
const writeResult = await executor(adbPath, [
|
|
1549
|
+
'-s',
|
|
1550
|
+
device.serial,
|
|
1551
|
+
'shell',
|
|
1552
|
+
buildAndroidAsyncStorageWriteCommand({ packageName, write: resolvedWrite }),
|
|
1553
|
+
]);
|
|
1554
|
+
const writePassed = writeResult.exitCode === 0;
|
|
1555
|
+
raw[rawFileName] = formatAndroidCommandRawOutput(writeResult);
|
|
1023
1556
|
checks.push({
|
|
1024
|
-
name: '
|
|
1025
|
-
status: 'failed',
|
|
1557
|
+
name: 'android_async_storage_written',
|
|
1558
|
+
status: writePassed ? 'passed' : 'failed',
|
|
1026
1559
|
source: 'runner',
|
|
1027
|
-
code: '
|
|
1028
|
-
message:
|
|
1029
|
-
|
|
1560
|
+
code: writePassed ? 'android_async_storage_written' : 'android_async_storage_write_failed',
|
|
1561
|
+
message: writePassed
|
|
1562
|
+
? `Wrote Android AsyncStorage value ${write.label ?? index + 1}.`
|
|
1563
|
+
: `Failed to write Android AsyncStorage value ${write.label ?? index + 1}.`,
|
|
1564
|
+
...(!writePassed
|
|
1565
|
+
? {
|
|
1566
|
+
metadata: nextActionHint('inspect_android_async_storage_write', `Inspect raw/${rawFileName}, confirm the app is debuggable and sqlite3 can access RKStorage through run-as, then rerun the capture.`),
|
|
1567
|
+
}
|
|
1568
|
+
: {}),
|
|
1030
1569
|
});
|
|
1570
|
+
if (write.waitMs && write.waitMs > 0) {
|
|
1571
|
+
await wait(write.waitMs);
|
|
1572
|
+
checks.push({
|
|
1573
|
+
name: 'android_async_storage_waited',
|
|
1574
|
+
status: 'passed',
|
|
1575
|
+
source: 'runner',
|
|
1576
|
+
code: 'android_async_storage_waited',
|
|
1577
|
+
message: `Waited ${write.waitMs}ms after Android AsyncStorage write ${write.label ?? index + 1}.`,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1031
1580
|
}
|
|
1032
|
-
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1581
|
+
for (const [index, deepLink] of deepLinks.entries()) {
|
|
1582
|
+
const rawFileName = `adb-deep-link-${index + 1}.txt`;
|
|
1583
|
+
if (!startupReady) {
|
|
1584
|
+
checks.push({
|
|
1585
|
+
name: 'android_deep_link_opened',
|
|
1586
|
+
status: 'failed',
|
|
1587
|
+
source: 'runner',
|
|
1588
|
+
code: 'android_deep_link_skipped_startup_not_ready',
|
|
1589
|
+
message: `Skipped Android deep link ${deepLink.label ?? index + 1} because startup readiness failed.`,
|
|
1590
|
+
metadata: nextActionHint('verify_android_startup_readiness', 'Rerun after Android startup readiness passes; ASL will not deliver profile-session command deep links into an app that has not proven bundle readiness.'),
|
|
1591
|
+
});
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const deepLinkResult = await driver.openDeepLink({
|
|
1595
|
+
packageName,
|
|
1596
|
+
rawFileName,
|
|
1597
|
+
url: deepLink.url,
|
|
1598
|
+
});
|
|
1599
|
+
const deepLinkOpened = deepLinkResult.exitCode === 0;
|
|
1600
|
+
raw[deepLinkResult.rawFileName] = formatAndroidAdbRawOutput(deepLinkResult);
|
|
1036
1601
|
checks.push({
|
|
1037
|
-
name: '
|
|
1038
|
-
status:
|
|
1602
|
+
name: 'android_deep_link_opened',
|
|
1603
|
+
status: deepLinkOpened ? 'passed' : 'failed',
|
|
1039
1604
|
source: 'runner',
|
|
1040
|
-
code:
|
|
1041
|
-
message:
|
|
1042
|
-
? `
|
|
1043
|
-
: `Failed to
|
|
1044
|
-
...(!
|
|
1605
|
+
code: deepLinkOpened ? 'android_deep_link_opened' : 'android_deep_link_failed',
|
|
1606
|
+
message: deepLinkOpened
|
|
1607
|
+
? `Opened Android deep link ${deepLink.label ?? index + 1}.`
|
|
1608
|
+
: `Failed to open Android deep link ${deepLink.label ?? index + 1}.`,
|
|
1609
|
+
...(!deepLinkOpened
|
|
1045
1610
|
? {
|
|
1046
|
-
metadata: nextActionHint('
|
|
1611
|
+
metadata: nextActionHint('inspect_android_deep_link', `Inspect raw/${deepLinkResult.rawFileName}, verify the app scheme/intent filter, and rerun with --package if the intent must target one app.`),
|
|
1047
1612
|
}
|
|
1048
1613
|
: {}),
|
|
1049
1614
|
});
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
exitCode: launchResult.exitCode,
|
|
1053
|
-
rawPath: `raw/${launchResult.rawFileName}`,
|
|
1054
|
-
};
|
|
1055
|
-
if (launchPassed && launchWaitMs > 0) {
|
|
1056
|
-
await wait(launchWaitMs);
|
|
1615
|
+
if (deepLink.waitMs && deepLink.waitMs > 0) {
|
|
1616
|
+
await wait(deepLink.waitMs);
|
|
1057
1617
|
checks.push({
|
|
1058
|
-
name: '
|
|
1618
|
+
name: 'android_deep_link_waited',
|
|
1059
1619
|
status: 'passed',
|
|
1060
1620
|
source: 'runner',
|
|
1061
|
-
code: '
|
|
1062
|
-
message: `Waited ${
|
|
1621
|
+
code: 'android_deep_link_waited',
|
|
1622
|
+
message: `Waited ${deepLink.waitMs}ms after Android deep link ${deepLink.label ?? index + 1}.`,
|
|
1063
1623
|
});
|
|
1064
|
-
metadata.launchWaitMs = launchWaitMs;
|
|
1065
1624
|
}
|
|
1066
|
-
if (
|
|
1625
|
+
if (deepLinkOpened && packageName && !lifecyclePackageName) {
|
|
1067
1626
|
lifecyclePackageName = packageName;
|
|
1068
|
-
const
|
|
1627
|
+
const pidofAfterDeepLink = await executor(adbPath, [
|
|
1069
1628
|
'-s',
|
|
1070
1629
|
device.serial,
|
|
1071
1630
|
'shell',
|
|
1072
1631
|
'pidof',
|
|
1073
1632
|
packageName,
|
|
1074
1633
|
]);
|
|
1075
|
-
const rawPath = 'raw/adb-app-pidof-after-
|
|
1076
|
-
raw['adb-app-pidof-after-
|
|
1077
|
-
const
|
|
1078
|
-
knownLifecyclePids =
|
|
1079
|
-
const
|
|
1634
|
+
const rawPath = 'raw/adb-app-pidof-after-deep-link.txt';
|
|
1635
|
+
raw['adb-app-pidof-after-deep-link.txt'] = formatAndroidCommandRawOutput(pidofAfterDeepLink);
|
|
1636
|
+
const afterDeepLinkPids = parseAndroidPidofOutput(pidofAfterDeepLink.stdout);
|
|
1637
|
+
knownLifecyclePids = afterDeepLinkPids;
|
|
1638
|
+
const runningAfterDeepLink = pidofAfterDeepLink.exitCode === 0 && afterDeepLinkPids.length > 0;
|
|
1080
1639
|
checks.push({
|
|
1081
|
-
name: '
|
|
1082
|
-
status:
|
|
1640
|
+
name: 'android_app_process_running_after_deep_link',
|
|
1641
|
+
status: runningAfterDeepLink ? 'passed' : 'failed',
|
|
1083
1642
|
source: 'runner',
|
|
1084
|
-
code:
|
|
1085
|
-
? '
|
|
1086
|
-
: '
|
|
1087
|
-
message:
|
|
1088
|
-
? `Package ${packageName} is running after
|
|
1089
|
-
: `Package ${packageName} is not running after
|
|
1090
|
-
...(!
|
|
1643
|
+
code: runningAfterDeepLink
|
|
1644
|
+
? 'android_app_process_running_after_deep_link'
|
|
1645
|
+
: 'android_app_not_running_after_deep_link',
|
|
1646
|
+
message: runningAfterDeepLink
|
|
1647
|
+
? `Package ${packageName} is running after deep link with PID ${afterDeepLinkPids.join(', ')}.`
|
|
1648
|
+
: `Package ${packageName} is not running after opening the deep link.`,
|
|
1649
|
+
...(!runningAfterDeepLink
|
|
1091
1650
|
? {
|
|
1092
|
-
metadata: nextActionHint('
|
|
1651
|
+
metadata: nextActionHint('inspect_android_deep_link_launch', `Inspect ${rawPath} and the app's device logs to find why the package-targeted deep link did not leave the app process running.`),
|
|
1093
1652
|
}
|
|
1094
1653
|
: {}),
|
|
1095
1654
|
});
|
|
1096
1655
|
Object.assign(appLifecycleMetadata, {
|
|
1097
|
-
|
|
1098
|
-
|
|
1656
|
+
afterDeepLinkPids,
|
|
1657
|
+
afterDeepLinkRawPath: rawPath,
|
|
1099
1658
|
});
|
|
1100
1659
|
}
|
|
1101
1660
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1661
|
+
const driverActionMetadata = [];
|
|
1662
|
+
const logcatMetadata = [];
|
|
1663
|
+
const selectorResolutionMetadata = [];
|
|
1664
|
+
for (const [index, driverStep] of resolvedDriverSteps.entries()) {
|
|
1665
|
+
if (driverStep.waitMs && driverStep.waitMs > 0) {
|
|
1666
|
+
await wait(driverStep.waitMs);
|
|
1667
|
+
checks.push({
|
|
1668
|
+
name: 'android_capture_window_waited',
|
|
1669
|
+
status: 'passed',
|
|
1670
|
+
source: 'runner',
|
|
1671
|
+
code: 'android_capture_window_waited',
|
|
1672
|
+
message: `Waited ${driverStep.waitMs}ms before running adb driver action ${driverStep.driverAction}.`,
|
|
1673
|
+
metadata: {
|
|
1674
|
+
driverAction: driverStep.driverAction,
|
|
1675
|
+
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
let executableDriverStep = driverStep;
|
|
1680
|
+
if (needsAndroidSelectorResolution(driverStep)) {
|
|
1681
|
+
const selectorRawFileName = `adb-selector-tree-${index + 1}.xml`;
|
|
1682
|
+
const treeResult = await driver.inspectTree({ rawFileName: selectorRawFileName });
|
|
1683
|
+
raw[treeResult.rawFileName] = formatAndroidAdbRawOutput(treeResult);
|
|
1684
|
+
const resolution = treeResult.exitCode === 0 && driverStep.selector
|
|
1685
|
+
? resolveAndroidSelectorFromUiTree({
|
|
1686
|
+
selector: driverStep.selector,
|
|
1687
|
+
uiTreeXml: treeResult.stdout,
|
|
1688
|
+
})
|
|
1689
|
+
: null;
|
|
1690
|
+
const resolved = Boolean(resolution);
|
|
1691
|
+
if (resolution) {
|
|
1692
|
+
executableDriverStep = applyAndroidSelectorResolution({
|
|
1693
|
+
driverStep,
|
|
1694
|
+
resolution,
|
|
1695
|
+
});
|
|
1123
1696
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1697
|
+
checks.push({
|
|
1698
|
+
name: 'android_selector_resolved',
|
|
1699
|
+
status: resolved ? 'passed' : driverStep.required === false ? 'warning' : 'failed',
|
|
1700
|
+
source: 'runner',
|
|
1701
|
+
code: resolved ? 'android_selector_resolved' : 'android_selector_resolution_failed',
|
|
1702
|
+
message: resolved
|
|
1703
|
+
? `Resolved Android selector for adb driver action ${driverStep.driverAction}.`
|
|
1704
|
+
: `Failed to resolve Android selector for adb driver action ${driverStep.driverAction}.`,
|
|
1705
|
+
metadata: {
|
|
1706
|
+
driverAction: driverStep.driverAction,
|
|
1707
|
+
...buildAndroidSelectorHealthMetadata(driverStep.selector),
|
|
1708
|
+
...(!resolved
|
|
1709
|
+
? nextActionHint('fix_android_selector', `Inspect raw/${treeResult.rawFileName}, update the scenario selector, or provide explicit adb coordinates for this driver action.`)
|
|
1710
|
+
: {}),
|
|
1711
|
+
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1712
|
+
},
|
|
1713
|
+
});
|
|
1714
|
+
selectorResolutionMetadata.push({
|
|
1715
|
+
...(resolution ? { bounds: resolution.bounds } : {}),
|
|
1716
|
+
driverAction: driverStep.driverAction,
|
|
1717
|
+
rawPath: `raw/${treeResult.rawFileName}`,
|
|
1718
|
+
...(driverStep.selector ? { selector: driverStep.selector } : {}),
|
|
1719
|
+
status: resolved ? 'passed' : 'failed',
|
|
1720
|
+
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
const driverResult = await runAndroidAdbDriverStep({
|
|
1724
|
+
capturesDir: layout.captures,
|
|
1139
1725
|
driver,
|
|
1726
|
+
driverStep: executableDriverStep,
|
|
1140
1727
|
logcatLines,
|
|
1141
|
-
pattern: deepLink.readyLogPattern,
|
|
1142
|
-
quietMs: deepLink.readyLogQuietMs ?? 0,
|
|
1143
|
-
rawFileName,
|
|
1144
|
-
timeoutMs: deepLink.readyLogTimeoutMs ?? 60000,
|
|
1145
|
-
wait,
|
|
1146
1728
|
});
|
|
1147
|
-
raw[rawFileName] = formatAndroidAdbRawOutput(
|
|
1729
|
+
raw[driverResult.rawFileName] = formatAndroidAdbRawOutput(driverResult);
|
|
1730
|
+
const failed = driverResult.exitCode !== 0;
|
|
1731
|
+
const codeSuffix = androidDriverActionCode(driverStep.driverAction);
|
|
1732
|
+
const isReadLogs = driverStep.driverAction === 'readLogs';
|
|
1148
1733
|
checks.push({
|
|
1149
|
-
name: '
|
|
1150
|
-
status:
|
|
1734
|
+
name: isReadLogs ? 'android_logcat_captured' : `android_${codeSuffix}`,
|
|
1735
|
+
status: failed && driverStep.required === false ? 'warning' : failed ? 'failed' : 'passed',
|
|
1151
1736
|
source: 'runner',
|
|
1152
|
-
code:
|
|
1153
|
-
? '
|
|
1154
|
-
:
|
|
1155
|
-
message:
|
|
1156
|
-
?
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (!packageName) {
|
|
1169
|
-
checks.push({
|
|
1170
|
-
name: 'android_async_storage_written',
|
|
1171
|
-
status: 'failed',
|
|
1172
|
-
source: 'runner',
|
|
1173
|
-
code: 'android_async_storage_missing_package',
|
|
1174
|
-
message: 'Android AsyncStorage write was requested, but --package was not provided.',
|
|
1175
|
-
metadata: nextActionHint('provide_android_package', 'Rerun with --package set to the installed Android application id when Android AsyncStorage profile-session storage is enabled.'),
|
|
1176
|
-
});
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
|
-
let resolvedWrite = write;
|
|
1180
|
-
if (write.value.includes(ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER)) {
|
|
1181
|
-
const deviceEpoch = await readAndroidDeviceEpochMs({
|
|
1182
|
-
adbPath,
|
|
1183
|
-
deviceSerial: device.serial,
|
|
1184
|
-
executor,
|
|
1737
|
+
code: isReadLogs
|
|
1738
|
+
? driverResult.exitCode === 0 ? 'android_logcat_captured' : 'android_logcat_failed'
|
|
1739
|
+
: driverResult.exitCode === 0 ? `android_${codeSuffix}_completed` : `android_${codeSuffix}_failed`,
|
|
1740
|
+
message: isReadLogs
|
|
1741
|
+
? driverResult.exitCode === 0
|
|
1742
|
+
? `Captured the last ${driverStep.lines ?? logcatLines} adb logcat lines.`
|
|
1743
|
+
: 'adb logcat capture failed.'
|
|
1744
|
+
: driverResult.exitCode === 0
|
|
1745
|
+
? `Completed adb driver action ${driverStep.driverAction}.`
|
|
1746
|
+
: `adb driver action ${driverStep.driverAction} failed.`,
|
|
1747
|
+
metadata: {
|
|
1748
|
+
driverAction: executableDriverStep.driverAction,
|
|
1749
|
+
...buildAndroidSelectorHealthMetadata(executableDriverStep.selector),
|
|
1750
|
+
...(failed ? buildAndroidDriverFailureMetadata({ driverResult, isReadLogs }) : {}),
|
|
1751
|
+
...(executableDriverStep.stepId ? { stepId: executableDriverStep.stepId } : {}),
|
|
1752
|
+
},
|
|
1185
1753
|
});
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
code: deviceClockRead ? 'android_device_clock_read' : 'android_device_clock_unavailable',
|
|
1193
|
-
message: deviceClockRead
|
|
1194
|
-
? `Read Android device clock as ${deviceEpoch.epochMs}ms since epoch.`
|
|
1195
|
-
: 'Failed to read Android device clock for AsyncStorage timing evidence.',
|
|
1196
|
-
...(!deviceClockRead
|
|
1197
|
-
? {
|
|
1198
|
-
metadata: nextActionHint('inspect_android_device_clock', 'Inspect raw/adb-device-epoch-ms.txt and confirm the selected Android device supports `adb shell date +%s` before using storage-backed timing evidence.'),
|
|
1199
|
-
}
|
|
1754
|
+
const actionMetadata = {
|
|
1755
|
+
args: driverResult.args,
|
|
1756
|
+
driverAction: executableDriverStep.driverAction,
|
|
1757
|
+
exitCode: driverResult.exitCode,
|
|
1758
|
+
...(driverResult.capturePath
|
|
1759
|
+
? { capturePath: `captures/${path.basename(driverResult.capturePath)}` }
|
|
1200
1760
|
: {}),
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
resolvedWrite = {
|
|
1206
|
-
...write,
|
|
1207
|
-
value: write.value.replaceAll(ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, String(deviceEpoch.epochMs)),
|
|
1761
|
+
rawPath: `raw/${driverResult.rawFileName}`,
|
|
1762
|
+
...(executableDriverStep.selector ? { selector: executableDriverStep.selector } : {}),
|
|
1763
|
+
...(executableDriverStep.stepId ? { stepId: executableDriverStep.stepId } : {}),
|
|
1208
1764
|
};
|
|
1765
|
+
driverActionMetadata.push(actionMetadata);
|
|
1766
|
+
if (executableDriverStep.driverAction === 'readLogs') {
|
|
1767
|
+
logcatMetadata.push(actionMetadata);
|
|
1768
|
+
}
|
|
1209
1769
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
device.serial,
|
|
1213
|
-
'shell',
|
|
1214
|
-
buildAndroidAsyncStorageWriteCommand({ packageName, write: resolvedWrite }),
|
|
1215
|
-
]);
|
|
1216
|
-
const writePassed = writeResult.exitCode === 0;
|
|
1217
|
-
raw[rawFileName] = formatAndroidCommandRawOutput(writeResult);
|
|
1218
|
-
checks.push({
|
|
1219
|
-
name: 'android_async_storage_written',
|
|
1220
|
-
status: writePassed ? 'passed' : 'failed',
|
|
1221
|
-
source: 'runner',
|
|
1222
|
-
code: writePassed ? 'android_async_storage_written' : 'android_async_storage_write_failed',
|
|
1223
|
-
message: writePassed
|
|
1224
|
-
? `Wrote Android AsyncStorage value ${write.label ?? index + 1}.`
|
|
1225
|
-
: `Failed to write Android AsyncStorage value ${write.label ?? index + 1}.`,
|
|
1226
|
-
...(!writePassed
|
|
1227
|
-
? {
|
|
1228
|
-
metadata: nextActionHint('inspect_android_async_storage_write', `Inspect raw/${rawFileName}, confirm the app is debuggable and sqlite3 can access RKStorage through run-as, then rerun the capture.`),
|
|
1229
|
-
}
|
|
1230
|
-
: {}),
|
|
1231
|
-
});
|
|
1232
|
-
if (write.waitMs && write.waitMs > 0) {
|
|
1233
|
-
await wait(write.waitMs);
|
|
1234
|
-
checks.push({
|
|
1235
|
-
name: 'android_async_storage_waited',
|
|
1236
|
-
status: 'passed',
|
|
1237
|
-
source: 'runner',
|
|
1238
|
-
code: 'android_async_storage_waited',
|
|
1239
|
-
message: `Waited ${write.waitMs}ms after Android AsyncStorage write ${write.label ?? index + 1}.`,
|
|
1240
|
-
});
|
|
1770
|
+
if (selectorResolutionMetadata.length > 0) {
|
|
1771
|
+
metadata.selectorResolutions = selectorResolutionMetadata;
|
|
1241
1772
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
rawFileName,
|
|
1248
|
-
url: deepLink.url,
|
|
1249
|
-
});
|
|
1250
|
-
const deepLinkOpened = deepLinkResult.exitCode === 0;
|
|
1251
|
-
raw[deepLinkResult.rawFileName] = formatAndroidAdbRawOutput(deepLinkResult);
|
|
1252
|
-
checks.push({
|
|
1253
|
-
name: 'android_deep_link_opened',
|
|
1254
|
-
status: deepLinkOpened ? 'passed' : 'failed',
|
|
1255
|
-
source: 'runner',
|
|
1256
|
-
code: deepLinkOpened ? 'android_deep_link_opened' : 'android_deep_link_failed',
|
|
1257
|
-
message: deepLinkOpened
|
|
1258
|
-
? `Opened Android deep link ${deepLink.label ?? index + 1}.`
|
|
1259
|
-
: `Failed to open Android deep link ${deepLink.label ?? index + 1}.`,
|
|
1260
|
-
...(!deepLinkOpened
|
|
1261
|
-
? {
|
|
1262
|
-
metadata: nextActionHint('inspect_android_deep_link', `Inspect raw/${deepLinkResult.rawFileName}, verify the app scheme/intent filter, and rerun with --package if the intent must target one app.`),
|
|
1263
|
-
}
|
|
1264
|
-
: {}),
|
|
1265
|
-
});
|
|
1266
|
-
if (deepLink.waitMs && deepLink.waitMs > 0) {
|
|
1267
|
-
await wait(deepLink.waitMs);
|
|
1268
|
-
checks.push({
|
|
1269
|
-
name: 'android_deep_link_waited',
|
|
1270
|
-
status: 'passed',
|
|
1271
|
-
source: 'runner',
|
|
1272
|
-
code: 'android_deep_link_waited',
|
|
1273
|
-
message: `Waited ${deepLink.waitMs}ms after Android deep link ${deepLink.label ?? index + 1}.`,
|
|
1274
|
-
});
|
|
1773
|
+
if (driverActionMetadata.length > 0) {
|
|
1774
|
+
metadata.driverActions = driverActionMetadata;
|
|
1775
|
+
}
|
|
1776
|
+
if (logcatMetadata.length === 1) {
|
|
1777
|
+
metadata.logcat = logcatMetadata[0];
|
|
1275
1778
|
}
|
|
1276
|
-
if (
|
|
1277
|
-
|
|
1278
|
-
|
|
1779
|
+
else if (logcatMetadata.length > 1) {
|
|
1780
|
+
metadata.logcat = logcatMetadata;
|
|
1781
|
+
}
|
|
1782
|
+
if (lifecyclePackageName) {
|
|
1783
|
+
const pidofAfterCapture = await executor(adbPath, [
|
|
1279
1784
|
'-s',
|
|
1280
1785
|
device.serial,
|
|
1281
1786
|
'shell',
|
|
1282
1787
|
'pidof',
|
|
1283
|
-
|
|
1788
|
+
lifecyclePackageName,
|
|
1284
1789
|
]);
|
|
1285
|
-
const
|
|
1286
|
-
raw['adb-app-pidof-after-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1790
|
+
const pidofAfterCaptureRawPath = 'raw/adb-app-pidof-after-capture.txt';
|
|
1791
|
+
raw['adb-app-pidof-after-capture.txt'] = formatAndroidCommandRawOutput(pidofAfterCapture);
|
|
1792
|
+
const afterCapturePids = parseAndroidPidofOutput(pidofAfterCapture.stdout);
|
|
1793
|
+
const lifecycleLogLines = Math.max(logcatLines, 200);
|
|
1794
|
+
const lifecycleLog = await driver.readLogs({
|
|
1795
|
+
lines: lifecycleLogLines,
|
|
1796
|
+
rawFileName: 'adb-app-lifecycle-log.txt',
|
|
1797
|
+
});
|
|
1798
|
+
raw[lifecycleLog.rawFileName] = formatAndroidAdbRawOutput(lifecycleLog);
|
|
1799
|
+
const allKnownPids = Array.from(new Set([...knownLifecyclePids, ...afterCapturePids]));
|
|
1800
|
+
const scan = lifecycleLog.exitCode === 0
|
|
1801
|
+
? scanAndroidAppLifecycleLog({
|
|
1802
|
+
logText: `${lifecycleLog.stdout}\n${lifecycleLog.stderr}`,
|
|
1803
|
+
packageName: lifecyclePackageName,
|
|
1804
|
+
pids: allKnownPids,
|
|
1805
|
+
})
|
|
1806
|
+
: { crashed: false, evidence: [] };
|
|
1807
|
+
const runningAfterCapture = pidofAfterCapture.exitCode === 0 && afterCapturePids.length > 0;
|
|
1808
|
+
const lifecycleStatus = !runningAfterCapture || scan.crashed
|
|
1809
|
+
? 'failed'
|
|
1810
|
+
: lifecycleLog.exitCode === 0
|
|
1811
|
+
? 'passed'
|
|
1812
|
+
: 'warning';
|
|
1290
1813
|
checks.push({
|
|
1291
|
-
name: '
|
|
1292
|
-
status:
|
|
1814
|
+
name: 'android_app_lifecycle_stable',
|
|
1815
|
+
status: lifecycleStatus,
|
|
1293
1816
|
source: 'runner',
|
|
1294
|
-
code:
|
|
1295
|
-
? '
|
|
1296
|
-
:
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1817
|
+
code: !runningAfterCapture
|
|
1818
|
+
? 'android_app_exited_during_capture'
|
|
1819
|
+
: scan.crashed
|
|
1820
|
+
? 'android_app_crashed_during_capture'
|
|
1821
|
+
: lifecycleLog.exitCode === 0
|
|
1822
|
+
? 'android_app_lifecycle_stable'
|
|
1823
|
+
: 'android_app_lifecycle_log_unavailable',
|
|
1824
|
+
message: !runningAfterCapture
|
|
1825
|
+
? `Package ${lifecyclePackageName} was not running after evidence capture.`
|
|
1826
|
+
: scan.crashed
|
|
1827
|
+
? `Package ${lifecyclePackageName} emitted crash evidence during capture.`
|
|
1828
|
+
: lifecycleLog.exitCode === 0
|
|
1829
|
+
? `Package ${lifecyclePackageName} remained running with no crash evidence in the bounded log window.`
|
|
1830
|
+
: `Could not read Android lifecycle logs for package ${lifecyclePackageName}.`,
|
|
1831
|
+
...(!runningAfterCapture || scan.crashed
|
|
1301
1832
|
? {
|
|
1302
|
-
metadata: nextActionHint('
|
|
1833
|
+
metadata: nextActionHint('inspect_android_app_crash', `Inspect raw/${lifecycleLog.rawFileName} and ${pidofAfterCaptureRawPath}; scenario timing evidence is not trustworthy until the app stays alive.`),
|
|
1303
1834
|
}
|
|
1304
|
-
:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1835
|
+
: lifecycleLog.exitCode !== 0
|
|
1836
|
+
? {
|
|
1837
|
+
metadata: nextActionHint('inspect_android_lifecycle_log', `Inspect raw/${lifecycleLog.rawFileName}; lifecycle log capture failed but the app process was still running.`),
|
|
1838
|
+
}
|
|
1839
|
+
: {}),
|
|
1309
1840
|
});
|
|
1841
|
+
metadata.appLifecycle = {
|
|
1842
|
+
...appLifecycleMetadata,
|
|
1843
|
+
afterCapturePids,
|
|
1844
|
+
afterCaptureRawPath: pidofAfterCaptureRawPath,
|
|
1845
|
+
crashEvidence: scan.evidence,
|
|
1846
|
+
lifecycleLogLines,
|
|
1847
|
+
lifecycleLogRawPath: `raw/${lifecycleLog.rawFileName}`,
|
|
1848
|
+
packageName: lifecyclePackageName,
|
|
1849
|
+
};
|
|
1310
1850
|
}
|
|
1311
1851
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
const selectorResolutionMetadata = [];
|
|
1315
|
-
for (const [index, driverStep] of resolvedDriverSteps.entries()) {
|
|
1316
|
-
if (driverStep.waitMs && driverStep.waitMs > 0) {
|
|
1317
|
-
await wait(driverStep.waitMs);
|
|
1852
|
+
else {
|
|
1853
|
+
if (clearLogcat || launch || startupDeepLinks.length > 0 || storageWrites.length > 0) {
|
|
1318
1854
|
checks.push({
|
|
1319
|
-
name: '
|
|
1320
|
-
status: '
|
|
1855
|
+
name: 'android_capture_window_started',
|
|
1856
|
+
status: 'failed',
|
|
1321
1857
|
source: 'runner',
|
|
1322
|
-
code: '
|
|
1323
|
-
message:
|
|
1324
|
-
metadata:
|
|
1325
|
-
driverAction: driverStep.driverAction,
|
|
1326
|
-
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1327
|
-
},
|
|
1858
|
+
code: 'android_capture_window_no_device',
|
|
1859
|
+
message: 'Android capture window setup was requested, but no online Android device was selected.',
|
|
1860
|
+
metadata: nextActionHint('select_android_device', 'Start or unlock an Android emulator/device, confirm it appears as `device` in adb devices -l, then rerun the capture.'),
|
|
1328
1861
|
});
|
|
1329
1862
|
}
|
|
1330
|
-
|
|
1331
|
-
if (needsAndroidSelectorResolution(driverStep)) {
|
|
1332
|
-
const selectorRawFileName = `adb-selector-tree-${index + 1}.xml`;
|
|
1333
|
-
const treeResult = await driver.inspectTree({ rawFileName: selectorRawFileName });
|
|
1334
|
-
raw[treeResult.rawFileName] = formatAndroidAdbRawOutput(treeResult);
|
|
1335
|
-
const resolution = treeResult.exitCode === 0 && driverStep.selector
|
|
1336
|
-
? resolveAndroidSelectorFromUiTree({
|
|
1337
|
-
selector: driverStep.selector,
|
|
1338
|
-
uiTreeXml: treeResult.stdout,
|
|
1339
|
-
})
|
|
1340
|
-
: null;
|
|
1341
|
-
const resolved = Boolean(resolution);
|
|
1342
|
-
if (resolution) {
|
|
1343
|
-
executableDriverStep = applyAndroidSelectorResolution({
|
|
1344
|
-
driverStep,
|
|
1345
|
-
resolution,
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1863
|
+
if (resolvedDriverSteps.some((step) => step.driverAction === 'readLogs')) {
|
|
1348
1864
|
checks.push({
|
|
1349
|
-
name: '
|
|
1350
|
-
status:
|
|
1865
|
+
name: 'android_logcat_captured',
|
|
1866
|
+
status: 'failed',
|
|
1351
1867
|
source: 'runner',
|
|
1352
|
-
code:
|
|
1353
|
-
message:
|
|
1354
|
-
|
|
1355
|
-
: `Failed to resolve Android selector for adb driver action ${driverStep.driverAction}.`,
|
|
1356
|
-
metadata: {
|
|
1357
|
-
driverAction: driverStep.driverAction,
|
|
1358
|
-
...buildAndroidSelectorHealthMetadata(driverStep.selector),
|
|
1359
|
-
...(!resolved
|
|
1360
|
-
? nextActionHint('fix_android_selector', `Inspect raw/${treeResult.rawFileName}, update the scenario selector, or provide explicit adb coordinates for this driver action.`)
|
|
1361
|
-
: {}),
|
|
1362
|
-
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1363
|
-
},
|
|
1364
|
-
});
|
|
1365
|
-
selectorResolutionMetadata.push({
|
|
1366
|
-
...(resolution ? { bounds: resolution.bounds } : {}),
|
|
1367
|
-
driverAction: driverStep.driverAction,
|
|
1368
|
-
rawPath: `raw/${treeResult.rawFileName}`,
|
|
1369
|
-
...(driverStep.selector ? { selector: driverStep.selector } : {}),
|
|
1370
|
-
status: resolved ? 'passed' : 'failed',
|
|
1371
|
-
...(driverStep.stepId ? { stepId: driverStep.stepId } : {}),
|
|
1868
|
+
code: 'android_logcat_no_device',
|
|
1869
|
+
message: 'adb logcat capture was requested, but no online Android device was selected.',
|
|
1870
|
+
metadata: nextActionHint('select_android_device', 'Start or unlock an Android emulator/device before requesting logcat capture.'),
|
|
1372
1871
|
});
|
|
1373
1872
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const isReadLogs = driverStep.driverAction === 'readLogs';
|
|
1384
|
-
checks.push({
|
|
1385
|
-
name: isReadLogs ? 'android_logcat_captured' : `android_${codeSuffix}`,
|
|
1386
|
-
status: failed && driverStep.required === false ? 'warning' : failed ? 'failed' : 'passed',
|
|
1387
|
-
source: 'runner',
|
|
1388
|
-
code: isReadLogs
|
|
1389
|
-
? driverResult.exitCode === 0 ? 'android_logcat_captured' : 'android_logcat_failed'
|
|
1390
|
-
: driverResult.exitCode === 0 ? `android_${codeSuffix}_completed` : `android_${codeSuffix}_failed`,
|
|
1391
|
-
message: isReadLogs
|
|
1392
|
-
? driverResult.exitCode === 0
|
|
1393
|
-
? `Captured the last ${driverStep.lines ?? logcatLines} adb logcat lines.`
|
|
1394
|
-
: 'adb logcat capture failed.'
|
|
1395
|
-
: driverResult.exitCode === 0
|
|
1396
|
-
? `Completed adb driver action ${driverStep.driverAction}.`
|
|
1397
|
-
: `adb driver action ${driverStep.driverAction} failed.`,
|
|
1398
|
-
metadata: {
|
|
1399
|
-
driverAction: executableDriverStep.driverAction,
|
|
1400
|
-
...buildAndroidSelectorHealthMetadata(executableDriverStep.selector),
|
|
1401
|
-
...(failed ? buildAndroidDriverFailureMetadata({ driverResult, isReadLogs }) : {}),
|
|
1402
|
-
...(executableDriverStep.stepId ? { stepId: executableDriverStep.stepId } : {}),
|
|
1403
|
-
},
|
|
1404
|
-
});
|
|
1405
|
-
const actionMetadata = {
|
|
1406
|
-
args: driverResult.args,
|
|
1407
|
-
driverAction: executableDriverStep.driverAction,
|
|
1408
|
-
exitCode: driverResult.exitCode,
|
|
1409
|
-
...(driverResult.capturePath
|
|
1410
|
-
? { capturePath: `captures/${path.basename(driverResult.capturePath)}` }
|
|
1411
|
-
: {}),
|
|
1412
|
-
rawPath: `raw/${driverResult.rawFileName}`,
|
|
1413
|
-
...(executableDriverStep.selector ? { selector: executableDriverStep.selector } : {}),
|
|
1414
|
-
...(executableDriverStep.stepId ? { stepId: executableDriverStep.stepId } : {}),
|
|
1415
|
-
};
|
|
1416
|
-
driverActionMetadata.push(actionMetadata);
|
|
1417
|
-
if (executableDriverStep.driverAction === 'readLogs') {
|
|
1418
|
-
logcatMetadata.push(actionMetadata);
|
|
1873
|
+
if (resolvedDriverSteps.some((step) => step.driverAction !== 'readLogs')) {
|
|
1874
|
+
checks.push({
|
|
1875
|
+
name: 'android_driver_actions_completed',
|
|
1876
|
+
status: 'failed',
|
|
1877
|
+
source: 'runner',
|
|
1878
|
+
code: 'android_driver_actions_no_device',
|
|
1879
|
+
message: 'adb driver actions were requested, but no online Android device was selected.',
|
|
1880
|
+
metadata: nextActionHint('select_android_device', 'Start or unlock an Android emulator/device before running adb driver actions.'),
|
|
1881
|
+
});
|
|
1419
1882
|
}
|
|
1420
1883
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1427
|
-
if (logcatMetadata.length === 1) {
|
|
1428
|
-
metadata.logcat = logcatMetadata[0];
|
|
1429
|
-
}
|
|
1430
|
-
else if (logcatMetadata.length > 1) {
|
|
1431
|
-
metadata.logcat = logcatMetadata;
|
|
1432
|
-
}
|
|
1433
|
-
if (lifecyclePackageName) {
|
|
1434
|
-
const pidofAfterCapture = await executor(adbPath, [
|
|
1435
|
-
'-s',
|
|
1436
|
-
device.serial,
|
|
1437
|
-
'shell',
|
|
1438
|
-
'pidof',
|
|
1439
|
-
lifecyclePackageName,
|
|
1440
|
-
]);
|
|
1441
|
-
const pidofAfterCaptureRawPath = 'raw/adb-app-pidof-after-capture.txt';
|
|
1442
|
-
raw['adb-app-pidof-after-capture.txt'] = formatAndroidCommandRawOutput(pidofAfterCapture);
|
|
1443
|
-
const afterCapturePids = parseAndroidPidofOutput(pidofAfterCapture.stdout);
|
|
1444
|
-
const lifecycleLogLines = Math.max(logcatLines, 200);
|
|
1445
|
-
const lifecycleLog = await driver.readLogs({
|
|
1446
|
-
lines: lifecycleLogLines,
|
|
1447
|
-
rawFileName: 'adb-app-lifecycle-log.txt',
|
|
1448
|
-
});
|
|
1449
|
-
raw[lifecycleLog.rawFileName] = formatAndroidAdbRawOutput(lifecycleLog);
|
|
1450
|
-
const allKnownPids = Array.from(new Set([...knownLifecyclePids, ...afterCapturePids]));
|
|
1451
|
-
const scan = lifecycleLog.exitCode === 0
|
|
1452
|
-
? scanAndroidAppLifecycleLog({
|
|
1453
|
-
logText: `${lifecycleLog.stdout}\n${lifecycleLog.stderr}`,
|
|
1454
|
-
packageName: lifecyclePackageName,
|
|
1455
|
-
pids: allKnownPids,
|
|
1456
|
-
})
|
|
1457
|
-
: { crashed: false, evidence: [] };
|
|
1458
|
-
const runningAfterCapture = pidofAfterCapture.exitCode === 0 && afterCapturePids.length > 0;
|
|
1459
|
-
const lifecycleStatus = !runningAfterCapture || scan.crashed
|
|
1460
|
-
? 'failed'
|
|
1461
|
-
: lifecycleLog.exitCode === 0
|
|
1462
|
-
? 'passed'
|
|
1463
|
-
: 'warning';
|
|
1464
|
-
checks.push({
|
|
1465
|
-
name: 'android_app_lifecycle_stable',
|
|
1466
|
-
status: lifecycleStatus,
|
|
1467
|
-
source: 'runner',
|
|
1468
|
-
code: !runningAfterCapture
|
|
1469
|
-
? 'android_app_exited_during_capture'
|
|
1470
|
-
: scan.crashed
|
|
1471
|
-
? 'android_app_crashed_during_capture'
|
|
1472
|
-
: lifecycleLog.exitCode === 0
|
|
1473
|
-
? 'android_app_lifecycle_stable'
|
|
1474
|
-
: 'android_app_lifecycle_log_unavailable',
|
|
1475
|
-
message: !runningAfterCapture
|
|
1476
|
-
? `Package ${lifecyclePackageName} was not running after evidence capture.`
|
|
1477
|
-
: scan.crashed
|
|
1478
|
-
? `Package ${lifecyclePackageName} emitted crash evidence during capture.`
|
|
1479
|
-
: lifecycleLog.exitCode === 0
|
|
1480
|
-
? `Package ${lifecyclePackageName} remained running with no crash evidence in the bounded log window.`
|
|
1481
|
-
: `Could not read Android lifecycle logs for package ${lifecyclePackageName}.`,
|
|
1482
|
-
...(!runningAfterCapture || scan.crashed
|
|
1483
|
-
? {
|
|
1484
|
-
metadata: nextActionHint('inspect_android_app_crash', `Inspect raw/${lifecycleLog.rawFileName} and ${pidofAfterCaptureRawPath}; scenario timing evidence is not trustworthy until the app stays alive.`),
|
|
1485
|
-
}
|
|
1486
|
-
: lifecycleLog.exitCode !== 0
|
|
1487
|
-
? {
|
|
1488
|
-
metadata: nextActionHint('inspect_android_lifecycle_log', `Inspect raw/${lifecycleLog.rawFileName}; lifecycle log capture failed but the app process was still running.`),
|
|
1489
|
-
}
|
|
1490
|
-
: {}),
|
|
1491
|
-
});
|
|
1492
|
-
metadata.appLifecycle = {
|
|
1493
|
-
...appLifecycleMetadata,
|
|
1494
|
-
afterCapturePids,
|
|
1495
|
-
afterCaptureRawPath: pidofAfterCaptureRawPath,
|
|
1496
|
-
crashEvidence: scan.evidence,
|
|
1497
|
-
lifecycleLogLines,
|
|
1498
|
-
lifecycleLogRawPath: `raw/${lifecycleLog.rawFileName}`,
|
|
1499
|
-
packageName: lifecyclePackageName,
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1884
|
+
};
|
|
1885
|
+
try {
|
|
1886
|
+
await runAndroidAdbCaptureBodyWithWatchdog({
|
|
1887
|
+
body: runCaptureBody,
|
|
1888
|
+
watchdog: captureWatchdog,
|
|
1889
|
+
});
|
|
1502
1890
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
const timedOut = isAndroidAdbCaptureWatchdogError(error);
|
|
1893
|
+
const failure = {
|
|
1894
|
+
...normalizeAndroidRunnerFailure(error),
|
|
1895
|
+
...(timedOut
|
|
1896
|
+
? {
|
|
1897
|
+
code: error.code,
|
|
1898
|
+
watchdog: error.watchdog,
|
|
1899
|
+
}
|
|
1900
|
+
: {}),
|
|
1901
|
+
};
|
|
1902
|
+
const rawFileName = timedOut ? 'adb-runner-watchdog-timeout.txt' : 'adb-runner-failure.txt';
|
|
1903
|
+
raw[rawFileName] = formatAndroidRunnerFailureRaw(failure);
|
|
1904
|
+
metadata.runnerFailure = {
|
|
1905
|
+
...failure,
|
|
1906
|
+
rawPath: `raw/${rawFileName}`,
|
|
1907
|
+
collectedRawArtifacts: Object.keys(raw).map((fileName) => `raw/${fileName}`),
|
|
1908
|
+
};
|
|
1909
|
+
checks.push({
|
|
1910
|
+
name: 'android_adb_capture_liveness',
|
|
1911
|
+
status: 'failed',
|
|
1912
|
+
source: 'runner',
|
|
1913
|
+
code: timedOut ? 'android_adb_runner_liveness_timeout' : 'android_adb_runner_liveness_failure',
|
|
1914
|
+
message: timedOut
|
|
1915
|
+
? `Android adb capture did not complete before the ${captureWatchdog.timeoutMs}ms watchdog deadline.`
|
|
1916
|
+
: 'Android adb capture stopped before normal artifact finalization.',
|
|
1917
|
+
metadata: {
|
|
1918
|
+
...nextActionHint(timedOut ? 'inspect_android_adb_runner_timeout' : 'inspect_android_adb_runner_failure', timedOut
|
|
1919
|
+
? `Inspect raw/${rawFileName}, raw/android-metadata.json, and the last collected raw adb artifact to identify the command or declared wait that exceeded the whole-capture watchdog.`
|
|
1920
|
+
: `Inspect raw/${rawFileName} and raw/android-metadata.json to determine which adb or runner step stopped before finalization, then rerun with a bounded command timeout if the host command stalled.`),
|
|
1921
|
+
rawPath: `raw/${rawFileName}`,
|
|
1922
|
+
},
|
|
1923
|
+
});
|
|
1534
1924
|
}
|
|
1535
1925
|
const health = buildAndroidHealth({ runId, checks });
|
|
1536
1926
|
const verdict = buildAndroidVerdict({ runId, health });
|
|
1537
1927
|
const agentSummary = buildAgentSummaryMarkdown({ health, verdict });
|
|
1538
|
-
await
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
await writeJsonArtifact({
|
|
1547
|
-
filePath: layout.verdict,
|
|
1548
|
-
value: verdict,
|
|
1549
|
-
schema: SCHEMAS.verdict,
|
|
1550
|
-
label: 'Verdict artifact',
|
|
1551
|
-
});
|
|
1552
|
-
await writeTextArtifact({
|
|
1553
|
-
filePath: layout.agentSummary,
|
|
1554
|
-
content: agentSummary,
|
|
1928
|
+
await writeAndroidAdbArtifacts({
|
|
1929
|
+
agentSummary,
|
|
1930
|
+
health,
|
|
1931
|
+
layout,
|
|
1932
|
+
metadata,
|
|
1933
|
+
raw,
|
|
1934
|
+
rawDir,
|
|
1935
|
+
verdict,
|
|
1555
1936
|
});
|
|
1556
1937
|
return {
|
|
1557
1938
|
agentSummary,
|
|
@@ -1579,6 +1960,7 @@ async function main() {
|
|
|
1579
1960
|
...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
|
|
1580
1961
|
captureLogcat: args['capture-logcat'] === true || args['capture-logcat'] === 'true',
|
|
1581
1962
|
clearLogcat: args['clear-logcat'] === true || args['clear-logcat'] === 'true',
|
|
1963
|
+
commandTimeoutMs: parsePositiveInteger(args['command-timeout-ms'], DEFAULT_ADB_COMMAND_TIMEOUT_MS),
|
|
1582
1964
|
launch: args.launch === true || args.launch === 'true',
|
|
1583
1965
|
launchWaitMs: parsePositiveInteger(args['launch-wait-ms'], 0),
|
|
1584
1966
|
logcatLines: parsePositiveInteger(args['logcat-lines'], 1000),
|