agent-device 0.19.2 → 0.20.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 (178) hide show
  1. package/README.md +61 -70
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk +0 -0
  3. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk.sha256 +1 -0
  4. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.manifest.json +10 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk +0 -0
  6. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk.sha256 +1 -0
  7. package/{android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.manifest.json → android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.manifest.json} +6 -6
  8. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +2 -0
  9. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +5 -0
  10. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +12 -10
  11. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +154 -185
  12. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +113 -19
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift +82 -0
  14. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +275 -113
  15. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +2 -2
  16. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +150 -104
  17. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +56 -2
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +73 -0
  19. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +44 -14
  20. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +2 -2
  21. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +2 -106
  22. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +126 -11
  23. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +51 -8
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +24 -0
  25. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +1 -1
  26. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +43 -35
  27. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +14 -0
  28. package/dist/src/agent-browser-tool.js +1 -1
  29. package/dist/src/agent-device-client.js +3 -0
  30. package/dist/src/android-adb.d.ts +1 -1
  31. package/dist/src/android-adb.js +1 -1
  32. package/dist/src/android-input-method-overlays.js +1 -0
  33. package/dist/src/android-input-ownership.js +1 -6
  34. package/dist/src/android-system-chrome.js +1 -0
  35. package/dist/src/android.js +1 -1
  36. package/dist/src/app-lifecycle.js +5 -5
  37. package/dist/src/app-log-request-scope.js +1 -0
  38. package/dist/src/app-resolution.js +1 -0
  39. package/dist/src/apple-multitouch-support.js +1 -0
  40. package/dist/src/apps.js +5 -5
  41. package/dist/src/audio-probe.js +1 -1
  42. package/dist/src/boot-diagnostics.js +2 -0
  43. package/dist/src/cli-config.js +1 -0
  44. package/dist/src/cli-help.js +64 -30
  45. package/dist/src/cli-runner.js +1 -1
  46. package/dist/src/cli.js +15 -11
  47. package/dist/src/command-schema.js +3 -3
  48. package/dist/src/config.js +1 -0
  49. package/dist/src/context.js +1 -0
  50. package/dist/src/contracts.d.ts +1 -1
  51. package/dist/src/contracts.js +1 -1
  52. package/dist/src/daemon-shutdown-report.js +1 -0
  53. package/dist/src/debug-symbols.js +1 -1
  54. package/dist/src/device-input-state.js +1 -1
  55. package/dist/src/devices2.js +1 -1
  56. package/dist/src/find.js +1 -1
  57. package/dist/src/finders.js +1 -1
  58. package/dist/src/generic.js +1 -1
  59. package/dist/src/helper.js +1 -1
  60. package/dist/src/index.d.ts +412 -289
  61. package/dist/src/index.js +1 -1
  62. package/dist/src/input-actions.js +4 -3
  63. package/dist/src/input-actions2.js +1 -1
  64. package/dist/src/install-artifact.js +1 -0
  65. package/dist/src/install-source.d.ts +5 -1
  66. package/dist/src/install-source.js +1 -1
  67. package/dist/src/install-source2.js +1 -1
  68. package/dist/src/interaction-snapshot.js +1 -1
  69. package/dist/src/interaction.d.ts +230 -0
  70. package/dist/src/interaction.js +1 -1
  71. package/dist/src/interactor.js +2 -2
  72. package/dist/src/internal/bin.js +1 -1
  73. package/dist/src/internal/daemon.js +43 -17
  74. package/dist/src/internal/png-worker.d.ts +22 -0
  75. package/dist/src/internal/png-worker.js +1 -1
  76. package/dist/src/launch-console.js +1 -0
  77. package/dist/src/lease-context.js +1 -0
  78. package/dist/src/lease.js +1 -1
  79. package/dist/src/linux.js +1 -1
  80. package/dist/src/logcat.js +5 -0
  81. package/dist/src/metro.d.ts +2 -2
  82. package/dist/src/notifications.js +1 -1
  83. package/dist/src/png-worker-contract.js +1 -1
  84. package/dist/src/process-lock.js +1 -0
  85. package/dist/src/react-native.js +1 -1
  86. package/dist/src/record-trace.js +1 -1
  87. package/dist/src/register-builtins.js +1 -0
  88. package/dist/src/registry.js +24 -24
  89. package/dist/src/reporting.js +8 -7
  90. package/dist/src/runner-client.js +1 -1
  91. package/dist/src/runner-disposal.js +2 -0
  92. package/dist/src/runner-lease.js +1 -0
  93. package/dist/src/runner-sequence.js +1 -1
  94. package/dist/src/runtime-transport.js +1 -1
  95. package/dist/src/runtime.js +3 -3
  96. package/dist/src/runtime2.js +1 -0
  97. package/dist/src/sdk-android-adb.d.ts +119 -33
  98. package/dist/src/sdk-android-adb.js +4 -4
  99. package/dist/src/sdk-app-inventory.js +1 -1
  100. package/dist/src/sdk-batch-runner.d.ts +2 -1
  101. package/dist/src/sdk-batch-runner.js +2 -2
  102. package/dist/src/sdk-batch.js +2 -2
  103. package/dist/src/sdk-contracts.d.ts +105 -98
  104. package/dist/src/sdk-contracts.js +2 -2
  105. package/dist/src/sdk-device.js +1 -0
  106. package/dist/src/sdk-finders.d.ts +3 -3
  107. package/dist/src/sdk-finders.js +1 -1
  108. package/dist/src/sdk-io.d.ts +1 -1
  109. package/dist/src/sdk-io.js +1 -1
  110. package/dist/src/sdk-remote-config.d.ts +9 -1
  111. package/dist/src/sdk-remote-config.js +1 -1
  112. package/dist/src/sdk-selectors.js +1 -1
  113. package/dist/src/selector-runtime.js +1 -1
  114. package/dist/src/selector-vocabulary.js +1 -0
  115. package/dist/src/selectors.d.ts +12 -3
  116. package/dist/src/selectors.js +1 -1
  117. package/dist/src/server.js +3 -3
  118. package/dist/src/session-target-evidence.js +1 -0
  119. package/dist/src/session.js +8 -10
  120. package/dist/src/setting-state.js +1 -1
  121. package/dist/src/simulator.js +1 -1
  122. package/dist/src/snapshot-diagnostics.js +5 -2
  123. package/dist/src/snapshot.js +1 -1
  124. package/dist/src/snapshot2.js +1 -2
  125. package/dist/src/tool-provider.js +2 -2
  126. package/dist/src/tool-provider2.js +1 -1
  127. package/dist/src/tv-remote.js +13 -12
  128. package/dist/src/url.js +1 -1
  129. package/dist/src/web.js +1 -1
  130. package/package.json +33 -20
  131. package/server.json +2 -2
  132. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.apk +0 -0
  133. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.apk.sha256 +0 -1
  134. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.manifest.json +0 -10
  135. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.apk +0 -0
  136. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.apk.sha256 +0 -1
  137. package/dist/src/apple-runner-options.js +0 -1
  138. package/dist/src/cli-output.js +0 -1
  139. package/dist/src/client.js +0 -3
  140. package/dist/src/handler-utils.js +0 -1
  141. package/dist/src/output-path.js +0 -1
  142. package/dist/src/points.js +0 -4
  143. package/dist/src/provider.js +0 -1
  144. package/dist/src/providers.js +0 -1
  145. package/dist/src/record-trace-recording.js +0 -23
  146. package/dist/src/recording-provider.js +0 -1
  147. package/dist/src/selectors2.js +0 -1
  148. package/dist/src/simctl.js +0 -1
  149. package/dist/src/snapshot-helper.js +0 -5
  150. package/dist/src/ttl-memo.js +0 -1
  151. package/dist/src/xml.js +0 -2
  152. /package/{macos-helper → apple/macos-helper}/Package.swift +0 -0
  153. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/AudioProbe.swift +0 -0
  154. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +0 -0
  155. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/main.swift +0 -0
  156. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  157. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  158. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  159. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  160. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  161. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  162. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  163. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  164. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -0
  165. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +0 -0
  166. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  167. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  168. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -0
  169. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +0 -0
  170. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  171. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +0 -0
  172. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +0 -0
  173. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  174. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  175. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  176. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  177. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
  178. /package/{src/platforms/linux → linux}/atspi-dump.py +0 -0
@@ -1,5 +1,6 @@
1
1
  import XCTest
2
2
 
3
+
3
4
  extension RunnerTests {
4
5
  // MARK: - Main Thread Dispatch
5
6
 
@@ -58,7 +59,7 @@ extension RunnerTests {
58
59
 
59
60
  /// Runs a gesture action with uniform timing capture. Touch gestures pass `idleTimeout: true`
60
61
  /// (the default) to run inside the scroll idle-timeout + quiescence-skip wrapper; synthesis
61
- /// gestures (pinch/rotate/transform) pass `false` because RunnerSynthesizedGesture governs its
62
+ /// pointer-plan gestures pass `false` because RunnerSynthesizedGesture governs their
62
63
  /// own timing. Returns the captured timing and the action's outcome.
63
64
  ///
64
65
  /// NOTE: a new SYNTHESIS gesture must pass `idleTimeout: false` — the default `true` would wrap
@@ -85,7 +86,8 @@ extension RunnerTests {
85
86
  message: String,
86
87
  timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double),
87
88
  frame: GestureFrame = .none,
88
- fallback: GestureFallback? = nil
89
+ fallback: GestureFallback? = nil,
90
+ maestroNonHittableCoordinateFallbackUsed: Bool? = nil
89
91
  ) -> Response {
90
92
  let data: DataPayload
91
93
  switch frame {
@@ -96,7 +98,8 @@ extension RunnerTests {
96
98
  gestureEndUptimeMs: timing.gestureEndUptimeMs,
97
99
  gestureFallback: fallback?.strategy,
98
100
  gestureFallbackMessage: fallback?.message,
99
- gestureFallbackHint: fallback?.hint
101
+ gestureFallbackHint: fallback?.hint,
102
+ maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
100
103
  )
101
104
  case .touch(let f):
102
105
  data = DataPayload(
@@ -109,7 +112,8 @@ extension RunnerTests {
109
112
  referenceHeight: f?.referenceHeight,
110
113
  gestureFallback: fallback?.strategy,
111
114
  gestureFallbackMessage: fallback?.message,
112
- gestureFallbackHint: fallback?.hint
115
+ gestureFallbackHint: fallback?.hint,
116
+ maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
113
117
  )
114
118
  case .drag(let f):
115
119
  data = DataPayload(
@@ -130,6 +134,24 @@ extension RunnerTests {
130
134
  return Response(ok: true, data: data)
131
135
  }
132
136
 
137
+ /// Gesture plans already return canonical centroid endpoints from the portable runtime.
138
+ /// Keep runner timing/fallback diagnostics, but do not leak the coordinate-drag adapter's
139
+ /// visualization frame into only the fast-fling response shape.
140
+ private func canonicalPlannedGestureResponse(_ response: Response) -> Response {
141
+ guard response.ok, let data = response.data else { return response }
142
+ return Response(
143
+ ok: true,
144
+ data: DataPayload(
145
+ message: data.message,
146
+ gestureStartUptimeMs: data.gestureStartUptimeMs,
147
+ gestureEndUptimeMs: data.gestureEndUptimeMs,
148
+ gestureFallback: data.gestureFallback,
149
+ gestureFallbackMessage: data.gestureFallbackMessage,
150
+ gestureFallbackHint: data.gestureFallbackHint
151
+ )
152
+ )
153
+ }
154
+
133
155
 
134
156
  func execute(command: Command) throws -> Response {
135
157
  if command.command == .status {
@@ -252,31 +274,71 @@ extension RunnerTests {
252
274
  )
253
275
  }
254
276
 
255
- private func executeDispatched(command: Command) throws -> Response {
256
- // XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
257
- // block, queueing more main-thread commands behind it only buries the runner deeper.
258
- // Refuse fast instead so the daemon backs off while the abandoned work drains; past the
259
- // wedge threshold, escalate so the daemon recycles this runner (#1105).
277
+ private func runnerUnavailableResponse(command: Command) -> Response? {
260
278
  switch currentMainThreadBusyState() {
261
279
  case .idle:
262
- break
280
+ return nil
263
281
  case .busy(let abandonedForSeconds):
264
282
  return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
265
283
  case .wedged(let abandonedForSeconds):
266
284
  return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
267
285
  }
286
+ }
287
+
288
+ private func executeDispatched(command: Command) throws -> Response {
289
+ // XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
290
+ // block, queueing more main-thread commands behind it only buries the runner deeper.
291
+ // Refuse fast instead so the daemon backs off while the abandoned work drains; past the
292
+ // wedge threshold, escalate so the daemon recycles this runner (#1105).
293
+ if let unavailable = runnerUnavailableResponse(command: command) {
294
+ return unavailable
295
+ }
296
+ let alertDeadline = command.command == .alert
297
+ ? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs))
298
+ : nil
268
299
  if Thread.isMainThread {
269
- return try executeOnMainSafely(command: command)
300
+ let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
301
+ command,
302
+ deadline: alertDeadline
303
+ )
304
+ return try executeOnMainSafely(
305
+ command: command,
306
+ alertDeadline: alertDeadline,
307
+ routeToSpringboard: routeToSpringboard
308
+ )
309
+ }
310
+ // Resolve this before the command's outer main-thread block. If the bounded probe abandons
311
+ // slow XCTest enumeration, return the established recoverable response instead of queueing
312
+ // command preparation behind work that may outlive the 30-second command watchdog.
313
+ let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
314
+ command,
315
+ deadline: alertDeadline
316
+ )
317
+ if let unavailable = runnerUnavailableResponse(command: command) {
318
+ return unavailable
270
319
  }
271
320
  if command.command == .snapshot {
272
321
  return try executeSnapshotDispatched(command: command)
273
322
  }
323
+ if command.command == .alert, let deadline = alertDeadline {
324
+ return try runMainThreadWork(
325
+ command: command,
326
+ timeout: max(0.001, deadline.timeIntervalSinceNow),
327
+ timeoutError: mainThreadExecutionTimeoutError
328
+ ) {
329
+ try self.executeOnMainSafely(
330
+ command: command,
331
+ alertDeadline: deadline,
332
+ routeToSpringboard: routeToSpringboard
333
+ )
334
+ }
335
+ }
274
336
  return try runMainThreadWork(
275
337
  command: command,
276
338
  timeout: mainThreadExecutionTimeout,
277
339
  timeoutError: mainThreadExecutionTimeoutError
278
340
  ) {
279
- try self.executeOnMainSafely(command: command)
341
+ try self.executeOnMainSafely(command: command, routeToSpringboard: routeToSpringboard)
280
342
  }
281
343
  }
282
344
 
@@ -354,7 +416,11 @@ extension RunnerTests {
354
416
 
355
417
  // MARK: - Command Handling
356
418
 
357
- private func executeOnMainSafely(command: Command) throws -> Response {
419
+ private func executeOnMainSafely(
420
+ command: Command,
421
+ alertDeadline: Date? = nil,
422
+ routeToSpringboard: Bool
423
+ ) throws -> Response {
358
424
  var hasRetried = false
359
425
  while true {
360
426
  var response: Response?
@@ -362,7 +428,11 @@ extension RunnerTests {
362
428
  let failureCountBefore = currentXCTestFailureCount()
363
429
  let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
364
430
  do {
365
- response = try self.executeOnMain(command: command)
431
+ response = try self.executeOnMain(
432
+ command: command,
433
+ alertDeadline: alertDeadline,
434
+ routeToSpringboard: routeToSpringboard
435
+ )
366
436
  } catch {
367
437
  swiftError = error
368
438
  }
@@ -416,6 +486,18 @@ extension RunnerTests {
416
486
  }
417
487
 
418
488
  private func executeSnapshotDispatched(command: Command) throws -> Response {
489
+ try executeDispatchedWithRecovery(command: command) {
490
+ try self.executeSnapshotDispatchedOnce(command: command)
491
+ }
492
+ }
493
+
494
+ /// The dispatched snapshot recovery loop: read-only retry + XCTest-recorded-failure invalidation,
495
+ /// matching what `executeOnMainSafely` gives the generic path. `perform` runs the capture and its
496
+ /// own bounded main-thread work.
497
+ func executeDispatchedWithRecovery(
498
+ command: Command,
499
+ perform: () throws -> Response
500
+ ) throws -> Response {
419
501
  var hasRetried = false
420
502
  while true {
421
503
  let failureCountBefore = try runMainThreadWork(
@@ -425,7 +507,16 @@ extension RunnerTests {
425
507
  ) {
426
508
  self.currentXCTestFailureCount()
427
509
  }
428
- let response = try executeSnapshotDispatchedOnce(command: command)
510
+ let response = try perform()
511
+ // Recovered independently — re-entering main for bookkeeping would queue behind the still-
512
+ // abandoned XCTest query and re-stall the command (#1244), so skip it until that work drains.
513
+ if hasAbandonedTreeCapture() {
514
+ NSLog(
515
+ "AGENT_DEVICE_RUNNER_DISPATCH_RECOVERY_SKIPPED_XCTEST_OCCUPIED command=%@",
516
+ command.command.rawValue
517
+ )
518
+ return response
519
+ }
429
520
  let recordedFailureResponse = try runMainThreadWork(
430
521
  command: command,
431
522
  timeout: mainThreadExecutionTimeout,
@@ -471,7 +562,7 @@ extension RunnerTests {
471
562
  timeout: mainThreadExecutionTimeout,
472
563
  timeoutError: mainThreadExecutionTimeoutError
473
564
  ) {
474
- try self.prepareActiveCommandContextSafely(command: command)
565
+ try self.prepareActiveCommandContextSafely(command: command, routeToSpringboard: false)
475
566
  }
476
567
  switch preparation {
477
568
  case .response(let response):
@@ -550,10 +641,16 @@ extension RunnerTests {
550
641
  }
551
642
  }
552
643
 
553
- private func prepareActiveCommandContextSafely(command: Command) throws -> ActiveCommandPreparation {
644
+ private func prepareActiveCommandContextSafely(
645
+ command: Command,
646
+ routeToSpringboard: Bool
647
+ ) throws -> ActiveCommandPreparation {
554
648
  var preparation: ActiveCommandPreparation?
555
649
  let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
556
- preparation = self.prepareActiveCommandContext(command: command)
650
+ preparation = self.prepareActiveCommandContext(
651
+ command: command,
652
+ routeToSpringboard: routeToSpringboard
653
+ )
557
654
  })
558
655
  if let exceptionMessage {
559
656
  throw NSError(
@@ -572,8 +669,15 @@ extension RunnerTests {
572
669
  return preparation
573
670
  }
574
671
 
575
- private func executeOnMain(command: Command) throws -> Response {
576
- let preparation = prepareActiveCommandContext(command: command)
672
+ private func executeOnMain(
673
+ command: Command,
674
+ alertDeadline: Date?,
675
+ routeToSpringboard: Bool
676
+ ) throws -> Response {
677
+ let preparation = prepareActiveCommandContext(
678
+ command: command,
679
+ routeToSpringboard: routeToSpringboard
680
+ )
577
681
  let activeApp: XCUIApplication
578
682
  switch preparation {
579
683
  case .response(let response):
@@ -585,6 +689,8 @@ extension RunnerTests {
585
689
  switch command.command {
586
690
  case .status:
587
691
  return executeStatus(command: command)
692
+ case .targetReset:
693
+ return resetTargetAfterExternalRelaunch()
588
694
  case .shutdown:
589
695
  stopRecordingIfNeeded()
590
696
  return Response(ok: true, data: DataPayload(message: "shutdown"))
@@ -652,12 +758,21 @@ extension RunnerTests {
652
758
  default:
653
759
  break
654
760
  }
655
- return try executeOnMainPrepared(command: command, activeApp: activeApp)
761
+ return try executeOnMainPrepared(
762
+ command: command,
763
+ activeApp: activeApp,
764
+ alertDeadline: alertDeadline
765
+ )
656
766
  }
657
767
 
658
- private func prepareActiveCommandContext(command: Command) -> ActiveCommandPreparation {
768
+ private func prepareActiveCommandContext(
769
+ command: Command,
770
+ routeToSpringboard: Bool = false
771
+ ) -> ActiveCommandPreparation {
659
772
  var activeApp = currentApp ?? app
660
- if shouldSkipAppActivationPreflight(command) {
773
+ if routeToSpringboard {
774
+ activeApp = springboard
775
+ } else if shouldSkipAppActivationPreflight(command) {
661
776
  activeApp = resolveAppWithoutActivation(command: command)
662
777
  } else if !isRunnerLifecycleCommand(command.command) {
663
778
  let normalizedBundleId = command.appBundleId?
@@ -666,11 +781,12 @@ extension RunnerTests {
666
781
  if let bundleId = requestedBundleId {
667
782
  if currentBundleId != bundleId || currentApp == nil {
668
783
  _ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
784
+ } else {
785
+ refreshCachedTargetIfProcessChanged(bundleId: bundleId)
669
786
  }
670
787
  } else {
671
788
  // Do not reuse stale bundle targets when the caller does not explicitly request one.
672
- currentApp = nil
673
- currentBundleId = nil
789
+ invalidateCachedTarget(reason: "missing_app_bundle")
674
790
  }
675
791
 
676
792
  activeApp = currentApp ?? app
@@ -721,10 +837,14 @@ extension RunnerTests {
721
837
  return .context(ActiveCommandContext(app: activeApp))
722
838
  }
723
839
 
724
- private func executeOnMainPrepared(command: Command, activeApp: XCUIApplication) throws -> Response {
840
+ private func executeOnMainPrepared(
841
+ command: Command,
842
+ activeApp: XCUIApplication,
843
+ alertDeadline: Date? = nil
844
+ ) throws -> Response {
725
845
  var activeApp = activeApp
726
846
  switch command.command {
727
- case .status, .shutdown, .recordStart, .recordStop, .uptime:
847
+ case .status, .targetReset, .shutdown, .recordStart, .recordStop, .uptime:
728
848
  return Response(
729
849
  ok: false,
730
850
  error: ErrorPayload(
@@ -734,11 +854,21 @@ extension RunnerTests {
734
854
  )
735
855
  case .tap:
736
856
  if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
857
+ let expectedPoint: CGPoint?
858
+ if command.allowNonHittableCoordinateFallback == true,
859
+ let x = command.x,
860
+ let y = command.y
861
+ {
862
+ expectedPoint = CGPoint(x: x, y: y)
863
+ } else {
864
+ expectedPoint = nil
865
+ }
737
866
  let match = findElement(
738
867
  app: activeApp,
739
868
  selectorKey: selectorKey,
740
869
  selectorValue: selectorValue,
741
- allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
870
+ allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true,
871
+ expectedPoint: expectedPoint
742
872
  )
743
873
  if match.isAmbiguous {
744
874
  return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
@@ -764,15 +894,19 @@ extension RunnerTests {
764
894
  message: "element resolved off-screen at (\(Int(frame.midX)), \(Int(frame.midY)))"))
765
895
  }
766
896
  let isTextEntry = isTextEntryElement(element)
767
- let touchFrame = frame.isEmpty
768
- ? nil
769
- : resolvedTouchVisualizationFrame(app: activeApp, x: frame.midX, y: frame.midY)
897
+ let touchPoint = expectedPoint ?? CGPoint(x: frame.midX, y: frame.midY)
898
+ let touchFrame = resolvedTouchVisualizationFrame(
899
+ app: activeApp,
900
+ x: touchPoint.x,
901
+ y: touchPoint.y
902
+ )
770
903
  let (timing, outcome) = performGesture(activeApp) {
771
- if match.usedNonHittableFallback {
904
+ if expectedPoint != nil || match.usedNonHittableFallback {
772
905
  // Maestro compatibility: RN E2E backdoor controls can be 1x1 and
773
- // reported non-hittable by XCTest, while Maestro still taps their
774
- // resolved bounds. Keep this behind the explicit replay-only flag.
775
- return tapAt(app: activeApp, x: frame.midX, y: frame.midY)
906
+ // reported non-hittable by XCTest. Snapshot-resolved targets also
907
+ // need coordinate delivery because XCUIElement.activate() does not
908
+ // invoke every React Native accessibility wrapper.
909
+ return tapAt(app: activeApp, x: touchPoint.x, y: touchPoint.y)
776
910
  }
777
911
  return activateElement(app: activeApp, element: element, action: "tap by selector")
778
912
  }
@@ -785,7 +919,11 @@ extension RunnerTests {
785
919
  return gestureResponse(
786
920
  message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
787
921
  timing: timing,
788
- frame: .touch(touchFrame)
922
+ frame: .touch(touchFrame),
923
+ maestroNonHittableCoordinateFallbackUsed:
924
+ command.allowNonHittableCoordinateFallback == true
925
+ ? match.usedNonHittableFallback
926
+ : nil
789
927
  )
790
928
  }
791
929
  return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
@@ -1146,7 +1284,9 @@ extension RunnerTests {
1146
1284
  ok: false,
1147
1285
  error: ErrorPayload(
1148
1286
  code: "UNSUPPORTED_OPERATION",
1149
- message: "Unable to dismiss the iOS keyboard without a safe native dismiss control"
1287
+ message: "Unable to dismiss the iOS keyboard without a safe native dismiss control",
1288
+ hint:
1289
+ "The on-screen keyboard usually does not block agent-device interactions: press the next target directly instead of retrying dismiss. If that press fails or reports no visible effect, scroll the target into view, or use keyboard enter to press the return key when submission is wanted."
1150
1290
  )
1151
1291
  )
1152
1292
  }
@@ -1180,83 +1320,60 @@ extension RunnerTests {
1180
1320
  )
1181
1321
  case .alert:
1182
1322
  let action = (command.action ?? "get").lowercased()
1183
- guard let alert = resolveAlert(app: activeApp) else {
1323
+ let deadline = alertDeadline ?? Date().addingTimeInterval(
1324
+ Self.alertCommandTimeout(timeoutMs: command.timeoutMs)
1325
+ )
1326
+ guard let alert = resolveAlert(app: activeApp, deadline: deadline) else {
1184
1327
  return Response(ok: false, error: ErrorPayload(message: "alert not found"))
1185
1328
  }
1186
- return handleAlert(alert, action: action)
1187
- case .pinch:
1188
- guard let scale = command.scale, scale > 0 else {
1189
- return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
1190
- }
1191
- let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
1192
- pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
1193
- }
1194
- if let response = unsupportedResponse(for: outcome) {
1195
- return response
1196
- }
1197
- return gestureResponse(message: "pinched", timing: timing)
1198
- case .sequence:
1199
- return executeSequence(command: command, activeApp: activeApp)
1200
- case .rotateGesture:
1201
- guard let degrees = command.degrees, degrees.isFinite else {
1202
- return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
1203
- }
1204
- let velocity = command.velocity ?? (degrees >= 0 ? 1.0 : -1.0)
1205
- guard velocity.isFinite && velocity != 0 else {
1206
- return Response(ok: false, error: ErrorPayload(message: "rotateGesture velocity must be non-zero"))
1207
- }
1208
- let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
1209
- rotateGesture(
1210
- app: activeApp,
1211
- degrees: degrees,
1212
- x: command.x,
1213
- y: command.y,
1214
- velocity: velocity
1329
+ return handleAlert(alert, action: action, deadline: deadline)
1330
+ case .gesture:
1331
+ guard let plan = command.gesturePlan else {
1332
+ return Response(
1333
+ ok: false,
1334
+ error: ErrorPayload(code: "INVALID_ARGS", message: "gesture requires gesturePlan")
1215
1335
  )
1216
1336
  }
1217
- if let response = unsupportedResponse(for: outcome) {
1218
- return response
1219
- }
1220
- return gestureResponse(message: "rotatedGesture", timing: timing)
1221
- case .transformGesture:
1222
- guard
1223
- let x = command.x,
1224
- let y = command.y,
1225
- let dx = command.dx,
1226
- let dy = command.dy,
1227
- x.isFinite,
1228
- y.isFinite,
1229
- dx.isFinite,
1230
- dy.isFinite
1231
- else {
1232
- return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite x y dx dy"))
1233
- }
1234
- guard let scale = command.scale, scale.isFinite, scale > 0 else {
1235
- return Response(ok: false, error: ErrorPayload(message: "transformGesture requires scale > 0"))
1236
- }
1237
- guard let degrees = command.degrees, degrees.isFinite else {
1238
- return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite degrees"))
1337
+ if let validationError = plannedGestureValidationError(plan) {
1338
+ return Response(
1339
+ ok: false,
1340
+ error: ErrorPayload(code: "INVALID_ARGS", message: validationError)
1341
+ )
1239
1342
  }
1240
- let durationMs = command.durationMs ?? 300
1241
- guard durationMs.isFinite && durationMs >= 16 else {
1242
- return Response(ok: false, error: ErrorPayload(message: "transformGesture durationMs must be >= 16"))
1343
+ if plannedGestureExecution(for: plan) == .fastSwipe {
1344
+ // Validation above guarantees a non-empty, single-pointer path for this execution kind.
1345
+ let first = plan.pointers[0].samples.first!.point
1346
+ let last = plan.pointers[0].samples.last!.point
1347
+ return canonicalPlannedGestureResponse(
1348
+ executeDragGesture(
1349
+ activeApp: activeApp,
1350
+ x: first.x,
1351
+ y: first.y,
1352
+ x2: last.x,
1353
+ y2: last.y,
1354
+ durationMs: plan.durationMs,
1355
+ synthesized: true,
1356
+ message: plan.intent,
1357
+ synthesizedPolicyKind: .synthesizedDrag,
1358
+ synthesizedProfile: .fastSwipe
1359
+ )
1360
+ )
1243
1361
  }
1244
1362
  let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
1245
- transformGesture(
1246
- app: activeApp,
1247
- x: x,
1248
- y: y,
1249
- dx: dx,
1250
- dy: dy,
1251
- scale: scale,
1252
- degrees: degrees,
1253
- durationMs: durationMs
1254
- )
1363
+ sampledPlannedGesture(app: activeApp, plan: plan)
1255
1364
  }
1256
1365
  if let response = unsupportedResponse(for: outcome) {
1257
1366
  return response
1258
1367
  }
1259
- return gestureResponse(message: "transformedGesture", timing: timing)
1368
+ return gestureResponse(message: plan.intent, timing: timing)
1369
+ case .gestureViewport:
1370
+ let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
1371
+ guard !frame.isNull, !frame.isInfinite, !frame.isEmpty else {
1372
+ return Response(ok: false, error: ErrorPayload(code: "COMMAND_FAILED", message: "Active app interaction viewport is unavailable"))
1373
+ }
1374
+ return Response(ok: true, data: DataPayload(message: "gestureViewport", x: frame.minX, y: frame.minY, x2: frame.width, y2: frame.height))
1375
+ case .sequence:
1376
+ return executeSequence(command: command, activeApp: activeApp)
1260
1377
  }
1261
1378
  }
1262
1379
 
@@ -1273,7 +1390,8 @@ extension RunnerTests {
1273
1390
  synthesized: Bool,
1274
1391
  message: String,
1275
1392
  synthesizedContext: SynthesizedCoordinateContext? = nil,
1276
- synthesizedPolicyKind: SynthesizedGesturePolicyKind
1393
+ synthesizedPolicyKind: SynthesizedGesturePolicyKind,
1394
+ synthesizedProfile: SynthesizedDragProfile = .continuous
1277
1395
  ) -> Response {
1278
1396
  let commandName = dragCommandName(message: message)
1279
1397
  guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
@@ -1291,7 +1409,8 @@ extension RunnerTests {
1291
1409
  durationMs: durationMs,
1292
1410
  message: message,
1293
1411
  context: synthesizedContext,
1294
- policyKind: synthesizedPolicyKind
1412
+ policyKind: synthesizedPolicyKind,
1413
+ profile: synthesizedProfile
1295
1414
  ) {
1296
1415
  return synthesizedResponse
1297
1416
  }
@@ -1315,6 +1434,7 @@ extension RunnerTests {
1315
1434
  x2: dragPoints.x2,
1316
1435
  y2: dragPoints.y2,
1317
1436
  durationMs: durationMs,
1437
+ profile: synthesizedProfile,
1318
1438
  context: context
1319
1439
  )
1320
1440
  }
@@ -1356,7 +1476,8 @@ extension RunnerTests {
1356
1476
  durationMs: Double?,
1357
1477
  message: String,
1358
1478
  context: SynthesizedCoordinateContext?,
1359
- policyKind: SynthesizedGesturePolicyKind
1479
+ policyKind: SynthesizedGesturePolicyKind,
1480
+ profile: SynthesizedDragProfile
1360
1481
  ) -> Response? {
1361
1482
  #if os(iOS)
1362
1483
  let policy = synthesizedGesturePolicy(policyKind)
@@ -1408,6 +1529,7 @@ extension RunnerTests {
1408
1529
  x2: plan.points.x2,
1409
1530
  y2: plan.points.y2,
1410
1531
  durationMs: durationMs,
1532
+ profile: profile,
1411
1533
  context: plan.context
1412
1534
  )
1413
1535
  }
@@ -1527,23 +1649,58 @@ extension RunnerTests {
1527
1649
 
1528
1650
  private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
1529
1651
  #if os(iOS)
1530
- // Coordinate-only synthesized taps can run after an AX-fatal screen because they do not
1652
+ if command.command == .alert {
1653
+ return true
1654
+ }
1655
+ // Coordinate-only synthesized taps can run after an AX-fatal foreground screen because they do not
1531
1656
  // need app activation, window lookup, keyboard lookup, or element resolution. Selector/text
1532
1657
  // interactions intentionally stay on the normal AX path because they need an element query.
1533
1658
  // Scroll/drag/sequence keep the normal foreground guard and stabilization path.
1534
1659
  guard command.text == nil, command.selectorKey == nil else { return false }
1535
1660
  guard hasCachedTargetForActivationSkip(command: command) else { return false }
1536
- return command.command == .tap
1537
- && command.synthesized == true
1538
- && command.x != nil
1539
- && command.y != nil
1661
+ return isCoordinateOnlyTap(command)
1662
+ #else
1663
+ return false
1664
+ #endif
1665
+ }
1666
+
1667
+ private func shouldRouteToSpringboardBlockingSystemModal(
1668
+ _ command: Command,
1669
+ deadline: Date? = nil
1670
+ ) -> Bool {
1671
+ #if os(iOS)
1672
+ guard command.command == .alert || isCoordinateOnlyTap(command) else {
1673
+ return false
1674
+ }
1675
+ let budgetDeadline = Date().addingTimeInterval(systemModalProbeBudget)
1676
+ let probeDeadline = deadline.map { min($0, budgetDeadline) } ?? budgetDeadline
1677
+ // `runMainThreadWork` executes inline for a main-thread caller, so that path cannot use its
1678
+ // timeout machinery. Direct main-thread dispatch keeps the prior synchronous modal check;
1679
+ // normal off-main command dispatch uses the bounded probe and post-probe busy recovery.
1680
+ if Thread.isMainThread {
1681
+ return firstBlockingSystemModal(
1682
+ in: springboard,
1683
+ deadline: probeDeadline
1684
+ ) != nil
1685
+ }
1686
+ return boundedBlockingSystemAlertSnapshot(
1687
+ deadline: probeDeadline
1688
+ ) != nil
1540
1689
  #else
1541
1690
  return false
1542
1691
  #endif
1543
1692
  }
1544
1693
 
1694
+ private func isCoordinateOnlyTap(_ command: Command) -> Bool {
1695
+ return command.command == .tap
1696
+ && command.text == nil
1697
+ && command.selectorKey == nil
1698
+ && command.x != nil
1699
+ && command.y != nil
1700
+ }
1701
+
1545
1702
  private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
1546
- guard currentApp != nil else { return false }
1703
+ guard let currentApp, currentApp.state == .runningForeground else { return false }
1547
1704
  guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
1548
1705
  !bundleId.isEmpty
1549
1706
  else {
@@ -1572,6 +1729,7 @@ extension RunnerTests {
1572
1729
  let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
1573
1730
  let textEntryMode = resolveTextEntryMode(command)
1574
1731
  let target: TextEntryTarget
1732
+ var maestroNonHittableCoordinateFallbackUsed: Bool?
1575
1733
  let focusStartedAt = Date()
1576
1734
  if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
1577
1735
  let match = findElement(
@@ -1589,6 +1747,9 @@ extension RunnerTests {
1589
1747
  guard isTextEntryElement(element) else {
1590
1748
  return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input"))
1591
1749
  }
1750
+ if command.allowNonHittableCoordinateFallback == true {
1751
+ maestroNonHittableCoordinateFallbackUsed = match.usedNonHittableFallback
1752
+ }
1592
1753
  target = focusTextInputForTextEntry(app: activeApp, element: element)
1593
1754
  } else {
1594
1755
  target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
@@ -1637,7 +1798,8 @@ extension RunnerTests {
1637
1798
  x: point.map { Double($0.x) },
1638
1799
  y: point.map { Double($0.y) },
1639
1800
  referenceWidth: frame.isEmpty ? nil : Double(frame.width),
1640
- referenceHeight: frame.isEmpty ? nil : Double(frame.height)
1801
+ referenceHeight: frame.isEmpty ? nil : Double(frame.height),
1802
+ maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
1641
1803
  )
1642
1804
  )
1643
1805
  }
@@ -146,8 +146,8 @@ final class RunnerCommandJournal {
146
146
  case .tap, .mouseClick, .longPress, .drag,
147
147
  .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
148
148
  .backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
149
- .alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
150
- .status, .uptime, .shutdown:
149
+ .alert, .sequence, .gesture, .gestureViewport, .recordStart, .recordStop,
150
+ .status, .uptime, .targetReset, .shutdown:
151
151
  return true
152
152
  }
153
153
  }