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,175 @@
|
|
|
1
|
+
type AndroidAdbCommandResult = {
|
|
2
|
+
action: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
capturePath?: string;
|
|
5
|
+
command: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
rawFileName: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
stdout: string;
|
|
10
|
+
};
|
|
11
|
+
type AndroidAdbDriver = {
|
|
12
|
+
assertVisible: (options: AndroidAdbAssertVisibleOptions) => Promise<AndroidAdbCommandResult>;
|
|
13
|
+
clearLogs: () => Promise<AndroidAdbCommandResult>;
|
|
14
|
+
inspectTree: (options?: AndroidAdbInspectTreeOptions) => Promise<AndroidAdbCommandResult>;
|
|
15
|
+
launchPackage: (packageName: string) => Promise<AndroidAdbCommandResult>;
|
|
16
|
+
openDeepLink: (options: AndroidAdbDeepLinkOptions) => Promise<AndroidAdbCommandResult>;
|
|
17
|
+
readLogs: (options?: AndroidAdbReadLogsOptions) => Promise<AndroidAdbCommandResult>;
|
|
18
|
+
record: (options: AndroidAdbRecordOptions) => Promise<AndroidAdbCommandResult>;
|
|
19
|
+
screenshot: (options?: AndroidAdbScreenshotOptions) => Promise<AndroidAdbCommandResult>;
|
|
20
|
+
scroll: (options: AndroidAdbScrollOptions) => Promise<AndroidAdbCommandResult>;
|
|
21
|
+
tap: (options: AndroidAdbTapOptions) => Promise<AndroidAdbCommandResult>;
|
|
22
|
+
};
|
|
23
|
+
type AndroidAdbBounds = {
|
|
24
|
+
bottom: number;
|
|
25
|
+
left: number;
|
|
26
|
+
right: number;
|
|
27
|
+
top: number;
|
|
28
|
+
};
|
|
29
|
+
type AndroidAdbDriverOptions = {
|
|
30
|
+
adbPath: string;
|
|
31
|
+
deviceSerial: string;
|
|
32
|
+
executor: AndroidAdbCommandExecutor;
|
|
33
|
+
};
|
|
34
|
+
type AndroidAdbCommandExecutor = (command: string, args: string[]) => Promise<{
|
|
35
|
+
args: string[];
|
|
36
|
+
command: string;
|
|
37
|
+
exitCode: number;
|
|
38
|
+
stderr: string;
|
|
39
|
+
stdout: string;
|
|
40
|
+
}>;
|
|
41
|
+
type AndroidAdbDeepLinkOptions = {
|
|
42
|
+
packageName?: string | null;
|
|
43
|
+
rawFileName?: string;
|
|
44
|
+
url: string;
|
|
45
|
+
};
|
|
46
|
+
type AndroidAdbReadLogsOptions = {
|
|
47
|
+
lines?: number;
|
|
48
|
+
rawFileName?: string;
|
|
49
|
+
};
|
|
50
|
+
type AndroidAdbRecordOptions = {
|
|
51
|
+
durationSeconds?: number;
|
|
52
|
+
outputPath: string;
|
|
53
|
+
rawFileName?: string;
|
|
54
|
+
remotePath?: string;
|
|
55
|
+
};
|
|
56
|
+
type AndroidAdbInspectTreeOptions = {
|
|
57
|
+
rawFileName?: string;
|
|
58
|
+
};
|
|
59
|
+
type AndroidAdbAssertVisibleOptions = {
|
|
60
|
+
rawFileName?: string;
|
|
61
|
+
selector: AndroidSelector;
|
|
62
|
+
};
|
|
63
|
+
type AndroidAdbScreenshotOptions = {
|
|
64
|
+
rawFileName?: string;
|
|
65
|
+
};
|
|
66
|
+
type AndroidAdbScrollOptions = {
|
|
67
|
+
durationMs?: number;
|
|
68
|
+
endX: number;
|
|
69
|
+
endY: number;
|
|
70
|
+
rawFileName?: string;
|
|
71
|
+
startX: number;
|
|
72
|
+
startY: number;
|
|
73
|
+
};
|
|
74
|
+
type AndroidAdbTapOptions = {
|
|
75
|
+
rawFileName?: string;
|
|
76
|
+
x: number;
|
|
77
|
+
y: number;
|
|
78
|
+
};
|
|
79
|
+
type AndroidSelector = {
|
|
80
|
+
kind: string;
|
|
81
|
+
match?: string;
|
|
82
|
+
value: string;
|
|
83
|
+
};
|
|
84
|
+
type AndroidUiNode = {
|
|
85
|
+
attributes: Record<string, string>;
|
|
86
|
+
bounds: AndroidAdbBounds;
|
|
87
|
+
};
|
|
88
|
+
type AndroidSelectorResolution = {
|
|
89
|
+
bounds: AndroidAdbBounds;
|
|
90
|
+
centerX: number;
|
|
91
|
+
centerY: number;
|
|
92
|
+
node: AndroidUiNode;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Quotes one argument for the Android device shell.
|
|
96
|
+
*
|
|
97
|
+
* `adb shell` still lets the device shell interpret metacharacters in later
|
|
98
|
+
* tokens, so deep-link URLs with `&` must be quoted before execution.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} value
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
declare function quoteAndroidShellArg(value: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Combines stdout and stderr into the raw evidence text written by callers.
|
|
106
|
+
*
|
|
107
|
+
* @param {{stdout: string, stderr: string}} result
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
declare function formatAndroidAdbRawOutput(result: {
|
|
111
|
+
stdout: string;
|
|
112
|
+
stderr: string;
|
|
113
|
+
}): string;
|
|
114
|
+
/**
|
|
115
|
+
* Joins command output from a multi-command adb driver action.
|
|
116
|
+
*
|
|
117
|
+
* @param {Array<{args: string[], exitCode: number, stderr: string, stdout: string}>} results
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
declare function formatAndroidAdbCommandTranscript(results: Array<{
|
|
121
|
+
args: string[];
|
|
122
|
+
exitCode: number;
|
|
123
|
+
stderr: string;
|
|
124
|
+
stdout: string;
|
|
125
|
+
}>): string;
|
|
126
|
+
/**
|
|
127
|
+
* Parses Android UIAutomator bounds such as `[0,100][300,240]`.
|
|
128
|
+
*
|
|
129
|
+
* @param {unknown} value
|
|
130
|
+
* @returns {AndroidAdbBounds | null}
|
|
131
|
+
*/
|
|
132
|
+
declare function parseAndroidAdbBounds(value: unknown): AndroidAdbBounds | null;
|
|
133
|
+
/**
|
|
134
|
+
* Extracts UIAutomator nodes that have usable bounds.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} xml
|
|
137
|
+
* @returns {AndroidUiNode[]}
|
|
138
|
+
*/
|
|
139
|
+
declare function parseAndroidUiAutomatorNodes(xml: string): AndroidUiNode[];
|
|
140
|
+
/**
|
|
141
|
+
* Resolves a portable selector against Android UIAutomator XML.
|
|
142
|
+
*
|
|
143
|
+
* @param {{selector: AndroidSelector, uiTreeXml: string}} options
|
|
144
|
+
* @returns {AndroidSelectorResolution | null}
|
|
145
|
+
*/
|
|
146
|
+
declare function resolveAndroidSelectorFromUiTree({ selector, uiTreeXml, }: {
|
|
147
|
+
selector: AndroidSelector;
|
|
148
|
+
uiTreeXml: string;
|
|
149
|
+
}): AndroidSelectorResolution | null;
|
|
150
|
+
/**
|
|
151
|
+
* Derives an in-bounds vertical scroll gesture from one resolved selector.
|
|
152
|
+
*
|
|
153
|
+
* @param {AndroidAdbBounds} bounds
|
|
154
|
+
* @returns {{endX: number, endY: number, startX: number, startY: number}}
|
|
155
|
+
*/
|
|
156
|
+
declare function buildAndroidScrollCoordinatesFromBounds(bounds: AndroidAdbBounds): {
|
|
157
|
+
endX: number;
|
|
158
|
+
endY: number;
|
|
159
|
+
startX: number;
|
|
160
|
+
startY: number;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Creates a small adb-backed Android driver for lifecycle helpers and log capture.
|
|
164
|
+
*
|
|
165
|
+
* The generic driver capability exposed here is `readLogs`. Launching packages,
|
|
166
|
+
* clearing logcat, and opening deep links are Android lifecycle helpers used by
|
|
167
|
+
* built-in runners; they are intentionally not advertised as portable driver
|
|
168
|
+
* actions.
|
|
169
|
+
*
|
|
170
|
+
* @param {AndroidAdbDriverOptions} options
|
|
171
|
+
* @returns {AndroidAdbDriver}
|
|
172
|
+
*/
|
|
173
|
+
declare function createAndroidAdbDriver({ adbPath, deviceSerial, executor, }: AndroidAdbDriverOptions): AndroidAdbDriver;
|
|
174
|
+
export { buildAndroidScrollCoordinatesFromBounds, createAndroidAdbDriver, formatAndroidAdbCommandTranscript, formatAndroidAdbRawOutput, parseAndroidAdbBounds, parseAndroidUiAutomatorNodes, quoteAndroidShellArg, resolveAndroidSelectorFromUiTree, };
|
|
175
|
+
export type { AndroidAdbBounds, AndroidAdbCommandExecutor, AndroidAdbCommandResult, AndroidAdbDeepLinkOptions, AndroidAdbDriver, AndroidAdbDriverOptions, AndroidAdbAssertVisibleOptions, AndroidAdbInspectTreeOptions, AndroidAdbReadLogsOptions, AndroidAdbRecordOptions, AndroidAdbScreenshotOptions, AndroidAdbScrollOptions, AndroidSelector, AndroidSelectorResolution, AndroidUiNode, AndroidAdbTapOptions, };
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAndroidScrollCoordinatesFromBounds = buildAndroidScrollCoordinatesFromBounds;
|
|
4
|
+
exports.createAndroidAdbDriver = createAndroidAdbDriver;
|
|
5
|
+
exports.formatAndroidAdbCommandTranscript = formatAndroidAdbCommandTranscript;
|
|
6
|
+
exports.formatAndroidAdbRawOutput = formatAndroidAdbRawOutput;
|
|
7
|
+
exports.parseAndroidAdbBounds = parseAndroidAdbBounds;
|
|
8
|
+
exports.parseAndroidUiAutomatorNodes = parseAndroidUiAutomatorNodes;
|
|
9
|
+
exports.quoteAndroidShellArg = quoteAndroidShellArg;
|
|
10
|
+
exports.resolveAndroidSelectorFromUiTree = resolveAndroidSelectorFromUiTree;
|
|
11
|
+
const fsp = require('node:fs/promises');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const UI_AUTOMATOR_DUMP_PATH = '/sdcard/agent-scenario-loop-ui.xml';
|
|
14
|
+
/**
|
|
15
|
+
* Quotes one argument for the Android device shell.
|
|
16
|
+
*
|
|
17
|
+
* `adb shell` still lets the device shell interpret metacharacters in later
|
|
18
|
+
* tokens, so deep-link URLs with `&` must be quoted before execution.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} value
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function quoteAndroidShellArg(value) {
|
|
24
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds a shell command that returns UIAutomator XML on stdout.
|
|
28
|
+
*
|
|
29
|
+
* Some emulator images do not stream XML for `uiautomator dump /dev/tty`;
|
|
30
|
+
* dumping to a remote file and then reading it gives the selector resolver a
|
|
31
|
+
* stable XML payload.
|
|
32
|
+
*
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function buildUiAutomatorDumpCommand() {
|
|
36
|
+
return [
|
|
37
|
+
`rm -f ${UI_AUTOMATOR_DUMP_PATH}`,
|
|
38
|
+
`uiautomator dump ${UI_AUTOMATOR_DUMP_PATH} >/dev/null`,
|
|
39
|
+
`cat ${UI_AUTOMATOR_DUMP_PATH}`,
|
|
40
|
+
'status=$?',
|
|
41
|
+
`rm -f ${UI_AUTOMATOR_DUMP_PATH}`,
|
|
42
|
+
'exit $status',
|
|
43
|
+
].join('; ');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Adds stable driver metadata to one adb command result.
|
|
47
|
+
*
|
|
48
|
+
* @param {{action: string, rawFileName: string, result: Awaited<ReturnType<AndroidAdbCommandExecutor>>}} options
|
|
49
|
+
* @returns {AndroidAdbCommandResult}
|
|
50
|
+
*/
|
|
51
|
+
function buildDriverResult({ action, rawFileName, result, }) {
|
|
52
|
+
return {
|
|
53
|
+
action,
|
|
54
|
+
args: result.args,
|
|
55
|
+
command: result.command,
|
|
56
|
+
exitCode: result.exitCode,
|
|
57
|
+
rawFileName,
|
|
58
|
+
stderr: result.stderr,
|
|
59
|
+
stdout: result.stdout,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Combines stdout and stderr into the raw evidence text written by callers.
|
|
64
|
+
*
|
|
65
|
+
* @param {{stdout: string, stderr: string}} result
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function formatAndroidAdbRawOutput(result) {
|
|
69
|
+
return [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Joins command output from a multi-command adb driver action.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array<{args: string[], exitCode: number, stderr: string, stdout: string}>} results
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function formatAndroidAdbCommandTranscript(results) {
|
|
78
|
+
return results
|
|
79
|
+
.map((result) => [
|
|
80
|
+
`$ adb ${result.args.join(' ')}`,
|
|
81
|
+
`exitCode=${result.exitCode}`,
|
|
82
|
+
result.stdout,
|
|
83
|
+
result.stderr,
|
|
84
|
+
].filter(Boolean).join('\n'))
|
|
85
|
+
.join('\n\n');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Decodes XML attribute entities emitted by `uiautomator dump`.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} value
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function decodeXmlAttribute(value) {
|
|
94
|
+
return value
|
|
95
|
+
.replace(/"/gu, '"')
|
|
96
|
+
.replace(/'/gu, "'")
|
|
97
|
+
.replace(/</gu, '<')
|
|
98
|
+
.replace(/>/gu, '>')
|
|
99
|
+
.replace(/&/gu, '&');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parses Android UIAutomator bounds such as `[0,100][300,240]`.
|
|
103
|
+
*
|
|
104
|
+
* @param {unknown} value
|
|
105
|
+
* @returns {AndroidAdbBounds | null}
|
|
106
|
+
*/
|
|
107
|
+
function parseAndroidAdbBounds(value) {
|
|
108
|
+
if (typeof value !== 'string') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const match = /^\[(?<left>-?\d+),(?<top>-?\d+)\]\[(?<right>-?\d+),(?<bottom>-?\d+)\]$/u.exec(value);
|
|
112
|
+
if (!match?.groups) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const bounds = {
|
|
116
|
+
bottom: Number(match.groups.bottom),
|
|
117
|
+
left: Number(match.groups.left),
|
|
118
|
+
right: Number(match.groups.right),
|
|
119
|
+
top: Number(match.groups.top),
|
|
120
|
+
};
|
|
121
|
+
if (!Number.isFinite(bounds.left) ||
|
|
122
|
+
!Number.isFinite(bounds.top) ||
|
|
123
|
+
!Number.isFinite(bounds.right) ||
|
|
124
|
+
!Number.isFinite(bounds.bottom) ||
|
|
125
|
+
bounds.right <= bounds.left ||
|
|
126
|
+
bounds.bottom <= bounds.top) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return bounds;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extracts UIAutomator nodes that have usable bounds.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} xml
|
|
135
|
+
* @returns {AndroidUiNode[]}
|
|
136
|
+
*/
|
|
137
|
+
function parseAndroidUiAutomatorNodes(xml) {
|
|
138
|
+
const nodes = [];
|
|
139
|
+
for (const nodeMatch of String(xml).matchAll(/<node\b(?<attributes>[^>]*)\/?>/gu)) {
|
|
140
|
+
const attributesText = nodeMatch.groups?.attributes ?? '';
|
|
141
|
+
const attributes = {};
|
|
142
|
+
for (const attributeMatch of attributesText.matchAll(/\s(?<name>[\w:-]+)="(?<value>[^"]*)"/gu)) {
|
|
143
|
+
if (attributeMatch.groups?.name && attributeMatch.groups.value !== undefined) {
|
|
144
|
+
attributes[attributeMatch.groups.name] = decodeXmlAttribute(attributeMatch.groups.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const bounds = parseAndroidAdbBounds(attributes.bounds);
|
|
148
|
+
if (bounds) {
|
|
149
|
+
nodes.push({ attributes, bounds });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return nodes;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns true when a UI attribute satisfies a portable selector match.
|
|
156
|
+
*
|
|
157
|
+
* @param {{actual: string | undefined, expected: string, match: string | undefined}} options
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
function matchesSelectorValue({ actual = '', expected, match = 'exact', }) {
|
|
161
|
+
if (match === 'contains') {
|
|
162
|
+
return actual.includes(expected);
|
|
163
|
+
}
|
|
164
|
+
if (match === 'regex') {
|
|
165
|
+
try {
|
|
166
|
+
return new RegExp(expected, 'u').test(actual);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return actual === expected;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolves a portable selector against Android UIAutomator XML.
|
|
176
|
+
*
|
|
177
|
+
* @param {{selector: AndroidSelector, uiTreeXml: string}} options
|
|
178
|
+
* @returns {AndroidSelectorResolution | null}
|
|
179
|
+
*/
|
|
180
|
+
function resolveAndroidSelectorFromUiTree({ selector, uiTreeXml, }) {
|
|
181
|
+
const nodes = parseAndroidUiAutomatorNodes(uiTreeXml);
|
|
182
|
+
const node = nodes.find((candidate) => {
|
|
183
|
+
if (selector.kind === 'resourceId') {
|
|
184
|
+
return matchesSelectorValue({
|
|
185
|
+
actual: candidate.attributes['resource-id'],
|
|
186
|
+
expected: selector.value,
|
|
187
|
+
match: selector.match,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (selector.kind === 'testId') {
|
|
191
|
+
const resourceId = candidate.attributes['resource-id'] ?? '';
|
|
192
|
+
return matchesSelectorValue({
|
|
193
|
+
actual: resourceId,
|
|
194
|
+
expected: selector.value,
|
|
195
|
+
match: selector.match,
|
|
196
|
+
}) || resourceId.endsWith(`:id/${selector.value}`);
|
|
197
|
+
}
|
|
198
|
+
if (selector.kind === 'accessibilityId' || selector.kind === 'accessibilityLabel') {
|
|
199
|
+
return matchesSelectorValue({
|
|
200
|
+
actual: candidate.attributes['content-desc'],
|
|
201
|
+
expected: selector.value,
|
|
202
|
+
match: selector.match,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (selector.kind === 'text') {
|
|
206
|
+
return matchesSelectorValue({
|
|
207
|
+
actual: candidate.attributes.text,
|
|
208
|
+
expected: selector.value,
|
|
209
|
+
match: selector.match,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
});
|
|
214
|
+
if (!node) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
bounds: node.bounds,
|
|
219
|
+
centerX: Math.round((node.bounds.left + node.bounds.right) / 2),
|
|
220
|
+
centerY: Math.round((node.bounds.top + node.bounds.bottom) / 2),
|
|
221
|
+
node,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Derives an in-bounds vertical scroll gesture from one resolved selector.
|
|
226
|
+
*
|
|
227
|
+
* @param {AndroidAdbBounds} bounds
|
|
228
|
+
* @returns {{endX: number, endY: number, startX: number, startY: number}}
|
|
229
|
+
*/
|
|
230
|
+
function buildAndroidScrollCoordinatesFromBounds(bounds) {
|
|
231
|
+
const width = bounds.right - bounds.left;
|
|
232
|
+
const height = bounds.bottom - bounds.top;
|
|
233
|
+
const x = Math.round(bounds.left + width / 2);
|
|
234
|
+
return {
|
|
235
|
+
endX: x,
|
|
236
|
+
endY: Math.round(bounds.top + height * 0.2),
|
|
237
|
+
startX: x,
|
|
238
|
+
startY: Math.round(bounds.top + height * 0.8),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Creates a small adb-backed Android driver for lifecycle helpers and log capture.
|
|
243
|
+
*
|
|
244
|
+
* The generic driver capability exposed here is `readLogs`. Launching packages,
|
|
245
|
+
* clearing logcat, and opening deep links are Android lifecycle helpers used by
|
|
246
|
+
* built-in runners; they are intentionally not advertised as portable driver
|
|
247
|
+
* actions.
|
|
248
|
+
*
|
|
249
|
+
* @param {AndroidAdbDriverOptions} options
|
|
250
|
+
* @returns {AndroidAdbDriver}
|
|
251
|
+
*/
|
|
252
|
+
function createAndroidAdbDriver({ adbPath, deviceSerial, executor, }) {
|
|
253
|
+
return {
|
|
254
|
+
async clearLogs() {
|
|
255
|
+
const rawFileName = 'adb-logcat-clear.txt';
|
|
256
|
+
const result = await executor(adbPath, ['-s', deviceSerial, 'logcat', '-c']);
|
|
257
|
+
return buildDriverResult({ action: 'clearLogs', rawFileName, result });
|
|
258
|
+
},
|
|
259
|
+
async launchPackage(packageName) {
|
|
260
|
+
const rawFileName = 'adb-launch.txt';
|
|
261
|
+
const result = await executor(adbPath, [
|
|
262
|
+
'-s',
|
|
263
|
+
deviceSerial,
|
|
264
|
+
'shell',
|
|
265
|
+
'monkey',
|
|
266
|
+
'-p',
|
|
267
|
+
packageName,
|
|
268
|
+
'-c',
|
|
269
|
+
'android.intent.category.LAUNCHER',
|
|
270
|
+
'1',
|
|
271
|
+
]);
|
|
272
|
+
return buildDriverResult({ action: 'launchPackage', rawFileName, result });
|
|
273
|
+
},
|
|
274
|
+
async inspectTree({ rawFileName = 'adb-ui-tree.xml', } = {}) {
|
|
275
|
+
const result = await executor(adbPath, [
|
|
276
|
+
'-s',
|
|
277
|
+
deviceSerial,
|
|
278
|
+
'shell',
|
|
279
|
+
buildUiAutomatorDumpCommand(),
|
|
280
|
+
]);
|
|
281
|
+
return buildDriverResult({ action: 'inspectTree', rawFileName, result });
|
|
282
|
+
},
|
|
283
|
+
async assertVisible({ rawFileName = 'adb-assert-visible.xml', selector, }) {
|
|
284
|
+
const result = await executor(adbPath, [
|
|
285
|
+
'-s',
|
|
286
|
+
deviceSerial,
|
|
287
|
+
'shell',
|
|
288
|
+
buildUiAutomatorDumpCommand(),
|
|
289
|
+
]);
|
|
290
|
+
const resolution = result.exitCode === 0
|
|
291
|
+
? resolveAndroidSelectorFromUiTree({ selector, uiTreeXml: result.stdout })
|
|
292
|
+
: null;
|
|
293
|
+
return buildDriverResult({
|
|
294
|
+
action: 'assertVisible',
|
|
295
|
+
rawFileName,
|
|
296
|
+
result: {
|
|
297
|
+
...result,
|
|
298
|
+
exitCode: resolution ? 0 : result.exitCode === 0 ? 1 : result.exitCode,
|
|
299
|
+
stderr: resolution
|
|
300
|
+
? result.stderr
|
|
301
|
+
: [result.stderr, `Android selector ${selector.kind}=${selector.value} was not visible.`]
|
|
302
|
+
.filter(Boolean)
|
|
303
|
+
.join('\n'),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
async openDeepLink({ packageName = null, rawFileName = 'adb-deep-link.txt', url, }) {
|
|
308
|
+
const deepLinkCommand = [
|
|
309
|
+
'am',
|
|
310
|
+
'start',
|
|
311
|
+
'-a',
|
|
312
|
+
quoteAndroidShellArg('android.intent.action.VIEW'),
|
|
313
|
+
'-d',
|
|
314
|
+
quoteAndroidShellArg(url),
|
|
315
|
+
...(packageName ? ['-p', quoteAndroidShellArg(packageName)] : []),
|
|
316
|
+
].join(' ');
|
|
317
|
+
const result = await executor(adbPath, ['-s', deviceSerial, 'shell', deepLinkCommand]);
|
|
318
|
+
return buildDriverResult({ action: 'openDeepLink', rawFileName, result });
|
|
319
|
+
},
|
|
320
|
+
async readLogs({ lines = 1000, rawFileName = 'adb-logcat.txt', } = {}) {
|
|
321
|
+
const result = await executor(adbPath, [
|
|
322
|
+
'-s',
|
|
323
|
+
deviceSerial,
|
|
324
|
+
'logcat',
|
|
325
|
+
'-d',
|
|
326
|
+
'-v',
|
|
327
|
+
'time',
|
|
328
|
+
'-t',
|
|
329
|
+
String(lines),
|
|
330
|
+
]);
|
|
331
|
+
return buildDriverResult({ action: 'readLogs', rawFileName, result });
|
|
332
|
+
},
|
|
333
|
+
async record({ durationSeconds = 5, outputPath, rawFileName = 'adb-record.txt', remotePath = `/sdcard/agent-scenario-loop-${Date.now()}.mp4`, }) {
|
|
334
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
335
|
+
const recordResult = await executor(adbPath, [
|
|
336
|
+
'-s',
|
|
337
|
+
deviceSerial,
|
|
338
|
+
'shell',
|
|
339
|
+
'screenrecord',
|
|
340
|
+
'--time-limit',
|
|
341
|
+
String(durationSeconds),
|
|
342
|
+
remotePath,
|
|
343
|
+
]);
|
|
344
|
+
const pullResult = recordResult.exitCode === 0
|
|
345
|
+
? await executor(adbPath, ['-s', deviceSerial, 'pull', remotePath, outputPath])
|
|
346
|
+
: null;
|
|
347
|
+
const cleanupResult = await executor(adbPath, ['-s', deviceSerial, 'shell', 'rm', '-f', remotePath]);
|
|
348
|
+
const outputFile = pullResult?.exitCode === 0 ? await fsp.stat(outputPath).catch(() => null) : null;
|
|
349
|
+
const outputCheckResult = pullResult?.exitCode === 0 && !outputFile?.isFile()
|
|
350
|
+
? {
|
|
351
|
+
args: ['verify-output', outputPath],
|
|
352
|
+
command: adbPath,
|
|
353
|
+
exitCode: 1,
|
|
354
|
+
stderr: `Android screenrecord output was not found at ${outputPath}.`,
|
|
355
|
+
stdout: '',
|
|
356
|
+
}
|
|
357
|
+
: null;
|
|
358
|
+
const results = [recordResult, ...(pullResult ? [pullResult] : []), ...(outputCheckResult ? [outputCheckResult] : []), cleanupResult];
|
|
359
|
+
const failedResult = [recordResult, pullResult, outputCheckResult].find((result) => result && result.exitCode !== 0);
|
|
360
|
+
return {
|
|
361
|
+
...buildDriverResult({
|
|
362
|
+
action: 'record',
|
|
363
|
+
rawFileName,
|
|
364
|
+
result: {
|
|
365
|
+
args: recordResult.args,
|
|
366
|
+
command: recordResult.command,
|
|
367
|
+
exitCode: failedResult?.exitCode ?? 0,
|
|
368
|
+
stderr: '',
|
|
369
|
+
stdout: formatAndroidAdbCommandTranscript(results),
|
|
370
|
+
},
|
|
371
|
+
}),
|
|
372
|
+
...(failedResult ? {} : { capturePath: outputPath }),
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
async screenshot({ rawFileName = 'adb-screenshot.png', } = {}) {
|
|
376
|
+
const result = await executor(adbPath, ['-s', deviceSerial, 'exec-out', 'screencap', '-p']);
|
|
377
|
+
return buildDriverResult({ action: 'screenshot', rawFileName, result });
|
|
378
|
+
},
|
|
379
|
+
async scroll({ durationMs = 300, endX, endY, rawFileName = 'adb-scroll.txt', startX, startY, }) {
|
|
380
|
+
const result = await executor(adbPath, [
|
|
381
|
+
'-s',
|
|
382
|
+
deviceSerial,
|
|
383
|
+
'shell',
|
|
384
|
+
'input',
|
|
385
|
+
'swipe',
|
|
386
|
+
String(startX),
|
|
387
|
+
String(startY),
|
|
388
|
+
String(endX),
|
|
389
|
+
String(endY),
|
|
390
|
+
String(durationMs),
|
|
391
|
+
]);
|
|
392
|
+
return buildDriverResult({ action: 'scroll', rawFileName, result });
|
|
393
|
+
},
|
|
394
|
+
async tap({ rawFileName = 'adb-tap.txt', x, y, }) {
|
|
395
|
+
const result = await executor(adbPath, ['-s', deviceSerial, 'shell', 'input', 'tap', String(x), String(y)]);
|
|
396
|
+
return buildDriverResult({ action: 'tap', rawFileName, result });
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|