agent-device 0.20.5 → 0.20.7

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 (196) hide show
  1. package/README.md +5 -3
  2. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.5.apk → agent-device-android-ime-helper-0.20.7.apk} +0 -0
  3. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.7.apk.sha256 +1 -0
  4. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.5.manifest.json → agent-device-android-ime-helper-0.20.7.manifest.json} +4 -4
  5. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.5.apk → agent-device-android-snapshot-helper-0.20.7.apk} +0 -0
  6. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.7.apk.sha256 +1 -0
  7. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.5.manifest.json → agent-device-android-snapshot-helper-0.20.7.manifest.json} +6 -6
  8. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +48 -0
  9. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  10. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +108 -1
  11. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +630 -89
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSelectorMatchPolicy.swift +78 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +126 -187
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.h +32 -0
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.m +212 -0
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +149 -10
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +161 -32
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CoordinateTextEntryTests.swift +4 -0
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +36 -28
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +23 -5
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +10 -0
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +0 -67
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +18 -1
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +2 -22
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SelectorMatchPolicyTests.swift +4 -0
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -1
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +145 -20
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +0 -20
  29. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +4 -0
  30. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +234 -0
  31. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +106 -448
  32. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryPolicyTests.swift +4 -0
  33. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +602 -0
  34. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +18 -0
  35. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +72 -0
  36. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +89 -0
  37. package/dist/src/agent-browser-tool.js +1 -1
  38. package/dist/src/agent-device-client.js +2 -2
  39. package/dist/src/android-adb.js +1 -1
  40. package/dist/src/android.js +1 -1
  41. package/dist/src/app-helpers.js +1 -1
  42. package/dist/src/app-lifecycle.js +7 -7
  43. package/dist/src/app-lifecycle2.js +2 -0
  44. package/dist/src/app-log-files.js +1 -0
  45. package/dist/src/app-log-reconnect.js +1 -0
  46. package/dist/src/app-log-runtime.js +4 -0
  47. package/dist/src/app-resolution.js +2 -1
  48. package/dist/src/apps.js +5 -5
  49. package/dist/src/audio-probe.js +1 -1
  50. package/dist/src/auth-session.js +1 -0
  51. package/dist/src/auth.js +2 -0
  52. package/dist/src/boot-diagnostics.js +2 -2
  53. package/dist/src/cli-config.js +1 -1
  54. package/dist/src/cli-grammar.js +1 -0
  55. package/dist/src/cli-help.js +204 -254
  56. package/dist/src/cli-output.js +1 -0
  57. package/dist/src/cli-runner.js +1 -2
  58. package/dist/src/cli.js +6 -16
  59. package/dist/src/client-metro.js +1 -1
  60. package/dist/src/cloud-artifacts.d.ts +2 -137
  61. package/dist/src/command-metadata.js +1 -1
  62. package/dist/src/command-schema.js +3 -3
  63. package/dist/src/connection-runtime.js +1 -0
  64. package/dist/src/connection.js +5 -0
  65. package/dist/src/context.js +1 -1
  66. package/dist/src/daemon-shutdown-report.js +1 -0
  67. package/dist/src/daemon.js +2 -0
  68. package/dist/src/debug-symbols.js +3 -3
  69. package/dist/src/device-claim-inspection.js +1 -0
  70. package/dist/src/device-input-state.js +2 -2
  71. package/dist/src/device.js +1 -1
  72. package/dist/src/device2.js +2 -0
  73. package/dist/src/find.js +1 -1
  74. package/dist/src/finders.js +1 -1
  75. package/dist/src/generic-settle.js +1 -0
  76. package/dist/src/generic.js +1 -1
  77. package/dist/src/harmonyos.js +1 -0
  78. package/dist/src/helper.js +1 -1
  79. package/dist/src/host-process.js +2 -2
  80. package/dist/src/ime-lifecycle.js +7 -7
  81. package/dist/src/index.d.ts +69 -18
  82. package/dist/src/input-actions.js +4 -4
  83. package/dist/src/input-actions2.js +1 -1
  84. package/dist/src/input-actions3.js +1 -0
  85. package/dist/src/install-artifact.js +1 -5
  86. package/dist/src/install-artifact2.js +1 -1
  87. package/dist/src/install-source2.js +1 -1
  88. package/dist/src/interaction-runtime.js +1 -0
  89. package/dist/src/interaction-snapshot.js +1 -1
  90. package/dist/src/interaction-touch-reference-frame.js +1 -0
  91. package/dist/src/interaction.js +1 -1
  92. package/dist/src/interactor.js +2 -2
  93. package/dist/src/interactor2.js +1 -1
  94. package/dist/src/internal/bin.js +2 -2
  95. package/dist/src/internal/daemon.js +9 -16
  96. package/dist/src/inventory.js +5 -0
  97. package/dist/src/inventory2.js +1 -0
  98. package/dist/src/inventory3.js +2 -0
  99. package/dist/src/inventory4.js +1 -0
  100. package/dist/src/inventory5.js +1 -0
  101. package/dist/src/inventory6.js +1 -0
  102. package/dist/src/lease-context.js +1 -1
  103. package/dist/src/lease.js +1 -1
  104. package/dist/src/limrun-runtime-dependencies.js +1 -0
  105. package/dist/src/limrun.d.ts +10 -104
  106. package/dist/src/limrun.js +1 -1
  107. package/dist/src/linux.js +1 -1
  108. package/dist/src/logcat.js +1 -1
  109. package/dist/src/manifest.js +1 -1
  110. package/dist/src/owner-identity.js +1 -0
  111. package/dist/src/physical-device-control.js +1 -1
  112. package/dist/src/platform-runtime-app-log-android-transport.js +1 -0
  113. package/dist/src/platform-runtime-app-log-process.js +1 -0
  114. package/dist/src/platform-runtime-host.js +1 -0
  115. package/dist/src/platform-runtime-network-web-transport.js +1 -0
  116. package/dist/src/platform-runtime-operation-host.js +5 -0
  117. package/dist/src/platform-runtime-toolchain-host.js +1 -0
  118. package/dist/src/platform-runtime.js +1 -0
  119. package/dist/src/prepare-kind.js +15 -0
  120. package/dist/src/process-entry.js +1 -0
  121. package/dist/src/process-exit.js +1 -0
  122. package/dist/src/process-lock.js +1 -1
  123. package/dist/src/provider-webdriver.js +1 -0
  124. package/dist/src/provider.js +1 -0
  125. package/dist/src/proxy.js +2 -0
  126. package/dist/src/react-native.js +1 -1
  127. package/dist/src/record-trace.js +1 -1
  128. package/dist/src/registry.js +25 -24
  129. package/dist/src/remote-config.js +1 -1
  130. package/dist/src/remote-config2.js +1 -1
  131. package/dist/src/replay.js +3 -0
  132. package/dist/src/reporting.js +4 -4
  133. package/dist/src/runner-client.js +1 -1
  134. package/dist/src/runner-disposal.js +1 -1
  135. package/dist/src/runner-lease.js +1 -1
  136. package/dist/src/runtime.js +3 -3
  137. package/dist/src/runtime2.js +4 -0
  138. package/dist/src/runtime3.js +3 -0
  139. package/dist/src/runtime4.js +1 -0
  140. package/dist/src/runtime5.js +1 -0
  141. package/dist/src/runtime6.js +1 -0
  142. package/dist/src/runtime7.js +1 -0
  143. package/dist/src/screenshot-result.js +4 -4
  144. package/dist/src/screenshot.js +1 -0
  145. package/dist/src/sdk-android-adb.d.ts +20 -2
  146. package/dist/src/sdk-android-adb.js +2 -2
  147. package/dist/src/sdk-batch-runner.js +3 -3
  148. package/dist/src/sdk-contracts.d.ts +47 -6
  149. package/dist/src/sdk-contracts.js +1 -1
  150. package/dist/src/sdk-device.js +1 -1
  151. package/dist/src/sdk-finders.d.ts +4 -20
  152. package/dist/src/sdk-finders.js +1 -1
  153. package/dist/src/sdk-remote-config.js +1 -1
  154. package/dist/src/sdk-selectors.d.ts +601 -0
  155. package/dist/src/sdk-selectors.js +1 -1
  156. package/dist/src/selector-runtime.js +1 -1
  157. package/dist/src/selector-vocabulary.js +1 -1
  158. package/dist/src/selectors.d.ts +1 -46
  159. package/dist/src/selectors.js +1 -1
  160. package/dist/src/server.js +3 -3
  161. package/dist/src/session-target-evidence.js +1 -1
  162. package/dist/src/session.js +10 -9
  163. package/dist/src/shared.js +2 -0
  164. package/dist/src/shell-quote.js +1 -0
  165. package/dist/src/simulator.js +1 -1
  166. package/dist/src/snapshot-runtime.js +1 -0
  167. package/dist/src/snapshot-session.js +1 -0
  168. package/dist/src/snapshot.js +1 -1
  169. package/dist/src/snapshot2.js +1 -1
  170. package/dist/src/src.js +1 -1
  171. package/dist/src/src2.js +2 -0
  172. package/dist/src/src3.js +1 -0
  173. package/dist/src/text-surface.js +1 -0
  174. package/dist/src/timing-safe-equal.js +1 -0
  175. package/dist/src/tool-provider.js +3 -3
  176. package/dist/src/tool-provider2.js +1 -1
  177. package/dist/src/tool-provider3.js +1 -1
  178. package/dist/src/update-check.js +1 -1
  179. package/dist/src/version.js +1 -1
  180. package/dist/src/web.js +1 -1
  181. package/package.json +72 -50
  182. package/server.json +3 -3
  183. package/skills/agent-device/SKILL.md +6 -31
  184. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.5.apk.sha256 +0 -1
  185. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.5.apk.sha256 +0 -1
  186. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -225
  187. package/dist/src/app-log-request-scope.js +0 -1
  188. package/dist/src/devices.js +0 -1
  189. package/dist/src/devices2.js +0 -1
  190. package/dist/src/devices3.js +0 -1
  191. package/dist/src/devices4.js +0 -1
  192. package/dist/src/interaction.d.ts +0 -230
  193. package/dist/src/provider-limrun-runtime.js +0 -1
  194. package/dist/src/register-builtins.js +0 -1
  195. package/dist/src/screenshot-geometry.js +0 -1
  196. package/dist/src/viewport-dimension.js +0 -14
@@ -0,0 +1,212 @@
1
+ #import "RunnerSynthesizedTextEntry.h"
2
+ #import "RunnerXCTestEventBridge.h"
3
+
4
+ #import <objc/message.h>
5
+
6
+ static NSString *const RunnerTextSynthesisSurface = @"text";
7
+
8
+ typedef id (*RunnerTextMsgSendInit)(id, SEL, NSString *);
9
+ typedef id (*RunnerTextMsgSendInitPath)(id, SEL);
10
+ typedef void (*RunnerTextMsgSendType)(id, SEL, NSString *, NSTimeInterval, NSUInteger, BOOL);
11
+ typedef void (*RunnerTextMsgSendTypeKey)(id, SEL, NSString *, NSUInteger, NSTimeInterval);
12
+
13
+ // Text-entry-specific extension of the shared bridge: the 1-arg `initWithName:`
14
+ // record initializer and the text-input path selectors, none of which gesture
15
+ // synthesis needs.
16
+ typedef struct {
17
+ RunnerXCTestEventBridge core;
18
+ SEL initRecordSelector;
19
+ SEL initPathSelector;
20
+ SEL typeTextSelector;
21
+ SEL typeKeySelector; // only required in `replace` mode
22
+ } RunnerTextEventBridge;
23
+
24
+ static NSString * _Nullable RunnerResolveTextEventBridge(
25
+ id application,
26
+ BOOL replace,
27
+ RunnerTextEventBridge *bridge
28
+ );
29
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryResult(
30
+ RunnerSynthesizedTextEntryStatus status,
31
+ NSString * _Nullable message
32
+ );
33
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryUnavailable(NSString *message);
34
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryFailed(NSString *message);
35
+ static RunnerSynthesizedTextEntryResult *RunnerSynthesizeTextWithMode(
36
+ id application,
37
+ NSString *text,
38
+ BOOL replace
39
+ );
40
+
41
+ @interface RunnerSynthesizedTextEntryResult ()
42
+
43
+ @property(nonatomic, readwrite) RunnerSynthesizedTextEntryStatus status;
44
+ @property(nonatomic, readwrite, nullable) NSString *message;
45
+
46
+ @end
47
+
48
+
49
+ @implementation RunnerSynthesizedTextEntryResult
50
+ @end
51
+
52
+ @implementation RunnerSynthesizedTextEntry
53
+
54
+ + (RunnerSynthesizedTextEntryResult *)synthesizeTextWithApplication:(id)application
55
+ text:(NSString *)text {
56
+ return RunnerSynthesizeTextWithMode(application, text, NO);
57
+ }
58
+
59
+ + (RunnerSynthesizedTextEntryResult *)replaceTextWithApplication:(id)application
60
+ text:(NSString *)text {
61
+ return RunnerSynthesizeTextWithMode(application, text, YES);
62
+ }
63
+
64
+ @end
65
+
66
+ static RunnerSynthesizedTextEntryResult *RunnerSynthesizeTextWithMode(
67
+ id application,
68
+ NSString *text,
69
+ BOOL replace
70
+ ) {
71
+ @try {
72
+ RunnerTextEventBridge bridge;
73
+ NSString *missing = RunnerResolveTextEventBridge(application, replace, &bridge);
74
+ if (missing != nil) return RunnerTextEntryUnavailable(missing);
75
+
76
+ NSInteger targetProcessID =
77
+ ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.core.processIDSelector);
78
+ if (targetProcessID <= 0) {
79
+ return RunnerTextEntryUnavailable(
80
+ @"private XCTest text synthesis unavailable: could not resolve target process ID"
81
+ );
82
+ }
83
+
84
+ if (replace) {
85
+ id selectionRecord = ((RunnerTextMsgSendInit)objc_msgSend)(
86
+ [bridge.core.recordClass alloc], bridge.initRecordSelector, @"agent-device-fill-select-all"
87
+ );
88
+ id selectionPath =
89
+ ((RunnerTextMsgSendInitPath)objc_msgSend)([bridge.core.pathClass alloc], bridge.initPathSelector);
90
+ if (selectionRecord == nil || selectionPath == nil) {
91
+ return RunnerTextEntryFailed(
92
+ @"private XCTest text synthesis failed: could not create the selection event"
93
+ );
94
+ }
95
+ ((RunnerMsgSendSetInteger)objc_msgSend)(
96
+ selectionRecord, bridge.core.setTargetProcessIDSelector, targetProcessID
97
+ );
98
+ // XCUIKeyModifierCommand is public XCTest API (1 << 4). Keeping the value local
99
+ // avoids importing XCUIAutomation headers into the bridging helper. A text-input
100
+ // path only performs its first operation reliably, so selection and replacement
101
+ // use ordered records rather than one multi-operation path.
102
+ NSUInteger commandModifier = 1UL << 4;
103
+ ((RunnerTextMsgSendTypeKey)objc_msgSend)(
104
+ selectionPath, bridge.typeKeySelector, @"a", commandModifier, 0.0
105
+ );
106
+ ((RunnerMsgSendAddPath)objc_msgSend)(selectionRecord, bridge.core.addPathSelector, selectionPath);
107
+ NSError *selectionError = nil;
108
+ BOOL selected = ((RunnerMsgSendSynthesize)objc_msgSend)(
109
+ selectionRecord, bridge.core.synthesizeSelector, &selectionError
110
+ );
111
+ if (!selected) {
112
+ NSString *detail = selectionError.localizedDescription ?: @"synthesizeWithError returned false";
113
+ return RunnerTextEntryFailed(
114
+ [NSString stringWithFormat:@"private XCTest text selection failed: %@", detail]
115
+ );
116
+ }
117
+ }
118
+
119
+ id record = ((RunnerTextMsgSendInit)objc_msgSend)(
120
+ [bridge.core.recordClass alloc],
121
+ bridge.initRecordSelector,
122
+ replace ? @"agent-device-fill-text" : @"agent-device-type"
123
+ );
124
+ id path = ((RunnerTextMsgSendInitPath)objc_msgSend)([bridge.core.pathClass alloc], bridge.initPathSelector);
125
+ if (record == nil || path == nil) {
126
+ return RunnerTextEntryFailed(
127
+ @"private XCTest text synthesis failed: could not create the text event"
128
+ );
129
+ }
130
+ ((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.core.setTargetProcessIDSelector, targetProcessID);
131
+ ((RunnerTextMsgSendType)objc_msgSend)(path, bridge.typeTextSelector, text, 0.0, 60, YES);
132
+ ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.core.addPathSelector, path);
133
+
134
+ NSError *error = nil;
135
+ BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.core.synthesizeSelector, &error);
136
+ if (!ok) {
137
+ NSString *detail = error.localizedDescription ?: @"synthesizeWithError returned false";
138
+ return RunnerTextEntryFailed(
139
+ [NSString stringWithFormat:@"private XCTest text synthesis failed: %@", detail]
140
+ );
141
+ }
142
+ return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusSucceeded, nil);
143
+ } @catch (NSException *exception) {
144
+ return RunnerTextEntryFailed(
145
+ RunnerFormatXCTestException(exception, @"private XCTest text synthesis failed")
146
+ );
147
+ }
148
+ }
149
+
150
+ static NSString * _Nullable RunnerResolveTextEventBridge(
151
+ id application,
152
+ BOOL replace,
153
+ RunnerTextEventBridge *bridge
154
+ ) {
155
+ RunnerXCTestEventBridge core;
156
+ NSString *missing = RunnerResolveXCTestEventBridge(application, RunnerTextSynthesisSurface, &core);
157
+ if (missing != nil) return missing;
158
+
159
+ SEL initRecordSelector = NSSelectorFromString(@"initWithName:");
160
+ SEL initPathSelector = NSSelectorFromString(@"initForTextInput");
161
+ SEL typeTextSelector = NSSelectorFromString(@"typeText:atOffset:typingSpeed:shouldRedact:");
162
+ SEL typeKeySelector = NSSelectorFromString(@"typeKey:modifiers:atOffset:");
163
+
164
+ missing = RunnerRequireSelector(
165
+ core.recordClass, initRecordSelector, @"initWithName:", RunnerTextSynthesisSurface
166
+ );
167
+ if (missing != nil) return missing;
168
+ missing = RunnerRequireSelector(
169
+ core.pathClass, initPathSelector, @"initForTextInput", RunnerTextSynthesisSurface
170
+ );
171
+ if (missing != nil) return missing;
172
+ missing = RunnerRequireSelector(
173
+ core.pathClass,
174
+ typeTextSelector,
175
+ @"typeText:atOffset:typingSpeed:shouldRedact:",
176
+ RunnerTextSynthesisSurface
177
+ );
178
+ if (missing != nil) return missing;
179
+ if (replace) {
180
+ missing = RunnerRequireSelector(
181
+ core.pathClass, typeKeySelector, @"typeKey:modifiers:atOffset:", RunnerTextSynthesisSurface
182
+ );
183
+ if (missing != nil) return missing;
184
+ }
185
+
186
+ *bridge = (RunnerTextEventBridge){
187
+ .core = core,
188
+ .initRecordSelector = initRecordSelector,
189
+ .initPathSelector = initPathSelector,
190
+ .typeTextSelector = typeTextSelector,
191
+ .typeKeySelector = typeKeySelector,
192
+ };
193
+ return nil;
194
+ }
195
+
196
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryResult(
197
+ RunnerSynthesizedTextEntryStatus status,
198
+ NSString * _Nullable message
199
+ ) {
200
+ RunnerSynthesizedTextEntryResult *result = [RunnerSynthesizedTextEntryResult new];
201
+ result.status = status;
202
+ result.message = message;
203
+ return result;
204
+ }
205
+
206
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryUnavailable(NSString *message) {
207
+ return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, message);
208
+ }
209
+
210
+ static RunnerSynthesizedTextEntryResult *RunnerTextEntryFailed(NSString *message) {
211
+ return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusFailed, message);
212
+ }
@@ -2,6 +2,34 @@ import XCTest
2
2
 
3
3
  extension RunnerTests {
4
4
  private static let privateAXSnapshotMaxNodes = 5_000
5
+
6
+ /// Upper bound on element-rooted follow-up snapshot requests per capture. A
7
+ /// depth-capped Bluesky-class tree resolves in 1-3 chained requests (~100-300ms
8
+ /// each); the bound exists so a pathological tree cannot stack requests past
9
+ /// the capture-plan deadline, which is also enforced per call.
10
+ private static let privateAXDeepExtensionCallLimit = 8
11
+
12
+ /// Upper bound on per-element custom-action reads per capture. Each is its own
13
+ /// AX round trip (~100ms on an idle simulator), so this caps the opt-in cost
14
+ /// at roughly a second on top of the capture; the capture-plan deadline stops
15
+ /// it earlier under load. A screenful of merged cards is well under this.
16
+ private static let privateAXCustomActionLimit = 12
17
+
18
+ /// A capture is depth-limited unless its frontier extension resolved every
19
+ /// capped node: frontiers left pending (budget/deadline) or missed (element
20
+ /// vanished, re-rooted request failed) mean subtrees are still absent, and
21
+ /// presenting such a capture as complete would hide exactly the content the
22
+ /// extension exists to recover. nil extension counts = extension never ran.
23
+ static func privateAXDepthLimited(
24
+ effectiveDepth: Int,
25
+ requestedDepth: Int,
26
+ pendingFrontiers: Int?,
27
+ missedFrontiers: Int?
28
+ ) -> Bool {
29
+ guard effectiveDepth < requestedDepth else { return false }
30
+ guard let pendingFrontiers, let missedFrontiers else { return true }
31
+ return pendingFrontiers > 0 || missedFrontiers > 0
32
+ }
5
33
  /// Deep React Native trees make the AX server reject bulk snapshot requests outright with
6
34
  /// kAXErrorIllegalArgument once the requested depth crosses a tree-size-dependent limit
7
35
  /// (observed between depth 56 and 64 on the Bluesky Home feed; the limit moves with live
@@ -10,6 +38,71 @@ extension RunnerTests {
10
38
  /// apps where the AX surface is genuinely unavailable.
11
39
  static let privateAXSnapshotDepthLadder = [56, 40, 24, 12]
12
40
 
41
+ /// Ladder rungs for one capture. A remembered accepted depth (recorded when a prior capture's
42
+ /// deep request was rejected) drops the rungs above it, so the known-rejected deep request is
43
+ /// not re-paid on every capture of the same screen class.
44
+ static func privateAXAttemptDepths(requestedDepth: Int, rememberedDepth: Int?) -> [Int] {
45
+ var depths = [requestedDepth]
46
+ depths.append(contentsOf: privateAXSnapshotDepthLadder.filter { $0 < requestedDepth })
47
+ guard let remembered = rememberedDepth, remembered < requestedDepth else { return depths }
48
+ return depths.filter { $0 <= remembered }
49
+ }
50
+
51
+ /// The bridge omits the key entirely when the capture did not ask for custom
52
+ /// actions, which is the difference between "nothing to disclose" and "read
53
+ /// none of them" — so a missing key must stay nil, never (0, 0).
54
+ static func privateAXCustomActionCoverage(_ raw: Any?) -> SnapshotCustomActionCoverage? {
55
+ guard let coverage = raw as? [String: Any],
56
+ let read = (coverage[RunnerAXSnapshotCustomActionsReadKey] as? NSNumber)?.intValue,
57
+ let candidates = (coverage[RunnerAXSnapshotCustomActionsCandidatesKey] as? NSNumber)?.intValue
58
+ else { return nil }
59
+ // read/candidates are the ratio and must both be present; the truncation
60
+ // count is additive, so an older bridge that omits it reads as zero rather
61
+ // than voiding the whole coverage.
62
+ let truncated =
63
+ (coverage[RunnerAXSnapshotCustomActionsTruncatedKey] as? NSNumber)?.intValue ?? 0
64
+ let blocked = (coverage[RunnerAXSnapshotCustomActionsBlockedKey] as? NSNumber)?.boolValue ?? false
65
+ return SnapshotCustomActionCoverage(
66
+ read: read, candidates: candidates, truncated: truncated, blocked: blocked)
67
+ }
68
+
69
+ func rememberPrivateAXAcceptedDepth(bundleId: String?, processIdentifier: Int?, depth: Int) {
70
+ // No PID means no way to notice a relaunch later; record nothing rather than risk serving
71
+ // a stale shallow rung to a fresh process.
72
+ guard let processIdentifier else { return }
73
+ privateAXAcceptedDepthLock.lock()
74
+ privateAXAcceptedDepthBundleId = bundleId
75
+ privateAXAcceptedDepthProcessIdentifier = processIdentifier
76
+ privateAXAcceptedDepth = depth
77
+ privateAXAcceptedDepthUntil = Date().addingTimeInterval(snapshotXCTestChannelPenaltyDuration)
78
+ privateAXAcceptedDepthLock.unlock()
79
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_DEPTH_REMEMBERED depth=%ld bundle=%@", depth, bundleId ?? "")
80
+ }
81
+
82
+ func rememberedPrivateAXAcceptedDepth(bundleId: String?, processIdentifier: Int?) -> Int? {
83
+ privateAXAcceptedDepthLock.lock()
84
+ defer { privateAXAcceptedDepthLock.unlock() }
85
+ guard Date() < privateAXAcceptedDepthUntil else { return nil }
86
+ guard privateAXAcceptedDepthBundleId == bundleId else { return nil }
87
+ guard let processIdentifier, privateAXAcceptedDepthProcessIdentifier == processIdentifier else {
88
+ return nil
89
+ }
90
+ return privateAXAcceptedDepth
91
+ }
92
+
93
+ func clearPrivateAXAcceptedDepth(reason: String) {
94
+ privateAXAcceptedDepthLock.lock()
95
+ let hadMemory = privateAXAcceptedDepth != nil && Date() < privateAXAcceptedDepthUntil
96
+ privateAXAcceptedDepthBundleId = nil
97
+ privateAXAcceptedDepthProcessIdentifier = nil
98
+ privateAXAcceptedDepth = nil
99
+ privateAXAcceptedDepthUntil = .distantPast
100
+ privateAXAcceptedDepthLock.unlock()
101
+ if hadMemory {
102
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_DEPTH_MEMORY_CLEARED reason=%@", reason)
103
+ }
104
+ }
105
+
13
106
  func privateAXSnapshotCapture(
14
107
  app: XCUIApplication,
15
108
  options: SnapshotOptions,
@@ -17,9 +110,19 @@ extension RunnerTests {
17
110
  ) -> SnapshotBackendCapture? {
18
111
  #if os(iOS) && targetEnvironment(simulator)
19
112
  let requestedDepth = options.depth ?? 64
20
- var attemptDepths = [requestedDepth]
21
- attemptDepths.append(
22
- contentsOf: Self.privateAXSnapshotDepthLadder.filter { $0 < requestedDepth }
113
+ // An explicit --depth request is honored as asked: no accepted-depth
114
+ // memory, no frontier extension past it.
115
+ let exactDepthRequested = options.depth != nil
116
+ let rememberedDepth =
117
+ exactDepthRequested
118
+ ? nil
119
+ : rememberedPrivateAXAcceptedDepth(
120
+ bundleId: currentBundleId,
121
+ processIdentifier: currentAppProcessIdentifier
122
+ )
123
+ let attemptDepths = Self.privateAXAttemptDepths(
124
+ requestedDepth: requestedDepth,
125
+ rememberedDepth: rememberedDepth
23
126
  )
24
127
  var response: [String: Any] = [:]
25
128
  var effectiveDepth = requestedDepth
@@ -35,7 +138,10 @@ extension RunnerTests {
35
138
  response = RunnerAXSnapshotBridge.snapshotTree(
36
139
  for: app,
37
140
  maxDepth: depth,
38
- maxNodes: Self.privateAXSnapshotMaxNodes
141
+ maxNodes: Self.privateAXSnapshotMaxNodes,
142
+ deepExtensionCallLimit: exactDepthRequested ? 0 : Self.privateAXDeepExtensionCallLimit,
143
+ customActionLimit: options.customActions ? Self.privateAXCustomActionLimit : 0,
144
+ deadline: deadline
39
145
  )
40
146
  if response["ok"] as? Bool == true {
41
147
  effectiveDepth = depth
@@ -52,6 +158,16 @@ extension RunnerTests {
52
158
  NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@", lastError)
53
159
  return nil
54
160
  }
161
+ // Only a capture that actually descended records memory: a first-rung success on a
162
+ // remembered depth deliberately does NOT refresh the TTL, so expiry re-probes the full
163
+ // requested depth once per window instead of capping this screen class forever.
164
+ if !exactDepthRequested, effectiveDepth != attemptDepths.first {
165
+ rememberPrivateAXAcceptedDepth(
166
+ bundleId: currentBundleId,
167
+ processIdentifier: currentAppProcessIdentifier,
168
+ depth: effectiveDepth
169
+ )
170
+ }
55
171
  guard let root = response["root"] as? [String: Any] else {
56
172
  NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=missing root")
57
173
  return nil
@@ -74,24 +190,46 @@ extension RunnerTests {
74
190
  return nil
75
191
  }
76
192
 
77
- let depthLimited = effectiveDepth < requestedDepth
193
+ // A capture whose frontier extension resolved every capped node is
194
+ // complete despite the per-request depth cap — reporting it as
195
+ // depth-limited would send agents chasing deeper content that is not
196
+ // there. Pending or missed frontiers keep the depth-limited verdict.
197
+ let deepExtension = response[RunnerAXSnapshotDeepExtensionKey] as? [String: Any]
198
+ let depthLimited = Self.privateAXDepthLimited(
199
+ effectiveDepth: effectiveDepth,
200
+ requestedDepth: requestedDepth,
201
+ pendingFrontiers: deepExtension?[RunnerAXSnapshotDeepExtensionPendingKey] as? Int,
202
+ missedFrontiers: deepExtension?[RunnerAXSnapshotDeepExtensionMissedKey] as? Int
203
+ )
78
204
  NSLog(
79
- "AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=%ld depth=%ld",
205
+ "AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=%ld depth=%ld extended=%ld",
80
206
  nodes.count,
81
- effectiveDepth
207
+ effectiveDepth,
208
+ deepExtension?[RunnerAXSnapshotDeepExtensionNodesAddedKey] as? Int ?? 0
82
209
  )
83
210
  return SnapshotBackendCapture(
84
211
  payload: DataPayload(nodes: nodes, truncated: (response["truncated"] as? Bool) == true),
85
- effectiveDepth: depthLimited ? effectiveDepth : nil
212
+ effectiveDepth: depthLimited ? effectiveDepth : nil,
213
+ customActions: Self.privateAXCustomActionCoverage(
214
+ response[RunnerAXSnapshotCustomActionsKey]
215
+ )
86
216
  )
87
217
  #else
88
218
  return nil
89
219
  #endif
90
220
  }
91
221
 
222
+ /// The viewport read is XCTest main-thread work — the exact channel the penalty marks as
223
+ /// grinding on this screen class. Under penalty it reliably burns its full timeout and
224
+ /// falls back anyway (~1s added to every private AX capture on the Bluesky bench feed),
225
+ /// so honor the penalty here the same way capture plans do.
226
+ func shouldReadPrivateAXViewportViaXCTest() -> Bool {
227
+ !hasAbandonedTreeCapture() && !isSnapshotXCTestChannelPenalized(bundleId: currentBundleId)
228
+ }
229
+
92
230
  private func privateAXSnapshotViewport(app: XCUIApplication, rootFrame: CGRect) -> CGRect {
93
231
  let fallback = rootFrame.isEmpty ? CGRect.infinite : rootFrame
94
- guard !hasAbandonedTreeCapture() else {
232
+ guard shouldReadPrivateAXViewportViaXCTest() else {
95
233
  return fallback
96
234
  }
97
235
  do {
@@ -164,7 +302,8 @@ extension RunnerTests {
164
302
  depth: depth,
165
303
  parentIndex: parentIndex,
166
304
  hiddenContentAbove: nil,
167
- hiddenContentBelow: nil
305
+ hiddenContentBelow: nil,
306
+ actions: rawNode["actions"] as? [String]
168
307
  )
169
308
  )
170
309
  } else {