agent-scenario-loop 0.1.0
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/LICENSE +21 -0
- package/README.md +119 -0
- package/app/profile-session.ts +812 -0
- package/core/config-template.json +41 -0
- package/dist/core/agent-summary.d.ts +15 -0
- package/dist/core/agent-summary.js +177 -0
- package/dist/core/artifact-contract.d.ts +151 -0
- package/dist/core/artifact-contract.js +897 -0
- package/dist/core/artifact-layout.d.ts +56 -0
- package/dist/core/artifact-layout.js +61 -0
- package/dist/core/artifact-writer.d.ts +44 -0
- package/dist/core/artifact-writer.js +55 -0
- package/dist/core/comparison.d.ts +133 -0
- package/dist/core/comparison.js +294 -0
- package/dist/core/evidence-interpreter.d.ts +28 -0
- package/dist/core/evidence-interpreter.js +69 -0
- package/dist/core/execution-plan.d.ts +44 -0
- package/dist/core/execution-plan.js +95 -0
- package/dist/core/planner.d.ts +132 -0
- package/dist/core/planner.js +812 -0
- package/dist/core/ports.d.ts +198 -0
- package/dist/core/ports.js +146 -0
- package/dist/core/run-index.d.ts +62 -0
- package/dist/core/run-index.js +143 -0
- package/dist/core/schema-validator.d.ts +86 -0
- package/dist/core/schema-validator.js +407 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/runner/agent-device-driver.d.ts +126 -0
- package/dist/runner/agent-device-driver.js +168 -0
- package/dist/runner/agent-device.d.ts +295 -0
- package/dist/runner/agent-device.js +1271 -0
- package/dist/runner/android-adb-driver.d.ts +175 -0
- package/dist/runner/android-adb-driver.js +399 -0
- package/dist/runner/android-adb.d.ts +254 -0
- package/dist/runner/android-adb.js +1618 -0
- package/dist/runner/argent-driver.d.ts +183 -0
- package/dist/runner/argent-driver.js +297 -0
- package/dist/runner/argent.d.ts +349 -0
- package/dist/runner/argent.js +1211 -0
- package/dist/runner/check-plan.d.ts +45 -0
- package/dist/runner/check-plan.js +210 -0
- package/dist/runner/cli.d.ts +20 -0
- package/dist/runner/cli.js +23 -0
- package/dist/runner/compare-latest.d.ts +99 -0
- package/dist/runner/compare-latest.js +233 -0
- package/dist/runner/compare.d.ts +58 -0
- package/dist/runner/compare.js +157 -0
- package/dist/runner/demo-loop.d.ts +45 -0
- package/dist/runner/demo-loop.js +170 -0
- package/dist/runner/example-android-live.d.ts +137 -0
- package/dist/runner/example-android-live.js +454 -0
- package/dist/runner/example-ios-live.d.ts +137 -0
- package/dist/runner/example-ios-live.js +471 -0
- package/dist/runner/host-doctor.d.ts +131 -0
- package/dist/runner/host-doctor.js +628 -0
- package/dist/runner/init-project.d.ts +88 -0
- package/dist/runner/init-project.js +263 -0
- package/dist/runner/ios-simctl-driver.d.ts +69 -0
- package/dist/runner/ios-simctl-driver.js +97 -0
- package/dist/runner/ios-simctl.d.ts +254 -0
- package/dist/runner/ios-simctl.js +1415 -0
- package/dist/runner/live-android.d.ts +137 -0
- package/dist/runner/live-android.js +539 -0
- package/dist/runner/live-comparison.d.ts +67 -0
- package/dist/runner/live-comparison.js +147 -0
- package/dist/runner/live-ios.d.ts +137 -0
- package/dist/runner/live-ios.js +460 -0
- package/dist/runner/live-proof-summary.d.ts +263 -0
- package/dist/runner/live-proof-summary.js +465 -0
- package/dist/runner/live-proof.d.ts +467 -0
- package/dist/runner/live-proof.js +920 -0
- package/dist/runner/local-env.d.ts +64 -0
- package/dist/runner/local-env.js +155 -0
- package/dist/runner/profile-android.d.ts +82 -0
- package/dist/runner/profile-android.js +671 -0
- package/dist/runner/profile-ios.d.ts +108 -0
- package/dist/runner/profile-ios.js +532 -0
- package/dist/runner/profile-mobile.d.ts +254 -0
- package/dist/runner/profile-mobile.js +1307 -0
- package/dist/runner/validate-project.d.ts +273 -0
- package/dist/runner/validate-project.js +1501 -0
- package/docs/adapters.md +145 -0
- package/docs/api.md +94 -0
- package/docs/authoring.md +196 -0
- package/docs/concepts.md +136 -0
- package/docs/consumer-rehearsal.md +115 -0
- package/docs/contracts.md +267 -0
- package/docs/live-proofs.md +270 -0
- package/docs/principles.md +46 -0
- package/examples/event-logs/app-startup-baseline.log +4 -0
- package/examples/event-logs/app-startup-current.log +4 -0
- package/examples/minimal-app/README.md +70 -0
- package/examples/mobile-app/README.md +302 -0
- package/examples/mobile-app/app.json +22 -0
- package/examples/mobile-app/asl/package-scripts.json +32 -0
- package/examples/mobile-app/asl.config.json +37 -0
- package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
- package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
- package/examples/mobile-app/event-logs/app-startup.log +4 -0
- package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
- package/examples/mobile-app/index.ts +20 -0
- package/examples/mobile-app/metro.config.js +20 -0
- package/examples/mobile-app/package.json +62 -0
- package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
- package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
- package/examples/mobile-app/pnpm-lock.yaml +4440 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
- package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
- package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
- package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
- package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
- package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
- package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
- package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
- package/examples/mobile-app/src/example-screen.tsx +322 -0
- package/examples/mobile-app/tsconfig.json +16 -0
- package/examples/mobile-app/tsconfig.typecheck.json +13 -0
- package/examples/runners/README.md +44 -0
- package/examples/runners/adb-android.json +25 -0
- package/examples/runners/agent-device-android.json +27 -0
- package/examples/runners/agent-device-ios.json +27 -0
- package/examples/runners/argent-android.json +32 -0
- package/examples/runners/argent-ios.json +32 -0
- package/examples/runners/argent-react-profiler-provider.json +15 -0
- package/examples/runners/axe-accessibility-provider.json +24 -0
- package/examples/runners/manual-log-ingest.json +9 -0
- package/examples/runners/rozenite-profiler-provider.json +9 -0
- package/examples/runners/script-accessibility-provider.json +24 -0
- package/examples/runners/script-memory-provider.json +24 -0
- package/examples/runners/script-network-provider.json +24 -0
- package/examples/runners/script-profiler-provider.json +30 -0
- package/examples/runners/xcodebuildmcp-ios.json +29 -0
- package/examples/scenarios/ios/app-startup.json +28 -0
- package/examples/scenarios/ios/open-close-cycle.json +35 -0
- package/examples/scenarios/mobile/app-startup.json +72 -0
- package/examples/scenarios/mobile/media-open-close.json +141 -0
- package/examples/scenarios/mobile/open-close-cycle.json +135 -0
- package/examples/scenarios/mobile/scroll-settle.json +106 -0
- package/package.json +240 -0
- package/schemas/budget-verdict.schema.json +115 -0
- package/schemas/causal-run.schema.json +279 -0
- package/schemas/comparison.schema.json +196 -0
- package/schemas/health.schema.json +108 -0
- package/schemas/live-proof-set.schema.json +195 -0
- package/schemas/live-proof.schema.json +413 -0
- package/schemas/manifest.schema.json +204 -0
- package/schemas/metrics.schema.json +137 -0
- package/schemas/project-validation.schema.json +343 -0
- package/schemas/runner-capabilities.schema.json +217 -0
- package/schemas/scenario.schema.json +400 -0
- package/schemas/verdict.schema.json +88 -0
- package/templates/evidence-provider.json +83 -0
- package/templates/gitignore-snippet +9 -0
- package/templates/integration-readme.md +125 -0
- package/templates/mobile-scenario.json +133 -0
- package/templates/package-scripts.json +32 -0
- package/templates/primary-runner.json +19 -0
- package/templates/project.config.json +37 -0
- package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +127 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.usage = exports.parseArgs = void 0;
|
|
5
|
+
exports.deriveProfileSessionCaptureWaitMs = deriveProfileSessionCaptureWaitMs;
|
|
6
|
+
exports.main = main;
|
|
7
|
+
exports.resolveAndroidAdbProfileCommands = resolveAndroidAdbProfileCommands;
|
|
8
|
+
exports.resolveAndroidAdbDriverSteps = resolveAndroidAdbDriverSteps;
|
|
9
|
+
exports.resolveProfileSessionCaptureWaitMs = resolveProfileSessionCaptureWaitMs;
|
|
10
|
+
exports.readAndroidAdbVideoCapturePath = readAndroidAdbVideoCapturePath;
|
|
11
|
+
exports.validateAndroidAdbDriverSteps = validateAndroidAdbDriverSteps;
|
|
12
|
+
exports.runProfileAndroid = runProfileAndroid;
|
|
13
|
+
exports.summarizeFailedAndroidChecks = summarizeFailedAndroidChecks;
|
|
14
|
+
const crypto = require('node:crypto');
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { hasHelpFlag } = require('./cli');
|
|
18
|
+
const { ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, parsePositiveInteger, runAndroidAdbPreflight, } = require('./android-adb');
|
|
19
|
+
const { parseArgs, readScalarArg, runProfileMobile, usage, } = require('./profile-mobile');
|
|
20
|
+
exports.parseArgs = parseArgs;
|
|
21
|
+
exports.usage = usage;
|
|
22
|
+
const { buildScenarioExecutionPlan } = require('../core/execution-plan');
|
|
23
|
+
const { runAgentDeviceCapture } = require('./agent-device');
|
|
24
|
+
const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
|
|
25
|
+
const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
|
|
26
|
+
const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
|
|
27
|
+
const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
|
|
28
|
+
const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
|
|
29
|
+
/**
|
|
30
|
+
* Reads and parses a JSON object from disk.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} filePath
|
|
33
|
+
* @returns {Record<string, unknown>}
|
|
34
|
+
*/
|
|
35
|
+
function readJson(filePath) {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether a boolean-style CLI flag is enabled.
|
|
40
|
+
*
|
|
41
|
+
* @param {string | boolean | undefined} value
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isEnabled(value) {
|
|
45
|
+
return value === true || value === 'true';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads a positive integer from unknown scenario adapter metadata.
|
|
49
|
+
*
|
|
50
|
+
* @param {unknown} value
|
|
51
|
+
* @param {number} fallback
|
|
52
|
+
* @returns {number}
|
|
53
|
+
*/
|
|
54
|
+
function readPositiveInteger(value, fallback) {
|
|
55
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reads the number of scenario iterations that can emit app-owned truth events.
|
|
59
|
+
*
|
|
60
|
+
* @param {Record<string, unknown>} scenario
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function readScenarioIterationCount(scenario) {
|
|
64
|
+
return readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates a short run id when adb capture must share the id with profile artifacts.
|
|
68
|
+
*
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function createRunId() {
|
|
72
|
+
return crypto.randomBytes(6).toString('hex');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolves the adb capture output directory for a profile run.
|
|
76
|
+
*
|
|
77
|
+
* @param {{args: import('./profile-mobile').CliArgs, runId: string}} options
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function resolveAdbCaptureOutputDir({ args, runId, }) {
|
|
81
|
+
if (typeof args['adb-out'] === 'string') {
|
|
82
|
+
return path.resolve(args['adb-out']);
|
|
83
|
+
}
|
|
84
|
+
if (typeof args.out === 'string') {
|
|
85
|
+
return path.resolve(args.out, '_adb-captures', runId);
|
|
86
|
+
}
|
|
87
|
+
return path.resolve('artifacts/android-adb-captures', runId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the agent-device capture output directory for a profile run.
|
|
91
|
+
*
|
|
92
|
+
* @param {{args: import('./profile-mobile').CliArgs, runId: string}} options
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function resolveAgentDeviceCaptureOutputDir({ args, runId, }) {
|
|
96
|
+
if (typeof args['agent-device-out'] === 'string') {
|
|
97
|
+
return path.resolve(args['agent-device-out']);
|
|
98
|
+
}
|
|
99
|
+
if (typeof args.out === 'string') {
|
|
100
|
+
return path.resolve(args.out, '_agent-device-captures', runId);
|
|
101
|
+
}
|
|
102
|
+
return path.resolve('artifacts/agent-device-captures', runId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the Android package name from explicit CLI input or project config.
|
|
106
|
+
*
|
|
107
|
+
* @param {{args: import('./profile-mobile').CliArgs, config: Record<string, unknown>}} options
|
|
108
|
+
* @returns {string | null}
|
|
109
|
+
*/
|
|
110
|
+
function resolveAndroidPackageName({ args, config, }) {
|
|
111
|
+
if (typeof args.package === 'string') {
|
|
112
|
+
return args.package;
|
|
113
|
+
}
|
|
114
|
+
return typeof config.app?.androidPackage === 'string' ? config.app.androidPackage : null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Builds a profile-session deep link for the example app or another configured app.
|
|
118
|
+
*
|
|
119
|
+
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
123
|
+
const scheme = typeof config.app?.profileSessionScheme === 'string'
|
|
124
|
+
? config.app.profileSessionScheme
|
|
125
|
+
: typeof config.app?.scheme === 'string'
|
|
126
|
+
? config.app.scheme
|
|
127
|
+
: 'app';
|
|
128
|
+
const params = new URLSearchParams({ runId, scenario });
|
|
129
|
+
if (action === 'command' && command) {
|
|
130
|
+
params.set('command', command);
|
|
131
|
+
}
|
|
132
|
+
return `${scheme}://profile-session/${action}?${params.toString()}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Builds Android AsyncStorage writes for one profile-session run.
|
|
136
|
+
*
|
|
137
|
+
* @param {{commands: AndroidAdbProfileCommand[], commandStorageKey: string, commandWaitMs: number, runId: string, scenario: string, sessionStorageKey: string}} options
|
|
138
|
+
* @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
|
|
139
|
+
*/
|
|
140
|
+
function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
clearKeys: [commandStorageKey],
|
|
144
|
+
key: sessionStorageKey,
|
|
145
|
+
label: 'profile-session-start',
|
|
146
|
+
value: JSON.stringify({
|
|
147
|
+
active: true,
|
|
148
|
+
scenario,
|
|
149
|
+
runId,
|
|
150
|
+
startedAt: ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER,
|
|
151
|
+
}).replace(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}"`, ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER),
|
|
152
|
+
waitMs: commandWaitMs,
|
|
153
|
+
},
|
|
154
|
+
...commands.map((profileCommand, index) => {
|
|
155
|
+
const timestamp = Date.now() + index + 1;
|
|
156
|
+
return {
|
|
157
|
+
key: commandStorageKey,
|
|
158
|
+
label: profileCommand.label ?? `profile-command-${index + 1}`,
|
|
159
|
+
value: JSON.stringify([{
|
|
160
|
+
id: `${timestamp}-${scenario}-${profileCommand.command}`,
|
|
161
|
+
scenario,
|
|
162
|
+
runId,
|
|
163
|
+
command: profileCommand.command,
|
|
164
|
+
timestamp,
|
|
165
|
+
}]),
|
|
166
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
167
|
+
};
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Reads Android-specific wait metadata from a normalized execution step.
|
|
173
|
+
*
|
|
174
|
+
* @param {import('../core/execution-plan').ScenarioExecutionStep} step
|
|
175
|
+
* @returns {number}
|
|
176
|
+
*/
|
|
177
|
+
function readStepWaitMs(step) {
|
|
178
|
+
const androidAdbOptions = step.adapterOptions?.androidAdb;
|
|
179
|
+
if (androidAdbOptions && typeof androidAdbOptions === 'object' && !Array.isArray(androidAdbOptions)) {
|
|
180
|
+
const waitMs = androidAdbOptions.waitMs;
|
|
181
|
+
if (typeof waitMs === 'number' && Number.isInteger(waitMs) && waitMs > 0) {
|
|
182
|
+
return waitMs;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return readPositiveInteger(step.timeoutMs, 0);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Derives a logcat-backed profile capture window from scenario waits and cycles.
|
|
189
|
+
*
|
|
190
|
+
* @param {Record<string, unknown>} scenario
|
|
191
|
+
* @returns {number}
|
|
192
|
+
*/
|
|
193
|
+
function deriveProfileSessionCaptureWaitMs(scenario) {
|
|
194
|
+
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
195
|
+
const iterations = readScenarioIterationCount(scenario);
|
|
196
|
+
const perIterationWaitMs = executionPlan.steps.reduce((total, step) => {
|
|
197
|
+
if (step.kind === 'command') {
|
|
198
|
+
return total + readStepWaitMs(step);
|
|
199
|
+
}
|
|
200
|
+
if (step.portMethod === 'waitForTruthEvent') {
|
|
201
|
+
return total + readPositiveInteger(step.timeoutMs, 0);
|
|
202
|
+
}
|
|
203
|
+
return total;
|
|
204
|
+
}, 0);
|
|
205
|
+
const derivedWaitMs = PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS + (perIterationWaitMs * iterations);
|
|
206
|
+
return Math.min(Math.max(derivedWaitMs, PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS), PROFILE_SESSION_CAPTURE_MAX_MS);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolves the Android adb capture wait, keeping explicit CLI waits authoritative.
|
|
210
|
+
*
|
|
211
|
+
* @param {{args: import('./profile-mobile').CliArgs, scenario: Record<string, unknown>, profileSessionEnabled: boolean}} options
|
|
212
|
+
* @returns {number}
|
|
213
|
+
*/
|
|
214
|
+
function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scenario, }) {
|
|
215
|
+
const explicitWaitMs = readScalarArg(args['wait-ms']);
|
|
216
|
+
if (explicitWaitMs !== undefined) {
|
|
217
|
+
return parsePositiveInteger(explicitWaitMs, 0);
|
|
218
|
+
}
|
|
219
|
+
return profileSessionEnabled ? deriveProfileSessionCaptureWaitMs(scenario) : 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Reads Android adb adapter metadata from a normalized scenario step.
|
|
223
|
+
*
|
|
224
|
+
* @param {import('../core/execution-plan').ScenarioExecutionStep} step
|
|
225
|
+
* @returns {Record<string, unknown>}
|
|
226
|
+
*/
|
|
227
|
+
function readAndroidAdbStepOptions(step) {
|
|
228
|
+
const androidAdbOptions = step.adapterOptions?.androidAdb;
|
|
229
|
+
return androidAdbOptions && typeof androidAdbOptions === 'object' && !Array.isArray(androidAdbOptions)
|
|
230
|
+
? androidAdbOptions
|
|
231
|
+
: {};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Reads a finite number from Android adb adapter metadata.
|
|
235
|
+
*
|
|
236
|
+
* @param {unknown} value
|
|
237
|
+
* @returns {number | undefined}
|
|
238
|
+
*/
|
|
239
|
+
function readFiniteNumber(value) {
|
|
240
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Returns true when a normalized execution step has a portable selector adb can try to resolve.
|
|
244
|
+
*
|
|
245
|
+
* @param {unknown} value
|
|
246
|
+
* @returns {value is import('./android-adb-driver').AndroidSelector}
|
|
247
|
+
*/
|
|
248
|
+
function isAndroidSelector(value) {
|
|
249
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const selector = value;
|
|
253
|
+
return typeof selector.kind === 'string' && typeof selector.value === 'string' && selector.value.length > 0;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Appends one repeatable profile capture argument without losing caller-provided values.
|
|
257
|
+
*
|
|
258
|
+
* @param {{args: import('./profile-mobile').CliArgs, value: string}} options
|
|
259
|
+
* @returns {string | boolean | Array<string | boolean>}
|
|
260
|
+
*/
|
|
261
|
+
function appendCaptureArg({ args, value, }) {
|
|
262
|
+
const existing = args.capture;
|
|
263
|
+
return existing === undefined ? value : Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Expands portable scenario command steps into Android profile-session commands.
|
|
267
|
+
*
|
|
268
|
+
* @param {Record<string, unknown>} scenario
|
|
269
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
270
|
+
*/
|
|
271
|
+
function resolveExecutionPlanProfileCommands(scenario) {
|
|
272
|
+
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
273
|
+
const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
|
|
274
|
+
const commands = executionPlan.steps
|
|
275
|
+
.filter((step) => step.portMethod === 'executeStep' && typeof step.command === 'string')
|
|
276
|
+
.map((step) => ({
|
|
277
|
+
command: step.command,
|
|
278
|
+
label: step.id,
|
|
279
|
+
waitMs: readStepWaitMs(step),
|
|
280
|
+
}));
|
|
281
|
+
return Array.from({ length: repeat }).flatMap(() => commands);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
285
|
+
*
|
|
286
|
+
* @param {Record<string, unknown>} scenario
|
|
287
|
+
* @returns {import('./android-adb').AndroidAdbDriverStep[]}
|
|
288
|
+
*/
|
|
289
|
+
function resolveAndroidAdbDriverSteps(scenario) {
|
|
290
|
+
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
291
|
+
let readLogsIndex = 0;
|
|
292
|
+
return executionPlan.steps
|
|
293
|
+
.filter((step) => ['assertVisible', 'inspectTree', 'readLogs', 'record', 'screenshot', 'scroll', 'tap'].includes(String(step.driverAction)))
|
|
294
|
+
.map((step) => {
|
|
295
|
+
const androidAdbOptions = readAndroidAdbStepOptions(step);
|
|
296
|
+
if (step.driverAction === 'readLogs') {
|
|
297
|
+
readLogsIndex += 1;
|
|
298
|
+
}
|
|
299
|
+
const rawFileName = typeof androidAdbOptions.rawFileName === 'string' && androidAdbOptions.rawFileName.length > 0
|
|
300
|
+
? androidAdbOptions.rawFileName
|
|
301
|
+
: step.driverAction === 'readLogs'
|
|
302
|
+
? readLogsIndex === 1
|
|
303
|
+
? 'adb-logcat.txt'
|
|
304
|
+
: `adb-logcat-${readLogsIndex}.txt`
|
|
305
|
+
: undefined;
|
|
306
|
+
return {
|
|
307
|
+
driverAction: step.driverAction,
|
|
308
|
+
...(typeof androidAdbOptions.captureFileName === 'string' && androidAdbOptions.captureFileName.length > 0
|
|
309
|
+
? { captureFileName: androidAdbOptions.captureFileName }
|
|
310
|
+
: {}),
|
|
311
|
+
...(typeof readFiniteNumber(androidAdbOptions.durationSeconds) === 'number'
|
|
312
|
+
? { durationSeconds: readFiniteNumber(androidAdbOptions.durationSeconds) }
|
|
313
|
+
: {}),
|
|
314
|
+
...(step.driverAction === 'readLogs' ? { lines: readPositiveInteger(androidAdbOptions.logcatLines, 1000) } : {}),
|
|
315
|
+
...(typeof rawFileName === 'string' ? { rawFileName } : {}),
|
|
316
|
+
required: step.required,
|
|
317
|
+
...(isAndroidSelector(step.selector) ? { selector: step.selector } : {}),
|
|
318
|
+
stepId: step.id,
|
|
319
|
+
...(typeof readFiniteNumber(androidAdbOptions.durationMs) === 'number'
|
|
320
|
+
? { durationMs: readFiniteNumber(androidAdbOptions.durationMs) }
|
|
321
|
+
: {}),
|
|
322
|
+
...(typeof readFiniteNumber(androidAdbOptions.endX) === 'number' ? { endX: readFiniteNumber(androidAdbOptions.endX) } : {}),
|
|
323
|
+
...(typeof readFiniteNumber(androidAdbOptions.endY) === 'number' ? { endY: readFiniteNumber(androidAdbOptions.endY) } : {}),
|
|
324
|
+
...(typeof readFiniteNumber(androidAdbOptions.startX) === 'number' ? { startX: readFiniteNumber(androidAdbOptions.startX) } : {}),
|
|
325
|
+
...(typeof readFiniteNumber(androidAdbOptions.startY) === 'number' ? { startY: readFiniteNumber(androidAdbOptions.startY) } : {}),
|
|
326
|
+
...(typeof androidAdbOptions.remotePath === 'string' && androidAdbOptions.remotePath.length > 0
|
|
327
|
+
? { remotePath: androidAdbOptions.remotePath }
|
|
328
|
+
: {}),
|
|
329
|
+
waitMs: readStepWaitMs(step),
|
|
330
|
+
...(typeof readFiniteNumber(androidAdbOptions.x) === 'number' ? { x: readFiniteNumber(androidAdbOptions.x) } : {}),
|
|
331
|
+
...(typeof readFiniteNumber(androidAdbOptions.y) === 'number' ? { y: readFiniteNumber(androidAdbOptions.y) } : {}),
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Reads the first video capture produced by adb driver actions.
|
|
337
|
+
*
|
|
338
|
+
* @param {Record<string, unknown>} metadata
|
|
339
|
+
* @returns {string | null}
|
|
340
|
+
*/
|
|
341
|
+
function readAndroidAdbVideoCapturePath(metadata) {
|
|
342
|
+
const actions = Array.isArray(metadata.driverActions) ? metadata.driverActions : [];
|
|
343
|
+
const recordAction = actions.find((action) => action.driverAction === 'record' &&
|
|
344
|
+
action.exitCode === 0 &&
|
|
345
|
+
typeof action.capturePath === 'string');
|
|
346
|
+
return typeof recordAction?.capturePath === 'string' ? recordAction.capturePath : null;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Returns profile-time validation errors for adb driver steps.
|
|
350
|
+
*
|
|
351
|
+
* @param {import('./android-adb').AndroidAdbDriverStep[]} driverSteps
|
|
352
|
+
* @returns {string[]}
|
|
353
|
+
*/
|
|
354
|
+
function validateAndroidAdbDriverSteps(driverSteps) {
|
|
355
|
+
const errors = [];
|
|
356
|
+
for (const step of driverSteps) {
|
|
357
|
+
const stepLabel = step.stepId ? `step \`${step.stepId}\`` : 'unnamed step';
|
|
358
|
+
if (step.driverAction === 'tap' && !step.selector && (typeof step.x !== 'number' || typeof step.y !== 'number')) {
|
|
359
|
+
errors.push(`${stepLabel} uses driverAction \`tap\` but is missing adapterOptions.androidAdb.x/y.`);
|
|
360
|
+
}
|
|
361
|
+
if (step.driverAction === 'assertVisible' && !step.selector) {
|
|
362
|
+
errors.push(`${stepLabel} uses driverAction \`assertVisible\` but is missing a portable selector.`);
|
|
363
|
+
}
|
|
364
|
+
if (step.driverAction === 'scroll' &&
|
|
365
|
+
!step.selector &&
|
|
366
|
+
(typeof step.startX !== 'number' ||
|
|
367
|
+
typeof step.startY !== 'number' ||
|
|
368
|
+
typeof step.endX !== 'number' ||
|
|
369
|
+
typeof step.endY !== 'number')) {
|
|
370
|
+
errors.push(`${stepLabel} uses driverAction \`scroll\` but is missing adapterOptions.androidAdb.startX/startY/endX/endY.`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return errors;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Expands scenario-declared Android commands for an adb capture profile session.
|
|
377
|
+
*
|
|
378
|
+
* @param {Record<string, unknown>} scenario
|
|
379
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
380
|
+
*/
|
|
381
|
+
function resolveAndroidAdbProfileCommands(scenario) {
|
|
382
|
+
const androidAdbOptions = scenario.adapterOptions?.androidAdb;
|
|
383
|
+
if (!androidAdbOptions || !Array.isArray(androidAdbOptions.commands)) {
|
|
384
|
+
return resolveExecutionPlanProfileCommands(scenario);
|
|
385
|
+
}
|
|
386
|
+
const repeat = readPositiveInteger(androidAdbOptions.repeat, readPositiveInteger(scenario.defaultIterations, 1));
|
|
387
|
+
const commands = [];
|
|
388
|
+
for (let iteration = 0; iteration < repeat; iteration += 1) {
|
|
389
|
+
for (const command of androidAdbOptions.commands) {
|
|
390
|
+
if (!command || typeof command.command !== 'string') {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
commands.push({
|
|
394
|
+
command: command.command,
|
|
395
|
+
...(typeof command.label === 'string' ? { label: command.label } : {}),
|
|
396
|
+
waitMs: readPositiveInteger(command.waitMs, 0),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return commands;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Summarizes failed adb capture checks for CLI errors.
|
|
404
|
+
*
|
|
405
|
+
* @param {Record<string, unknown>} health
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
function summarizeFailedAndroidChecks(health) {
|
|
409
|
+
const checks = Array.isArray(health.checks) ? health.checks : [];
|
|
410
|
+
const failedChecks = checks
|
|
411
|
+
.filter((check) => check?.status === 'failed')
|
|
412
|
+
.map((check) => (typeof check.message === 'string'
|
|
413
|
+
? check.message
|
|
414
|
+
: typeof check.code === 'string'
|
|
415
|
+
? check.code
|
|
416
|
+
: 'unknown failure'));
|
|
417
|
+
return failedChecks.length > 0 ? ` Failed checks: ${failedChecks.join(' ')}` : '';
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Summarizes failed agent-device checks for CLI errors.
|
|
421
|
+
*
|
|
422
|
+
* @param {Record<string, unknown>} health
|
|
423
|
+
* @returns {string}
|
|
424
|
+
*/
|
|
425
|
+
function summarizeFailedAgentDeviceChecks(health) {
|
|
426
|
+
const checks = Array.isArray(health.checks) ? health.checks : [];
|
|
427
|
+
const failedChecks = checks
|
|
428
|
+
.filter((check) => check?.status === 'failed')
|
|
429
|
+
.map((check) => (typeof check.message === 'string'
|
|
430
|
+
? check.message
|
|
431
|
+
: typeof check.code === 'string'
|
|
432
|
+
? check.code
|
|
433
|
+
: 'unknown failure'));
|
|
434
|
+
return failedChecks.length > 0 ? ` Failed checks: ${failedChecks.join(' ')}` : '';
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Appends screenshots from an agent-device capture as profile capture inputs.
|
|
438
|
+
*
|
|
439
|
+
* @param {{args: import('./profile-mobile').CliArgs, capture: import('./agent-device').AgentDeviceCaptureResult}} options
|
|
440
|
+
* @returns {import('./profile-mobile').CliArgs}
|
|
441
|
+
*/
|
|
442
|
+
function appendAgentDeviceCaptureArgs({ args, capture, }) {
|
|
443
|
+
let captureArg = args.capture;
|
|
444
|
+
for (const screenshot of capture.captures.screenshots) {
|
|
445
|
+
captureArg = appendCaptureArg({
|
|
446
|
+
args: captureArg === undefined ? {} : { capture: captureArg },
|
|
447
|
+
value: `screenshot:${path.join(capture.runDir, screenshot)}`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return captureArg === undefined ? args : { ...args, capture: captureArg };
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Runs the Android profile artifact pipeline.
|
|
454
|
+
*
|
|
455
|
+
* @param {import('./profile-mobile').CliArgs} args
|
|
456
|
+
* @param {AndroidProfileOptions} [options]
|
|
457
|
+
* @returns {Promise<import('./profile-mobile').ProfileRunResult>}
|
|
458
|
+
*/
|
|
459
|
+
async function runProfileAndroid(args, options = {}) {
|
|
460
|
+
if (!isEnabled(args['adb-capture']) && !isEnabled(args['agent-device-capture'])) {
|
|
461
|
+
return runProfileMobile(args, {
|
|
462
|
+
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
463
|
+
defaultDriver: 'adb-logcat',
|
|
464
|
+
...(typeof args['adb-artifacts'] === 'string' ? { interactionDriver: 'adb-logcat' } : {}),
|
|
465
|
+
platform: 'android',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (typeof args.config !== 'string' || typeof args.scenario !== 'string') {
|
|
469
|
+
throw new Error('Both --config and --scenario are required.');
|
|
470
|
+
}
|
|
471
|
+
const config = readJson(path.resolve(args.config));
|
|
472
|
+
const scenario = readJson(path.resolve(args.scenario));
|
|
473
|
+
const runId = typeof args['run-id'] === 'string' ? args['run-id'] : createRunId();
|
|
474
|
+
const adbCaptureEnabled = isEnabled(args['adb-capture']);
|
|
475
|
+
const agentDeviceCaptureEnabled = isEnabled(args['agent-device-capture']);
|
|
476
|
+
const profileSessionEnabled = isEnabled(args['profile-session']);
|
|
477
|
+
const profileSessionStorageEnabled = isEnabled(args['android-profile-session-storage']);
|
|
478
|
+
const profileSessionStorageKey = readStringArgOrEnv(args['android-profile-session-storage-key'], [
|
|
479
|
+
'ASL_ANDROID_PROFILE_SESSION_STORAGE_KEY',
|
|
480
|
+
'ASL_EXAMPLE_ANDROID_PROFILE_SESSION_STORAGE_KEY',
|
|
481
|
+
]) ?? DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY;
|
|
482
|
+
const profileCommandStorageKey = readStringArgOrEnv(args['android-profile-command-storage-key'], [
|
|
483
|
+
'ASL_ANDROID_PROFILE_COMMAND_STORAGE_KEY',
|
|
484
|
+
'ASL_EXAMPLE_ANDROID_PROFILE_COMMAND_STORAGE_KEY',
|
|
485
|
+
]) ?? DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY;
|
|
486
|
+
const androidDevClientUrl = readStringArgOrEnv(args['android-dev-client-url'], [
|
|
487
|
+
'ASL_ANDROID_DEV_CLIENT_URL',
|
|
488
|
+
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_URL',
|
|
489
|
+
]);
|
|
490
|
+
const androidDevClientWaitMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-wait-ms'], [
|
|
491
|
+
'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
492
|
+
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
493
|
+
]), 1000);
|
|
494
|
+
const androidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
|
|
495
|
+
'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
496
|
+
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
497
|
+
]);
|
|
498
|
+
const androidDevClientReadyQuietMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
|
|
499
|
+
'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
500
|
+
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
501
|
+
]), 0);
|
|
502
|
+
const androidDevClientReadyTimeoutMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-timeout-ms'], [
|
|
503
|
+
'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
504
|
+
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
505
|
+
]), 60000);
|
|
506
|
+
const scenarioName = typeof scenario.name === 'string' ? scenario.name : path.basename(args.scenario, '.json');
|
|
507
|
+
const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
|
|
508
|
+
if (adbCaptureEnabled) {
|
|
509
|
+
const driverStepErrors = validateAndroidAdbDriverSteps(driverSteps);
|
|
510
|
+
if (driverStepErrors.length > 0) {
|
|
511
|
+
throw new Error(`Invalid Android adb driver step metadata: ${driverStepErrors.join(' ')}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const profileSessionCommands = profileSessionEnabled ? resolveAndroidAdbProfileCommands(scenario) : [];
|
|
515
|
+
const commandWaitMs = parsePositiveInteger(readScalarArg(args['command-wait-ms']), 250);
|
|
516
|
+
const profileSessionDeepLinks = profileSessionEnabled && !profileSessionStorageEnabled
|
|
517
|
+
? [
|
|
518
|
+
{
|
|
519
|
+
label: 'profile-session-start',
|
|
520
|
+
url: buildProfileSessionUrl({
|
|
521
|
+
action: 'start',
|
|
522
|
+
config,
|
|
523
|
+
runId,
|
|
524
|
+
scenario: scenarioName,
|
|
525
|
+
}),
|
|
526
|
+
waitMs: commandWaitMs,
|
|
527
|
+
},
|
|
528
|
+
...profileSessionCommands.map((profileCommand, index) => ({
|
|
529
|
+
label: profileCommand.label ?? `profile-command-${index + 1}`,
|
|
530
|
+
url: buildProfileSessionUrl({
|
|
531
|
+
action: 'command',
|
|
532
|
+
command: profileCommand.command,
|
|
533
|
+
config,
|
|
534
|
+
runId,
|
|
535
|
+
scenario: scenarioName,
|
|
536
|
+
}),
|
|
537
|
+
waitMs: profileCommand.waitMs,
|
|
538
|
+
})),
|
|
539
|
+
]
|
|
540
|
+
: [];
|
|
541
|
+
const profileSessionStorageWrites = profileSessionEnabled && profileSessionStorageEnabled
|
|
542
|
+
? buildProfileSessionStorageWrites({
|
|
543
|
+
commandStorageKey: profileCommandStorageKey,
|
|
544
|
+
commandWaitMs,
|
|
545
|
+
commands: profileSessionCommands,
|
|
546
|
+
runId,
|
|
547
|
+
scenario: scenarioName,
|
|
548
|
+
sessionStorageKey: profileSessionStorageKey,
|
|
549
|
+
})
|
|
550
|
+
: [];
|
|
551
|
+
const startupDeepLinks = androidDevClientUrl
|
|
552
|
+
? [
|
|
553
|
+
{
|
|
554
|
+
label: 'android-dev-client-url',
|
|
555
|
+
...(androidDevClientReadyPattern ? { readyLogPattern: androidDevClientReadyPattern } : {}),
|
|
556
|
+
readyLogQuietMs: androidDevClientReadyQuietMs,
|
|
557
|
+
readyLogTimeoutMs: androidDevClientReadyTimeoutMs,
|
|
558
|
+
url: androidDevClientUrl,
|
|
559
|
+
waitMs: androidDevClientWaitMs,
|
|
560
|
+
},
|
|
561
|
+
]
|
|
562
|
+
: [];
|
|
563
|
+
const adbCapture = adbCaptureEnabled
|
|
564
|
+
? await runAndroidAdbPreflight({
|
|
565
|
+
...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
|
|
566
|
+
captureLogcat: true,
|
|
567
|
+
clearLogcat: isEnabled(args['clear-logcat']),
|
|
568
|
+
deepLinks: profileSessionDeepLinks,
|
|
569
|
+
...(options.delay ? { delay: options.delay } : {}),
|
|
570
|
+
...(options.executor ? { executor: options.executor } : {}),
|
|
571
|
+
driverSteps,
|
|
572
|
+
launch: isEnabled(args.launch),
|
|
573
|
+
launchWaitMs: parsePositiveInteger(readScalarArg(args['launch-wait-ms']), 0),
|
|
574
|
+
logcatLines: parsePositiveInteger(readScalarArg(args['logcat-lines']), 1000),
|
|
575
|
+
outputDir: resolveAdbCaptureOutputDir({ args, runId }),
|
|
576
|
+
packageName: resolveAndroidPackageName({ args, config }),
|
|
577
|
+
...(typeof args['react-native-debug-host'] === 'string'
|
|
578
|
+
? { reactNativeDebugHost: args['react-native-debug-host'] }
|
|
579
|
+
: {}),
|
|
580
|
+
runId,
|
|
581
|
+
...(typeof args.serial === 'string' ? { serial: args.serial } : {}),
|
|
582
|
+
startupDeepLinks,
|
|
583
|
+
storageWrites: profileSessionStorageWrites,
|
|
584
|
+
waitMs: resolveProfileSessionCaptureWaitMs({
|
|
585
|
+
args,
|
|
586
|
+
profileSessionEnabled,
|
|
587
|
+
scenario,
|
|
588
|
+
}),
|
|
589
|
+
})
|
|
590
|
+
: null;
|
|
591
|
+
if (adbCapture && adbCapture.health.healthStatus !== 'passed') {
|
|
592
|
+
throw new Error(`Android adb capture failed; inspect ${adbCapture.runDir}/agent-summary.md.${summarizeFailedAndroidChecks(adbCapture.health)}`);
|
|
593
|
+
}
|
|
594
|
+
const agentDeviceCapture = agentDeviceCaptureEnabled
|
|
595
|
+
? await runAgentDeviceCapture({
|
|
596
|
+
...(typeof args['agent-device'] === 'string' ? { agentDevicePath: args['agent-device'] } : {}),
|
|
597
|
+
app: typeof args['agent-device-app'] === 'string'
|
|
598
|
+
? args['agent-device-app']
|
|
599
|
+
: resolveAndroidPackageName({ args, config }),
|
|
600
|
+
...(options.agentDeviceExecutor ? { executor: options.agentDeviceExecutor } : {}),
|
|
601
|
+
...(typeof args['agent-device-device'] === 'string' ? { device: args['agent-device-device'] } : {}),
|
|
602
|
+
...(typeof args['agent-device-session'] === 'string' ? { session: args['agent-device-session'] } : {}),
|
|
603
|
+
...(typeof args['agent-device-session-mode'] === 'string'
|
|
604
|
+
? { sessionMode: args['agent-device-session-mode'] }
|
|
605
|
+
: {}),
|
|
606
|
+
...(typeof args.serial === 'string' ? { serial: args.serial } : {}),
|
|
607
|
+
open: isEnabled(args['agent-device-open']),
|
|
608
|
+
outputDir: resolveAgentDeviceCaptureOutputDir({ args, runId }),
|
|
609
|
+
platform: 'android',
|
|
610
|
+
runId,
|
|
611
|
+
scenario,
|
|
612
|
+
waitMs: parsePositiveInteger(readScalarArg(args['agent-device-wait-ms']), 0),
|
|
613
|
+
})
|
|
614
|
+
: null;
|
|
615
|
+
if (agentDeviceCapture && agentDeviceCapture.health.healthStatus !== 'passed') {
|
|
616
|
+
throw new Error(`agent-device capture failed; inspect ${agentDeviceCapture.runDir}/agent-summary.md.${summarizeFailedAgentDeviceChecks(agentDeviceCapture.health)}`);
|
|
617
|
+
}
|
|
618
|
+
const videoCapturePath = adbCapture ? readAndroidAdbVideoCapturePath(adbCapture.metadata) : null;
|
|
619
|
+
const baseProfileArgs = {
|
|
620
|
+
...args,
|
|
621
|
+
...(adbCapture ? { 'adb-artifacts': adbCapture.runDir } : {}),
|
|
622
|
+
...(videoCapturePath
|
|
623
|
+
? {
|
|
624
|
+
capture: appendCaptureArg({
|
|
625
|
+
args,
|
|
626
|
+
value: `video:${path.join(adbCapture?.runDir ?? '', videoCapturePath)}`,
|
|
627
|
+
}),
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
630
|
+
'run-id': runId,
|
|
631
|
+
};
|
|
632
|
+
if (adbCapture) {
|
|
633
|
+
delete baseProfileArgs.events;
|
|
634
|
+
}
|
|
635
|
+
const profileArgs = agentDeviceCapture
|
|
636
|
+
? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
|
|
637
|
+
: baseProfileArgs;
|
|
638
|
+
return runProfileMobile(profileArgs, {
|
|
639
|
+
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
640
|
+
defaultDriver: 'adb-logcat',
|
|
641
|
+
interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
|
|
642
|
+
platform: 'android',
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Runs the profile-android CLI.
|
|
647
|
+
*
|
|
648
|
+
* @returns {Promise<void>}
|
|
649
|
+
*/
|
|
650
|
+
async function main() {
|
|
651
|
+
const argv = process.argv.slice(2);
|
|
652
|
+
if (hasHelpFlag(argv)) {
|
|
653
|
+
usage({ binaryName: 'asl-profile-android', output: process.stdout, platform: 'android' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const args = parseArgs(argv);
|
|
657
|
+
loadAslLocalEnv();
|
|
658
|
+
if (typeof args.config !== 'string' || typeof args.scenario !== 'string') {
|
|
659
|
+
usage({ binaryName: 'asl-profile-android', platform: 'android' });
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const result = await runProfileAndroid(args);
|
|
664
|
+
process.stdout.write(`${result.runDir}\n`);
|
|
665
|
+
}
|
|
666
|
+
if (require.main === module) {
|
|
667
|
+
main().catch((error) => {
|
|
668
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
669
|
+
process.exitCode = 1;
|
|
670
|
+
});
|
|
671
|
+
}
|