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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/app/profile-session.ts +812 -0
  4. package/core/config-template.json +41 -0
  5. package/dist/core/agent-summary.d.ts +15 -0
  6. package/dist/core/agent-summary.js +177 -0
  7. package/dist/core/artifact-contract.d.ts +151 -0
  8. package/dist/core/artifact-contract.js +897 -0
  9. package/dist/core/artifact-layout.d.ts +56 -0
  10. package/dist/core/artifact-layout.js +61 -0
  11. package/dist/core/artifact-writer.d.ts +44 -0
  12. package/dist/core/artifact-writer.js +55 -0
  13. package/dist/core/comparison.d.ts +133 -0
  14. package/dist/core/comparison.js +294 -0
  15. package/dist/core/evidence-interpreter.d.ts +28 -0
  16. package/dist/core/evidence-interpreter.js +69 -0
  17. package/dist/core/execution-plan.d.ts +44 -0
  18. package/dist/core/execution-plan.js +95 -0
  19. package/dist/core/planner.d.ts +132 -0
  20. package/dist/core/planner.js +812 -0
  21. package/dist/core/ports.d.ts +198 -0
  22. package/dist/core/ports.js +146 -0
  23. package/dist/core/run-index.d.ts +62 -0
  24. package/dist/core/run-index.js +143 -0
  25. package/dist/core/schema-validator.d.ts +86 -0
  26. package/dist/core/schema-validator.js +407 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +27 -0
  29. package/dist/runner/agent-device-driver.d.ts +126 -0
  30. package/dist/runner/agent-device-driver.js +168 -0
  31. package/dist/runner/agent-device.d.ts +295 -0
  32. package/dist/runner/agent-device.js +1271 -0
  33. package/dist/runner/android-adb-driver.d.ts +175 -0
  34. package/dist/runner/android-adb-driver.js +399 -0
  35. package/dist/runner/android-adb.d.ts +254 -0
  36. package/dist/runner/android-adb.js +1618 -0
  37. package/dist/runner/argent-driver.d.ts +183 -0
  38. package/dist/runner/argent-driver.js +297 -0
  39. package/dist/runner/argent.d.ts +349 -0
  40. package/dist/runner/argent.js +1211 -0
  41. package/dist/runner/check-plan.d.ts +45 -0
  42. package/dist/runner/check-plan.js +210 -0
  43. package/dist/runner/cli.d.ts +20 -0
  44. package/dist/runner/cli.js +23 -0
  45. package/dist/runner/compare-latest.d.ts +99 -0
  46. package/dist/runner/compare-latest.js +233 -0
  47. package/dist/runner/compare.d.ts +58 -0
  48. package/dist/runner/compare.js +157 -0
  49. package/dist/runner/demo-loop.d.ts +45 -0
  50. package/dist/runner/demo-loop.js +170 -0
  51. package/dist/runner/example-android-live.d.ts +137 -0
  52. package/dist/runner/example-android-live.js +454 -0
  53. package/dist/runner/example-ios-live.d.ts +137 -0
  54. package/dist/runner/example-ios-live.js +471 -0
  55. package/dist/runner/host-doctor.d.ts +131 -0
  56. package/dist/runner/host-doctor.js +628 -0
  57. package/dist/runner/init-project.d.ts +88 -0
  58. package/dist/runner/init-project.js +263 -0
  59. package/dist/runner/ios-simctl-driver.d.ts +69 -0
  60. package/dist/runner/ios-simctl-driver.js +97 -0
  61. package/dist/runner/ios-simctl.d.ts +254 -0
  62. package/dist/runner/ios-simctl.js +1415 -0
  63. package/dist/runner/live-android.d.ts +137 -0
  64. package/dist/runner/live-android.js +539 -0
  65. package/dist/runner/live-comparison.d.ts +67 -0
  66. package/dist/runner/live-comparison.js +147 -0
  67. package/dist/runner/live-ios.d.ts +137 -0
  68. package/dist/runner/live-ios.js +460 -0
  69. package/dist/runner/live-proof-summary.d.ts +263 -0
  70. package/dist/runner/live-proof-summary.js +465 -0
  71. package/dist/runner/live-proof.d.ts +467 -0
  72. package/dist/runner/live-proof.js +920 -0
  73. package/dist/runner/local-env.d.ts +64 -0
  74. package/dist/runner/local-env.js +155 -0
  75. package/dist/runner/profile-android.d.ts +82 -0
  76. package/dist/runner/profile-android.js +671 -0
  77. package/dist/runner/profile-ios.d.ts +108 -0
  78. package/dist/runner/profile-ios.js +532 -0
  79. package/dist/runner/profile-mobile.d.ts +254 -0
  80. package/dist/runner/profile-mobile.js +1307 -0
  81. package/dist/runner/validate-project.d.ts +273 -0
  82. package/dist/runner/validate-project.js +1501 -0
  83. package/docs/adapters.md +145 -0
  84. package/docs/api.md +94 -0
  85. package/docs/authoring.md +196 -0
  86. package/docs/concepts.md +136 -0
  87. package/docs/consumer-rehearsal.md +115 -0
  88. package/docs/contracts.md +267 -0
  89. package/docs/live-proofs.md +270 -0
  90. package/docs/principles.md +46 -0
  91. package/examples/event-logs/app-startup-baseline.log +4 -0
  92. package/examples/event-logs/app-startup-current.log +4 -0
  93. package/examples/minimal-app/README.md +70 -0
  94. package/examples/mobile-app/README.md +302 -0
  95. package/examples/mobile-app/app.json +22 -0
  96. package/examples/mobile-app/asl/package-scripts.json +32 -0
  97. package/examples/mobile-app/asl.config.json +37 -0
  98. package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
  99. package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
  100. package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
  101. package/examples/mobile-app/event-logs/app-startup.log +4 -0
  102. package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
  103. package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
  104. package/examples/mobile-app/index.ts +20 -0
  105. package/examples/mobile-app/metro.config.js +20 -0
  106. package/examples/mobile-app/package.json +62 -0
  107. package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
  108. package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
  109. package/examples/mobile-app/pnpm-lock.yaml +4440 -0
  110. package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
  111. package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
  112. package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
  113. package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
  114. package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
  115. package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
  116. package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
  117. package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
  118. package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
  119. package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
  120. package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
  121. package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
  122. package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
  123. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
  124. package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
  125. package/examples/mobile-app/src/example-screen.tsx +322 -0
  126. package/examples/mobile-app/tsconfig.json +16 -0
  127. package/examples/mobile-app/tsconfig.typecheck.json +13 -0
  128. package/examples/runners/README.md +44 -0
  129. package/examples/runners/adb-android.json +25 -0
  130. package/examples/runners/agent-device-android.json +27 -0
  131. package/examples/runners/agent-device-ios.json +27 -0
  132. package/examples/runners/argent-android.json +32 -0
  133. package/examples/runners/argent-ios.json +32 -0
  134. package/examples/runners/argent-react-profiler-provider.json +15 -0
  135. package/examples/runners/axe-accessibility-provider.json +24 -0
  136. package/examples/runners/manual-log-ingest.json +9 -0
  137. package/examples/runners/rozenite-profiler-provider.json +9 -0
  138. package/examples/runners/script-accessibility-provider.json +24 -0
  139. package/examples/runners/script-memory-provider.json +24 -0
  140. package/examples/runners/script-network-provider.json +24 -0
  141. package/examples/runners/script-profiler-provider.json +30 -0
  142. package/examples/runners/xcodebuildmcp-ios.json +29 -0
  143. package/examples/scenarios/ios/app-startup.json +28 -0
  144. package/examples/scenarios/ios/open-close-cycle.json +35 -0
  145. package/examples/scenarios/mobile/app-startup.json +72 -0
  146. package/examples/scenarios/mobile/media-open-close.json +141 -0
  147. package/examples/scenarios/mobile/open-close-cycle.json +135 -0
  148. package/examples/scenarios/mobile/scroll-settle.json +106 -0
  149. package/package.json +240 -0
  150. package/schemas/budget-verdict.schema.json +115 -0
  151. package/schemas/causal-run.schema.json +279 -0
  152. package/schemas/comparison.schema.json +196 -0
  153. package/schemas/health.schema.json +108 -0
  154. package/schemas/live-proof-set.schema.json +195 -0
  155. package/schemas/live-proof.schema.json +413 -0
  156. package/schemas/manifest.schema.json +204 -0
  157. package/schemas/metrics.schema.json +137 -0
  158. package/schemas/project-validation.schema.json +343 -0
  159. package/schemas/runner-capabilities.schema.json +217 -0
  160. package/schemas/scenario.schema.json +400 -0
  161. package/schemas/verdict.schema.json +88 -0
  162. package/templates/evidence-provider.json +83 -0
  163. package/templates/gitignore-snippet +9 -0
  164. package/templates/integration-readme.md +125 -0
  165. package/templates/mobile-scenario.json +133 -0
  166. package/templates/package-scripts.json +32 -0
  167. package/templates/primary-runner.json +19 -0
  168. package/templates/project.config.json +37 -0
  169. package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
  170. 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(/&quot;/gu, '"')
96
+ .replace(/&apos;/gu, "'")
97
+ .replace(/&lt;/gu, '<')
98
+ .replace(/&gt;/gu, '>')
99
+ .replace(/&amp;/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
+ }