agent-device 0.18.2 → 0.19.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 (216) hide show
  1. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.2.apk → agent-device-android-multitouch-helper-0.19.0.apk} +0 -0
  2. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
  3. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.2.manifest.json → agent-device-android-multitouch-helper-0.19.0.manifest.json} +4 -4
  4. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.apk → agent-device-android-snapshot-helper-0.19.0.apk} +0 -0
  5. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
  6. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.manifest.json → agent-device-android-snapshot-helper-0.19.0.manifest.json} +6 -6
  7. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
  8. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
  9. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
  10. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
  11. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
  12. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
  13. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
  14. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
  15. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
  16. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
  17. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
  18. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
  19. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
  20. package/dist/src/agent-browser-tool.js +1 -0
  21. package/dist/src/android-adb.d.ts +64 -234
  22. package/dist/src/android-adb.js +1 -1
  23. package/dist/src/android.js +1 -5
  24. package/dist/src/app-lifecycle.js +7 -0
  25. package/dist/src/apple-runner-options.js +1 -0
  26. package/dist/src/apps.js +5 -11
  27. package/dist/src/artifacts.d.ts +4 -3
  28. package/dist/src/artifacts.js +1 -1
  29. package/dist/src/audio-probe.js +1 -1
  30. package/dist/src/batch.d.ts +2 -177
  31. package/dist/src/batch.js +1 -1
  32. package/dist/src/cli-help.js +64 -38
  33. package/dist/src/cli-output.js +1 -1
  34. package/dist/src/cli-runner.js +2 -0
  35. package/dist/src/cli.js +12 -41
  36. package/dist/src/client-metro-companion.js +1 -0
  37. package/dist/src/client.js +3 -0
  38. package/dist/src/command-metadata.js +1 -0
  39. package/dist/src/command-schema.js +22 -0
  40. package/dist/src/command-surface.js +1 -1
  41. package/dist/src/contracts.d.ts +2 -213
  42. package/dist/src/contracts.js +1 -1
  43. package/dist/src/debug-symbols.js +5 -3
  44. package/dist/src/device-input-state.js +2 -0
  45. package/dist/src/device-rotation.js +14 -0
  46. package/dist/src/devices.js +1 -1
  47. package/dist/src/devices2.js +1 -0
  48. package/dist/src/devices3.js +1 -0
  49. package/dist/src/find.js +1 -1
  50. package/dist/src/finders.d.ts +2 -81
  51. package/dist/src/finders.js +1 -1
  52. package/dist/src/generic.js +1 -1
  53. package/dist/src/handler-utils.js +1 -1
  54. package/dist/src/helper.js +2 -0
  55. package/dist/src/host-process.js +2 -0
  56. package/dist/src/index.d.ts +1265 -1768
  57. package/dist/src/index.js +1 -1
  58. package/dist/src/input-actions.js +3 -1
  59. package/dist/src/input-actions2.js +1 -0
  60. package/dist/src/install-source.d.ts +13 -14
  61. package/dist/src/install-source.js +1 -1
  62. package/dist/src/install-source2.js +1 -0
  63. package/dist/src/interaction-snapshot.js +1 -0
  64. package/dist/src/interaction.js +1 -1
  65. package/dist/src/interactor.js +2 -1
  66. package/dist/src/internal/bin.d.ts +1 -1
  67. package/dist/src/internal/bin.js +2 -5
  68. package/dist/src/internal/companion-tunnel.d.ts +1 -1
  69. package/dist/src/internal/companion-tunnel.js +1 -1
  70. package/dist/src/internal/daemon.d.ts +1 -1
  71. package/dist/src/internal/daemon.js +16 -1
  72. package/dist/src/internal/png-worker.d.ts +29 -26
  73. package/dist/src/internal/png-worker.js +1 -1
  74. package/dist/src/internal/update-check-entry.d.ts +1 -1
  75. package/dist/src/internal/update-check-entry.js +1 -1
  76. package/dist/src/io.d.ts +2 -85
  77. package/dist/src/io.js +1 -1
  78. package/dist/src/lease.js +1 -1
  79. package/dist/src/linux.js +1 -1
  80. package/dist/src/logcat.js +6 -0
  81. package/dist/src/manifest.js +1 -0
  82. package/dist/src/metro.d.ts +70 -113
  83. package/dist/src/metro.js +1 -1
  84. package/dist/src/metro2.js +1 -0
  85. package/dist/src/notifications.js +1 -1
  86. package/dist/src/output-path.js +1 -0
  87. package/dist/src/path-resolution.js +1 -0
  88. package/dist/src/png-worker-contract.js +1 -0
  89. package/dist/src/points.js +4 -0
  90. package/dist/src/project-runtime.js +1 -0
  91. package/dist/src/provider.js +1 -1
  92. package/dist/src/providers.js +1 -0
  93. package/dist/src/react-native.js +1 -1
  94. package/dist/src/record-trace-recording.js +2 -5
  95. package/dist/src/record-trace.js +1 -1
  96. package/dist/src/recording-provider.js +1 -1
  97. package/dist/src/registry.js +37 -0
  98. package/dist/src/remote-config.d.ts +2 -83
  99. package/dist/src/reporting.js +9 -0
  100. package/dist/src/rolldown-runtime.js +1 -0
  101. package/dist/src/runner-client.js +1 -0
  102. package/dist/src/runner-provider.js +1 -0
  103. package/dist/src/runner-sequence.js +1 -1
  104. package/dist/src/runtime-transport.js +1 -0
  105. package/dist/src/runtime.js +3 -0
  106. package/dist/src/scoped-provider.js +1 -0
  107. package/dist/src/sdk-android-adb.d.ts +144 -0
  108. package/dist/src/sdk-android-adb.js +4 -0
  109. package/dist/src/sdk-app-inventory.d.ts +4 -0
  110. package/dist/src/sdk-app-inventory.js +1 -0
  111. package/dist/src/sdk-batch-runner.d.ts +40 -0
  112. package/dist/src/sdk-batch-runner.js +2 -0
  113. package/dist/src/sdk-batch.js +2 -0
  114. package/dist/src/sdk-contracts.d.ts +386 -0
  115. package/dist/src/sdk-contracts.js +2 -0
  116. package/dist/src/sdk-finders.d.ts +61 -0
  117. package/dist/src/sdk-finders.js +1 -0
  118. package/dist/src/sdk-io.d.ts +79 -0
  119. package/dist/src/sdk-io.js +1 -0
  120. package/dist/src/sdk-remote-config.d.ts +140 -0
  121. package/dist/src/sdk-remote-config.js +1 -0
  122. package/dist/src/sdk-selectors.js +1 -0
  123. package/dist/src/selector-runtime.js +1 -1
  124. package/dist/src/selectors.d.ts +39 -137
  125. package/dist/src/selectors.js +1 -1
  126. package/dist/src/selectors2.js +1 -0
  127. package/dist/src/server.js +3 -2
  128. package/dist/src/session.js +10 -11
  129. package/dist/src/setting-state.js +1 -0
  130. package/dist/src/simctl.js +1 -0
  131. package/dist/src/simulator.js +1 -2
  132. package/dist/src/snapshot-diagnostics.js +3 -0
  133. package/dist/src/snapshot-helper.js +5 -0
  134. package/dist/src/snapshot-quality.js +1 -0
  135. package/dist/src/snapshot.js +1 -2
  136. package/dist/src/snapshot2.js +2 -0
  137. package/dist/src/{5560.js → timeouts.js} +1 -1
  138. package/dist/src/tool-provider.js +3 -0
  139. package/dist/src/tool-provider2.js +1 -0
  140. package/dist/src/ttl-memo.js +1 -0
  141. package/dist/src/update-check.js +1 -0
  142. package/dist/src/url.js +1 -0
  143. package/dist/src/version.js +1 -0
  144. package/dist/src/web.js +1 -1
  145. package/dist/src/xml.js +2 -0
  146. package/package.json +12 -19
  147. package/server.json +2 -2
  148. package/skills/agent-device/SKILL.md +2 -0
  149. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.2.apk.sha256 +0 -1
  150. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.2.apk.sha256 +0 -1
  151. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
  152. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
  153. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
  154. package/dist/src/1404.js +0 -1
  155. package/dist/src/1986.js +0 -1
  156. package/dist/src/2748.js +0 -1
  157. package/dist/src/2948.js +0 -30
  158. package/dist/src/3006.js +0 -1
  159. package/dist/src/314.js +0 -1
  160. package/dist/src/3340.js +0 -8
  161. package/dist/src/4418.js +0 -1
  162. package/dist/src/4428.js +0 -3
  163. package/dist/src/485.js +0 -1
  164. package/dist/src/4917.js +0 -1
  165. package/dist/src/495.js +0 -5
  166. package/dist/src/5448.js +0 -3
  167. package/dist/src/5484.js +0 -1
  168. package/dist/src/5643.js +0 -1
  169. package/dist/src/587.js +0 -1
  170. package/dist/src/5913.js +0 -4
  171. package/dist/src/6849.js +0 -1
  172. package/dist/src/6918.js +0 -1
  173. package/dist/src/6967.js +0 -1
  174. package/dist/src/760.js +0 -1
  175. package/dist/src/7871.js +0 -1
  176. package/dist/src/8184.js +0 -1
  177. package/dist/src/8225.js +0 -1
  178. package/dist/src/8391.js +0 -1
  179. package/dist/src/8404.js +0 -2
  180. package/dist/src/8414.js +0 -2
  181. package/dist/src/8875.js +0 -4
  182. package/dist/src/893.js +0 -1
  183. package/dist/src/8936.js +0 -1
  184. package/dist/src/9414.js +0 -1
  185. package/dist/src/9581.js +0 -22
  186. package/dist/src/9612.js +0 -1
  187. package/dist/src/9624.js +0 -1
  188. package/dist/src/9722.js +0 -38
  189. package/dist/src/devices~1.js +0 -1
  190. package/dist/src/devices~2.js +0 -1
  191. package/dist/src/input-actions~1.js +0 -1
  192. package/dist/src/rslib-runtime.js +0 -1
  193. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  194. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  195. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  196. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  197. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  198. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  199. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
  200. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
  201. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  202. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  203. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
  204. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
  205. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
  206. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  207. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  208. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
  209. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
  210. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  211. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
  212. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  213. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  214. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  215. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  216. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
@@ -0,0 +1 @@
1
+ fbcaeff3de2f16c64ffe2fef8a4736b4392a08f11e3c957d826b40009195cf2e agent-device-android-multitouch-helper-0.19.0.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-multitouch-helper",
3
- "version": "0.18.2",
4
- "assetName": "agent-device-android-multitouch-helper-0.18.2.apk",
5
- "sha256": "1c8e6f04f5453ecea2e26281896faf7d940237c0011ba8d50b436c0b7ba82c4e",
3
+ "version": "0.19.0",
4
+ "assetName": "agent-device-android-multitouch-helper-0.19.0.apk",
5
+ "sha256": "fbcaeff3de2f16c64ffe2fef8a4736b4392a08f11e3c957d826b40009195cf2e",
6
6
  "packageName": "com.callstack.agentdevice.multitouchhelper",
7
- "versionCode": 18002,
7
+ "versionCode": 19000,
8
8
  "instrumentationRunner": "com.callstack.agentdevice.multitouchhelper/.MultiTouchInstrumentation",
9
9
  "statusProtocol": "android-multitouch-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ 56bb052ba4868491bdace904dc41f905cdde3a589e4f4f314d2a7d57c6f8d76c agent-device-android-snapshot-helper-0.19.0.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.18.2",
4
- "releaseTag": "v0.18.2",
5
- "assetName": "agent-device-android-snapshot-helper-0.18.2.apk",
3
+ "version": "0.19.0",
4
+ "releaseTag": "v0.19.0",
5
+ "assetName": "agent-device-android-snapshot-helper-0.19.0.apk",
6
6
  "apkUrl": null,
7
- "sha256": "a91e0b0b4d8b5b1dc6d1e25abe1491efefa8b8d89cc5c406f1fd84a302e56ebf",
8
- "checksumName": "agent-device-android-snapshot-helper-0.18.2.apk.sha256",
7
+ "sha256": "56bb052ba4868491bdace904dc41f905cdde3a589e4f4f314d2a7d57c6f8d76c",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.19.0.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 18002,
10
+ "versionCode": 19000,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -0,0 +1,28 @@
1
+ import XCTest
2
+
3
+ // The tap-point rule the runner shares with the TS runtime (ADR 0011 Layer 2).
4
+ //
5
+ // RULE: the tap point is the element frame's CENTER; a tap is allowed iff that
6
+ // center lies inside the window frame, edges inclusive. An empty or invalid
7
+ // window frame fails open (allowed): resolving the best available frame is the
8
+ // impure caller's job (onScreenWindowFrame / app.frame), and the policy must
9
+ // not turn a missing frame into a refusal.
10
+ //
11
+ // This is pure geometry on purpose — no XCUIElement — so the exact decision
12
+ // can be proven against the golden fixture table shared with the TS twin:
13
+ // table: contracts/fixtures/tap-point-policy.json
14
+ // TS twin: src/snapshot/mobile-snapshot-semantics.ts#isTapPointInsideViewport
15
+ // TS test: src/snapshot/__tests__/tap-point-policy-parity.test.ts
16
+ // Drift on either side turns CI red without needing a simulator.
17
+ enum TapPointPolicy {
18
+ static func isAllowed(elementFrame: CGRect, windowFrame: CGRect) -> Bool {
19
+ if windowFrame.isNull || windowFrame.isEmpty || windowFrame.isInfinite {
20
+ return true
21
+ }
22
+ let centerX = elementFrame.midX
23
+ let centerY = elementFrame.midY
24
+ return centerX >= windowFrame.minX && centerX <= windowFrame.maxX
25
+ && centerY >= windowFrame.minY && centerY <= windowFrame.maxY
26
+ }
27
+ }
28
+
@@ -12,7 +12,8 @@ extension RunnerTests {
12
12
 
13
13
  func privateAXSnapshotCapture(
14
14
  app: XCUIApplication,
15
- options: SnapshotOptions
15
+ options: SnapshotOptions,
16
+ deadline: Date = .distantFuture
16
17
  ) -> SnapshotBackendCapture? {
17
18
  #if os(iOS) && targetEnvironment(simulator)
18
19
  let requestedDepth = options.depth ?? 64
@@ -24,6 +25,13 @@ extension RunnerTests {
24
25
  var effectiveDepth = requestedDepth
25
26
  var lastError = "unknown private AX snapshot failure"
26
27
  for depth in attemptDepths {
28
+ // The first rung always runs (the plan gated entry on its own budget); later rungs
29
+ // stop when the capture-plan deadline is spent so ladder retries can never stack
30
+ // past the runner's main-thread watchdog (#1105).
31
+ if depth != attemptDepths.first, Date() >= deadline {
32
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_BUDGET_EXHAUSTED depth=%ld", depth)
33
+ break
34
+ }
27
35
  response = RunnerAXSnapshotBridge.snapshotTree(
28
36
  for: app,
29
37
  maxDepth: depth,
@@ -208,122 +216,3 @@ extension RunnerTests {
208
216
  }
209
217
  }
210
218
 
211
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
212
- // MARK: - In-bundle unit tests
213
-
214
- extension RunnerTests {
215
- func testPrivateAXScopeSelectsSubtreeNotMatchingLabels() {
216
- let tree: [String: Any] = [
217
- "type": 1, "label": "App",
218
- "children": [
219
- [
220
- "type": 9, "identifier": "homeScreen",
221
- "children": [
222
- ["type": 48, "label": "Post body without the scope text", "children": []]
223
- ],
224
- ],
225
- ["type": 9, "label": "unrelated sibling", "children": []],
226
- ],
227
- ]
228
- var nodes: [SnapshotNode] = []
229
- appendPrivateAXNode(
230
- tree,
231
- to: &nodes,
232
- options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
233
- viewport: .infinite,
234
- depth: 0,
235
- parentIndex: nil,
236
- insideMatchedScope: false
237
- )
238
-
239
- let labels = nodes.compactMap { $0.label ?? $0.identifier }
240
- XCTAssertTrue(labels.contains("homeScreen"))
241
- // Descendants of the matched scope are included even when they do not contain the text.
242
- XCTAssertTrue(labels.contains("Post body without the scope text"))
243
- XCTAssertFalse(labels.contains("unrelated sibling"))
244
- }
245
-
246
- func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
247
- let tree: [String: Any] = [
248
- "type": Int(XCUIElement.ElementType.application.rawValue),
249
- "label": "Blue Sky",
250
- "frame": ["x": 0, "y": 0, "width": 390, "height": 844],
251
- "children": [
252
- [
253
- "type": Int(XCUIElement.ElementType.scrollView.rawValue),
254
- "frame": ["x": 0, "y": 0, "width": 390, "height": 844],
255
- "children": [
256
- [
257
- "type": Int(XCUIElement.ElementType.image.rawValue),
258
- "label": "Callstack",
259
- "frame": ["x": 145, "y": 104, "width": 100, "height": 100],
260
- "children": [],
261
- ],
262
- [
263
- "type": Int(XCUIElement.ElementType.staticText.rawValue),
264
- "label": "Welcome back",
265
- "frame": ["x": 32, "y": 260, "width": 326, "height": 32],
266
- "children": [],
267
- ],
268
- [
269
- "type": Int(XCUIElement.ElementType.textField.rawValue),
270
- "label": "Email",
271
- "identifier": "login.email",
272
- "frame": ["x": 32, "y": 348, "width": 326, "height": 48],
273
- "children": [],
274
- ],
275
- [
276
- "type": Int(XCUIElement.ElementType.secureTextField.rawValue),
277
- "label": "Password",
278
- "identifier": "login.password",
279
- "frame": ["x": 32, "y": 412, "width": 326, "height": 48],
280
- "children": [],
281
- ],
282
- [
283
- "type": Int(XCUIElement.ElementType.button.rawValue),
284
- "label": "Sign in",
285
- "identifier": "login.submit",
286
- "frame": ["x": 32, "y": 492, "width": 326, "height": 52],
287
- "children": [],
288
- ],
289
- [
290
- "type": Int(XCUIElement.ElementType.link.rawValue),
291
- "label": "Forgot password?",
292
- "frame": ["x": 128, "y": 568, "width": 134, "height": 32],
293
- "children": [],
294
- ],
295
- [
296
- "type": Int(XCUIElement.ElementType.button.rawValue),
297
- "label": "Admin settings",
298
- "frame": ["x": -260, "y": 184, "width": 220, "height": 44],
299
- "children": [],
300
- ],
301
- [
302
- "type": Int(XCUIElement.ElementType.other.rawValue),
303
- "frame": ["x": 16, "y": 184, "width": 220, "height": 44],
304
- "children": [],
305
- ],
306
- ],
307
- ]
308
- ],
309
- ]
310
- var nodes: [SnapshotNode] = []
311
- appendPrivateAXNode(
312
- tree,
313
- to: &nodes,
314
- options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
315
- viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
316
- depth: 0,
317
- parentIndex: nil,
318
- insideMatchedScope: false
319
- )
320
-
321
- let labels = nodes.compactMap { $0.label }
322
- XCTAssertEqual(
323
- labels,
324
- ["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
325
- )
326
- XCTAssertFalse(labels.contains("Admin settings"))
327
- }
328
- }
329
- #endif
@@ -130,58 +130,6 @@ extension RunnerTests {
130
130
  return Response(ok: true, data: data)
131
131
  }
132
132
 
133
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
134
- func testGestureResponseIncludesSynthesizedTapFallbackDiagnostics() {
135
- let response = gestureResponse(
136
- message: "tapped",
137
- timing: (gestureStartUptimeMs: 1, gestureEndUptimeMs: 2),
138
- fallback: GestureFallback(
139
- strategy: "xctest-coordinate-tap",
140
- message: "Runner synthesized coordinate tap is unavailable",
141
- hint: "Using XCTest coordinate tap fallback."
142
- )
143
- )
144
-
145
- XCTAssertEqual(response.ok, true)
146
- XCTAssertEqual(response.data?.gestureFallback, "xctest-coordinate-tap")
147
- XCTAssertEqual(
148
- response.data?.gestureFallbackMessage,
149
- "Runner synthesized coordinate tap is unavailable"
150
- )
151
- XCTAssertEqual(response.data?.gestureFallbackHint, "Using XCTest coordinate tap fallback.")
152
- }
153
-
154
- func testXCTestRecordedFailureResponseFailsMutatingSuccesses() throws {
155
- let command = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
156
- let response = Response(ok: true, data: DataPayload(message: "tapped"))
157
-
158
- let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
159
-
160
- XCTAssertEqual(failureResponse?.ok, false)
161
- XCTAssertEqual(failureResponse?.error?.code, "XCTEST_RECORDED_FAILURE")
162
- XCTAssertEqual(
163
- failureResponse?.error?.message,
164
- "XCTest recorded a failure while executing tap; the action may not have been performed."
165
- )
166
- }
167
-
168
- func testXCTestRecordedFailureResponseDoesNotWrapReadOnlyOrRunnerFatalResponses() throws {
169
- let snapshotCommand = try runnerCommandFixture(#"{"command":"snapshot","commandId":"snapshot-1"}"#)
170
- let tapCommand = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
171
- let runnerFatalResponse = Response(
172
- ok: true,
173
- data: DataPayload(runnerFatal: true, runnerFatalReason: "ax_snapshot_unavailable")
174
- )
175
-
176
- XCTAssertNil(
177
- xctestRecordedFailureResponse(
178
- command: snapshotCommand,
179
- response: Response(ok: true, data: DataPayload(nodes: [], truncated: false))
180
- )
181
- )
182
- XCTAssertNil(xctestRecordedFailureResponse(command: tapCommand, response: runnerFatalResponse))
183
- }
184
- #endif
185
133
 
186
134
  func execute(command: Command) throws -> Response {
187
135
  if command.command == .status {
@@ -233,23 +181,126 @@ extension RunnerTests {
233
181
  )
234
182
  }
235
183
 
184
+ /// Tracks one main-queue dispatch so the watchdog and the dispatched block can agree —
185
+ /// under `mainThreadWorkLock` — on exactly one of: finished in time, or abandoned.
186
+ private final class MainThreadWorkState {
187
+ var finished = false
188
+ var abandoned = false
189
+ }
190
+
191
+ enum MainThreadBusyState {
192
+ case idle
193
+ case busy(abandonedForSeconds: TimeInterval)
194
+ case wedged(abandonedForSeconds: TimeInterval)
195
+ }
196
+
197
+ func currentMainThreadBusyState() -> MainThreadBusyState {
198
+ mainThreadWorkLock.lock()
199
+ defer { mainThreadWorkLock.unlock() }
200
+ guard abandonedMainThreadWorkCount > 0 else { return .idle }
201
+ let abandonedFor = abandonedMainThreadWorkSince.map { Date().timeIntervalSince($0) } ?? 0
202
+ if abandonedFor > mainThreadWedgeThreshold {
203
+ return .wedged(abandonedForSeconds: abandonedFor)
204
+ }
205
+ return .busy(abandonedForSeconds: abandonedFor)
206
+ }
207
+
208
+ private func runnerBusyResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
209
+ NSLog(
210
+ "AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f",
211
+ command.command.rawValue,
212
+ command.commandId ?? "",
213
+ abandonedForSeconds
214
+ )
215
+ return Response(
216
+ ok: false,
217
+ error: ErrorPayload(
218
+ code: "RUNNER_BUSY",
219
+ message:
220
+ "The iOS runner is still finishing a previous command that exceeded its execution watchdog (usually an accessibility capture on a heavy or animating screen).",
221
+ hint:
222
+ "Wait a few seconds and retry. If snapshots keep failing on this screen, use screenshot as visual truth and interact by coordinates, or navigate to another screen."
223
+ )
224
+ )
225
+ }
226
+
227
+ private func runnerWedgedResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
228
+ NSLog(
229
+ "AGENT_DEVICE_RUNNER_WEDGED command=%@ commandId=%@ abandonedForSeconds=%.1f",
230
+ command.command.rawValue,
231
+ command.commandId ?? "",
232
+ abandonedForSeconds
233
+ )
234
+ return Response(
235
+ ok: false,
236
+ error: ErrorPayload(
237
+ code: "RUNNER_WEDGED",
238
+ message:
239
+ "The iOS runner main thread has been stuck in abandoned work for \(Int(abandonedForSeconds)) seconds and cannot recover on its own.",
240
+ hint:
241
+ "The runner session will be restarted. Retry the command after the restart; if this screen keeps wedging captures, use screenshot as visual truth and interact by coordinates."
242
+ )
243
+ )
244
+ }
245
+
236
246
  private func executeDispatched(command: Command) throws -> Response {
247
+ // XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
248
+ // block, queueing more main-thread commands behind it only buries the runner deeper.
249
+ // Refuse fast instead so the daemon backs off while the abandoned work drains; past the
250
+ // wedge threshold, escalate so the daemon recycles this runner (#1105).
251
+ switch currentMainThreadBusyState() {
252
+ case .idle:
253
+ break
254
+ case .busy(let abandonedForSeconds):
255
+ return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
256
+ case .wedged(let abandonedForSeconds):
257
+ return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
258
+ }
237
259
  if Thread.isMainThread {
238
260
  return try executeOnMainSafely(command: command)
239
261
  }
240
262
  var result: Result<Response, Error>?
241
263
  let semaphore = DispatchSemaphore(value: 0)
264
+ let workState = MainThreadWorkState()
242
265
  DispatchQueue.main.async {
243
266
  do {
244
267
  result = .success(try self.executeOnMainSafely(command: command))
245
268
  } catch {
246
269
  result = .failure(error)
247
270
  }
271
+ self.mainThreadWorkLock.lock()
272
+ if workState.abandoned {
273
+ self.abandonedMainThreadWorkCount -= 1
274
+ if self.abandonedMainThreadWorkCount == 0 {
275
+ self.abandonedMainThreadWorkSince = nil
276
+ NSLog("AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED")
277
+ }
278
+ } else {
279
+ workState.finished = true
280
+ }
281
+ self.mainThreadWorkLock.unlock()
248
282
  semaphore.signal()
249
283
  }
250
284
  let waitResult = semaphore.wait(timeout: .now() + mainThreadExecutionTimeout)
251
285
  if waitResult == .timedOut {
252
286
  // The main queue work may still be running; we stop waiting and report timeout.
287
+ mainThreadWorkLock.lock()
288
+ let stillRunning = !workState.finished
289
+ if stillRunning {
290
+ workState.abandoned = true
291
+ abandonedMainThreadWorkCount += 1
292
+ if abandonedMainThreadWorkSince == nil {
293
+ abandonedMainThreadWorkSince = Date()
294
+ }
295
+ }
296
+ mainThreadWorkLock.unlock()
297
+ if stillRunning && command.command == .snapshot {
298
+ // The next capture on this screen must not re-grind the tree backend.
299
+ penalizeSnapshotTreeBackend(
300
+ bundleId: command.appBundleId,
301
+ reason: "main_thread_watchdog"
302
+ )
303
+ }
253
304
  throw NSError(
254
305
  domain: RunnerErrorDomain.general,
255
306
  code: RunnerErrorCode.mainThreadExecutionTimedOut,
@@ -477,6 +528,24 @@ extension RunnerTests {
477
528
  }
478
529
  if let element = match.element {
479
530
  let frame = element.frame
531
+ // XCTest reports closed-drawer/off-viewport items as hittable, then
532
+ // "taps" coordinates outside the visible window as a silent no-op.
533
+ // Refuse instead; the daemon falls back to tree-based resolution,
534
+ // which can prefer an on-screen candidate or explain the off-screen
535
+ // state. The check uses the main window frame, not app.frame: on RN
536
+ // apps app.frame unions transformed subtrees (a closed drawer at
537
+ // negative x), so it happily "contains" unreachable coordinates.
538
+ // The DECISION lives in TapPointPolicy (golden parity table with the
539
+ // TS twin); onScreenWindowFrame only supplies the frame.
540
+ if !match.usedNonHittableFallback
541
+ && !TapPointPolicy.isAllowed(
542
+ elementFrame: frame,
543
+ windowFrame: onScreenWindowFrame(app: activeApp)
544
+ ) {
545
+ return Response(ok: false, error: ErrorPayload(
546
+ code: "ELEMENT_OFFSCREEN",
547
+ message: "element resolved off-screen at (\(Int(frame.midX)), \(Int(frame.midY)))"))
548
+ }
480
549
  let isTextEntry = isTextEntryElement(element)
481
550
  let touchFrame = frame.isEmpty
482
551
  ? nil
@@ -504,18 +573,6 @@ extension RunnerTests {
504
573
  }
505
574
  return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
506
575
  }
507
- if let text = command.text {
508
- if let element = findElement(app: activeApp, text: text) {
509
- let (timing, outcome) = performGesture(activeApp) {
510
- activateElement(app: activeApp, element: element, action: "tap by text")
511
- }
512
- if let response = unsupportedResponse(for: outcome) {
513
- return response
514
- }
515
- return gestureResponse(message: "tapped", timing: timing)
516
- }
517
- return Response(ok: false, error: ErrorPayload(message: "element not found"))
518
- }
519
576
  if let x = command.x, let y = command.y {
520
577
  var fallback: GestureFallback?
521
578
  if command.synthesized == true {
@@ -539,7 +596,7 @@ extension RunnerTests {
539
596
  fallback: fallback
540
597
  )
541
598
  }
542
- return Response(ok: false, error: ErrorPayload(message: "tap requires text or x/y"))
599
+ return Response(ok: false, error: ErrorPayload(message: "tap requires a selector or x/y"))
543
600
  case .mouseClick:
544
601
  guard let x = command.x, let y = command.y else {
545
602
  return Response(ok: false, error: ErrorPayload(message: "mouseClick requires x and y"))
@@ -1128,6 +1185,7 @@ extension RunnerTests {
1128
1185
  let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
1129
1186
  let textEntryMode = resolveTextEntryMode(command)
1130
1187
  let target: TextEntryTarget
1188
+ let focusStartedAt = Date()
1131
1189
  if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
1132
1190
  let match = findElement(
1133
1191
  app: activeApp,
@@ -1148,6 +1206,13 @@ extension RunnerTests {
1148
1206
  } else {
1149
1207
  target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
1150
1208
  }
1209
+ NSLog(
1210
+ "AGENT_DEVICE_RUNNER_TEXT_ENTRY_PHASE commandId=%@ phase=focus durationMs=%.1f chars=%d mode=%@",
1211
+ command.commandId ?? "",
1212
+ Date().timeIntervalSince(focusStartedAt) * 1000.0,
1213
+ text.count,
1214
+ textEntryModeName(textEntryMode)
1215
+ )
1151
1216
  if textEntryMode == .replacement {
1152
1217
  guard target.element != nil else {
1153
1218
  let message =
@@ -1162,7 +1227,8 @@ extension RunnerTests {
1162
1227
  target: target,
1163
1228
  text: text,
1164
1229
  delaySeconds: delaySeconds,
1165
- repairMode: textEntryMode
1230
+ repairMode: textEntryMode,
1231
+ commandId: command.commandId
1166
1232
  )
1167
1233
  if textResult.verified == false {
1168
1234
  let expected = textResult.expectedText ?? ""
@@ -0,0 +1,155 @@
1
+ import Foundation
2
+ import XCTest
3
+
4
+ enum RunnerCommandLifecycleState: String {
5
+ case notAccepted
6
+ case accepted
7
+ case started
8
+ case completed
9
+ case failed
10
+ }
11
+
12
+ struct RunnerCommandJournalEntry {
13
+ let commandId: String
14
+ let command: String
15
+ var state: RunnerCommandLifecycleState
16
+ var responseOk: Bool?
17
+ var responseJson: String?
18
+ var error: ErrorPayload?
19
+ }
20
+
21
+ final class RunnerCommandJournal {
22
+ private let lock = NSLock()
23
+ private let maxEntries = 64
24
+ private let maxResponseJsonBytes = 16 * 1024
25
+ private var entries: [String: RunnerCommandJournalEntry] = [:]
26
+ private var order: [String] = []
27
+
28
+ func accept(command: Command) {
29
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
30
+ lock.lock()
31
+ defer { lock.unlock() }
32
+ entries[commandId] = RunnerCommandJournalEntry(
33
+ commandId: commandId,
34
+ command: command.command.rawValue,
35
+ state: .accepted,
36
+ responseOk: nil,
37
+ responseJson: nil,
38
+ error: nil
39
+ )
40
+ order.removeAll { $0 == commandId }
41
+ order.append(commandId)
42
+ pruneIfNeeded()
43
+ }
44
+
45
+ func start(command: Command) {
46
+ update(command: command, state: .started, responseOk: nil, responseJson: nil, error: nil)
47
+ }
48
+
49
+ func finish(command: Command, response: Response) {
50
+ update(
51
+ command: command,
52
+ state: response.ok ? .completed : .failed,
53
+ responseOk: response.ok,
54
+ responseJson: encodeResponseJson(command: command, response: response),
55
+ error: response.error
56
+ )
57
+ }
58
+
59
+ func fail(command: Command, error: Error) {
60
+ update(
61
+ command: command,
62
+ state: .failed,
63
+ responseOk: nil,
64
+ responseJson: nil,
65
+ error: ErrorPayload(message: error.localizedDescription)
66
+ )
67
+ }
68
+
69
+ func status(commandId: String) -> DataPayload {
70
+ guard let normalized = normalizedCommandId(commandId) else {
71
+ return DataPayload(lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue)
72
+ }
73
+ lock.lock()
74
+ let entry = entries[normalized]
75
+ lock.unlock()
76
+ guard let entry else {
77
+ return DataPayload(
78
+ commandId: normalized,
79
+ lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue
80
+ )
81
+ }
82
+ return DataPayload(
83
+ commandId: entry.commandId,
84
+ lifecycleState: entry.state.rawValue,
85
+ lifecycleCommand: entry.command,
86
+ lifecycleResponseOk: entry.responseOk,
87
+ lifecycleResponseJson: entry.responseJson,
88
+ lifecycleErrorCode: entry.error?.code,
89
+ lifecycleErrorMessage: entry.error?.message,
90
+ lifecycleErrorHint: entry.error?.hint
91
+ )
92
+ }
93
+
94
+ private func update(
95
+ command: Command,
96
+ state: RunnerCommandLifecycleState,
97
+ responseOk: Bool?,
98
+ responseJson: String?,
99
+ error: ErrorPayload?
100
+ ) {
101
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
102
+ lock.lock()
103
+ defer { lock.unlock() }
104
+ var entry = entries[commandId] ?? RunnerCommandJournalEntry(
105
+ commandId: commandId,
106
+ command: command.command.rawValue,
107
+ state: .accepted,
108
+ responseOk: nil,
109
+ responseJson: nil,
110
+ error: nil
111
+ )
112
+ entry.state = state
113
+ entry.responseOk = responseOk
114
+ entry.responseJson = responseJson
115
+ entry.error = error
116
+ entries[commandId] = entry
117
+ order.removeAll { $0 == commandId }
118
+ order.append(commandId)
119
+ pruneIfNeeded()
120
+ }
121
+
122
+ private func pruneIfNeeded() {
123
+ while order.count > maxEntries {
124
+ let removed = order.removeFirst()
125
+ entries.removeValue(forKey: removed)
126
+ }
127
+ }
128
+
129
+ private func normalizedCommandId(_ value: String?) -> String? {
130
+ guard let value else { return nil }
131
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
132
+ return trimmed.isEmpty ? nil : trimmed
133
+ }
134
+
135
+ private func encodeResponseJson(command: Command, response: Response) -> String? {
136
+ guard shouldRetainResponseJson(command: command) else { return nil }
137
+ guard let data = try? JSONEncoder().encode(response) else { return nil }
138
+ guard data.count <= maxResponseJsonBytes else { return nil }
139
+ return String(data: data, encoding: .utf8)
140
+ }
141
+
142
+ private func shouldRetainResponseJson(command: Command) -> Bool {
143
+ switch command.command {
144
+ case .snapshot, .screenshot:
145
+ return false
146
+ case .tap, .mouseClick, .longPress, .drag,
147
+ .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
148
+ .backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
149
+ .alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
150
+ .status, .uptime, .shutdown:
151
+ return true
152
+ }
153
+ }
154
+ }
155
+