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
@@ -63,6 +63,46 @@ extension RunnerTests {
63
63
  static let regularVisiblePlan: [SnapshotBackendKind] = [.recursiveTree, .querySweep, .privateAX]
64
64
  static let rawDiagnosticPlan: [SnapshotBackendKind] = [.recursiveTree, .privateAX]
65
65
 
66
+ // MARK: Tree-backend penalty (cross-attempt memory, #1105)
67
+ //
68
+ // On some deep/dynamic screens the XCTest bulk snapshot no longer fails fast with
69
+ // kAXErrorIllegalArgument (the #758 signature) — it grinds for many seconds first. One slow
70
+ // grind is tolerable; re-grinding on every subsequent capture of the same screen buries the
71
+ // main thread past the execution watchdog. After a slow or abandoned tree capture, later
72
+ // plans for the same bundle lead with the private AX backend until the penalty expires.
73
+
74
+ func penalizeSnapshotTreeBackend(bundleId: String?, reason: String) {
75
+ snapshotTreePenaltyLock.lock()
76
+ snapshotTreePenaltyBundleId = bundleId
77
+ snapshotTreePenaltyUntil = Date().addingTimeInterval(snapshotTreePenaltyDuration)
78
+ snapshotTreePenaltyLock.unlock()
79
+ NSLog(
80
+ "AGENT_DEVICE_RUNNER_SNAPSHOT_TREE_PENALIZED bundle=%@ reason=%@",
81
+ bundleId ?? "",
82
+ reason
83
+ )
84
+ }
85
+
86
+ func isSnapshotTreeBackendPenalized(bundleId: String?) -> Bool {
87
+ snapshotTreePenaltyLock.lock()
88
+ defer { snapshotTreePenaltyLock.unlock() }
89
+ guard Date() < snapshotTreePenaltyUntil else { return false }
90
+ // A penalty recorded without a bundle id applies to whatever target is current.
91
+ guard let penalized = snapshotTreePenaltyBundleId else { return true }
92
+ return penalized == bundleId
93
+ }
94
+
95
+ /// Pure plan-reorder rule: a penalized tree backend defers to the private AX backend on the
96
+ /// regular plan only. The raw diagnostic plan keeps tree-first errors, and unknown plans are
97
+ /// left untouched.
98
+ static func effectiveSnapshotCapturePlan(
99
+ _ plan: [SnapshotBackendKind],
100
+ treePenalized: Bool
101
+ ) -> (plan: [SnapshotBackendKind], treeDeferred: Bool) {
102
+ guard treePenalized, plan == Self.regularVisiblePlan else { return (plan, false) }
103
+ return ([.privateAX, .querySweep, .recursiveTree], true)
104
+ }
105
+
66
106
  // MARK: Plan runner
67
107
 
68
108
  func runSnapshotCapturePlan(
@@ -76,21 +116,56 @@ extension RunnerTests {
76
116
  var axFailure: SnapshotCaptureFailure?
77
117
  let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
78
118
 
79
- for kind in plan {
80
- if kind != plan.first && Date() >= deadline {
119
+ // Reorder is gated to iOS simulators — the only place the private AX backend exists, so
120
+ // the deferred plan cannot degrade platforms where the tree backend is the strongest tier.
121
+ var treePenalized = false
122
+ #if os(iOS) && targetEnvironment(simulator)
123
+ treePenalized = isSnapshotTreeBackendPenalized(bundleId: currentBundleId)
124
+ #endif
125
+ let (effectivePlan, treeDeferred) = Self.effectiveSnapshotCapturePlan(
126
+ plan,
127
+ treePenalized: treePenalized
128
+ )
129
+ if treeDeferred {
130
+ firstFailure = (
131
+ "the XCTest tree backend was deferred after a recent slow capture on this screen",
132
+ "budget"
133
+ )
134
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_TREE_DEFERRED bundle=%@", currentBundleId ?? "")
135
+ }
136
+
137
+ for kind in effectivePlan {
138
+ if kind != effectivePlan.first && Date() >= deadline {
81
139
  NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_PLAN_BUDGET_EXHAUSTED skipped=%@", kind.rawValue)
82
140
  if firstFailure == nil {
83
141
  firstFailure = ("the capture plan ran out of its time budget", "budget")
84
142
  }
85
143
  break
86
144
  }
145
+ // While an abandoned tree capture is still grinding inside testmanagerd, XCTest-backed
146
+ // tiers would block behind it; only the private AX backend stays responsive (#1105).
147
+ if kind != .privateAX, hasAbandonedTreeCapture() {
148
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_TIER_SKIPPED_XCTEST_OCCUPIED tier=%@", kind.rawValue)
149
+ if firstFailure == nil {
150
+ firstFailure = (
151
+ "the XCTest capture channel is occupied by an abandoned tree capture",
152
+ "budget"
153
+ )
154
+ }
155
+ continue
156
+ }
87
157
  let capture: SnapshotBackendCapture
158
+ let backendStartedAt = Date()
88
159
  do {
89
- guard let result = try captureWithBackend(kind, app: app, options: options) else {
160
+ guard let result = try captureWithBackend(kind, app: app, options: options, deadline: deadline)
161
+ else {
162
+ recordSlowTreeBackendIfNeeded(kind, startedAt: backendStartedAt)
90
163
  continue
91
164
  }
92
165
  capture = result
166
+ recordSlowTreeBackendIfNeeded(kind, startedAt: backendStartedAt)
93
167
  } catch let failure as SnapshotCaptureFailure {
168
+ recordSlowTreeBackendIfNeeded(kind, startedAt: backendStartedAt)
94
169
  if Self.isAxSnapshotFailure(failure) { axFailure = failure }
95
170
  if firstFailure == nil {
96
171
  firstFailure = (failure.message, Self.isAxSnapshotFailure(failure) ? "ax-rejected" : "capture-failed")
@@ -111,7 +186,7 @@ extension RunnerTests {
111
186
  continue
112
187
  }
113
188
 
114
- let recovered = kind != plan.first
189
+ let recovered = kind != effectivePlan.first || treeDeferred
115
190
  if recovered {
116
191
  NSLog(
117
192
  "AGENT_DEVICE_RUNNER_SNAPSHOT_RECOVERED backend=%@ reason=%@",
@@ -157,14 +232,33 @@ extension RunnerTests {
157
232
  return fallbackPayload
158
233
  }
159
234
 
235
+ /// Marks the tree backend as penalized when a capture attempt ground past the slow-capture
236
+ /// threshold — even a successful one: the next capture of this screen must not re-grind.
237
+ private func recordSlowTreeBackendIfNeeded(_ kind: SnapshotBackendKind, startedAt: Date) {
238
+ guard kind == .recursiveTree else { return }
239
+ let elapsed = Date().timeIntervalSince(startedAt)
240
+ guard elapsed > snapshotTreeSlowCaptureThreshold else { return }
241
+ penalizeSnapshotTreeBackend(
242
+ bundleId: currentBundleId,
243
+ reason: "slow_tree_capture_\(Int(elapsed * 1000))ms"
244
+ )
245
+ }
246
+
160
247
  private func captureWithBackend(
161
248
  _ kind: SnapshotBackendKind,
162
249
  app: XCUIApplication,
163
- options: SnapshotOptions
250
+ options: SnapshotOptions,
251
+ deadline: Date
164
252
  ) throws -> SnapshotBackendCapture? {
165
253
  switch kind {
166
254
  case .recursiveTree:
167
- guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
255
+ guard
256
+ let context = try makeSnapshotTraversalContext(
257
+ app: app,
258
+ options: options,
259
+ captureDeadline: deadline
260
+ )
261
+ else {
168
262
  return nil
169
263
  }
170
264
  let payload = options.raw
@@ -173,11 +267,11 @@ extension RunnerTests {
173
267
  return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
174
268
  case .querySweep:
175
269
  return SnapshotBackendCapture(
176
- payload: snapshotFlatInteractive(app: app, options: options),
270
+ payload: snapshotFlatInteractive(app: app, options: options, planDeadline: deadline),
177
271
  effectiveDepth: nil
178
272
  )
179
273
  case .privateAX:
180
- return privateAXSnapshotCapture(app: app, options: options)
274
+ return privateAXSnapshotCapture(app: app, options: options, deadline: deadline)
181
275
  }
182
276
  }
183
277
 
@@ -304,124 +398,3 @@ extension RunnerTests {
304
398
  }
305
399
  }
306
400
 
307
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
308
- // MARK: - In-bundle unit tests
309
-
310
- extension RunnerTests {
311
- private func planTestNode(
312
- index: Int,
313
- type: String,
314
- label: String? = nil,
315
- identifier: String? = nil,
316
- hittable: Bool = false,
317
- parentIndex: Int? = nil
318
- ) -> SnapshotNode {
319
- SnapshotNode(
320
- index: index,
321
- type: type,
322
- label: label,
323
- identifier: identifier,
324
- value: nil,
325
- rect: snapshotRect(from: .zero),
326
- enabled: true,
327
- focused: nil,
328
- selected: nil,
329
- hittable: hittable,
330
- depth: parentIndex == nil ? 0 : 1,
331
- parentIndex: parentIndex,
332
- hiddenContentAbove: nil,
333
- hiddenContentBelow: nil
334
- )
335
- }
336
-
337
- func testSparsePayloadReasonMatrix() {
338
- let root = planTestNode(index: 0, type: "Application", label: "Example App", hittable: true)
339
- let window = planTestNode(index: 1, type: "Window", parentIndex: 0)
340
- let button = planTestNode(index: 1, type: "Button", label: "Ok", hittable: true, parentIndex: 0)
341
-
342
- // Labeled, hittable root over a bare window is still sparse.
343
- XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [root, window], truncated: false)))
344
- // Deadline-truncated near-empty sweep needs recovery even with one real control.
345
- XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [root, button], truncated: true)))
346
- // The same tiny tree from a completed sweep is a legitimately minimal screen.
347
- XCTAssertNil(Self.sparsePayloadReason(DataPayload(nodes: [root, button], truncated: false)))
348
- // Empty payloads are degraded.
349
- XCTAssertNotNil(Self.sparsePayloadReason(DataPayload(nodes: [], truncated: false)))
350
- }
351
-
352
- func testCollapsedLeafIndexesFlagsMergedContainersOnly() {
353
- let root = planTestNode(index: 0, type: "Application", label: "App")
354
- let merged = planTestNode(
355
- index: 1,
356
- type: "Other",
357
- label: (0...30).map { "Row \($0), Tap" }.joined(separator: ", "),
358
- parentIndex: 0
359
- )
360
- let prose = planTestNode(
361
- index: 2,
362
- type: "StaticText",
363
- label: (0...30).map { "clause \($0)" }.joined(separator: ", "),
364
- parentIndex: 0
365
- )
366
- XCTAssertEqual(Self.collapsedLeafIndexes([root, merged, prose]), [1])
367
- XCTAssertNil(Self.collapsedLeafIndexes([root, prose]))
368
- }
369
-
370
- func testLegacyQualityMessageStatesFallbackMeaning() {
371
- let recovered = SnapshotQuality(
372
- state: "recovered",
373
- backend: "queries",
374
- reason: "snapshot returned only structural application/window nodes",
375
- reasonCode: "sparse-tree",
376
- effectiveDepth: nil,
377
- collapsedLeafIndexes: nil
378
- )
379
- let message = Self.legacyQualityMessage(recovered)
380
- XCTAssertTrue(message?.contains("queries accessibility backend") == true)
381
- XCTAssertTrue(message?.contains("fixing the app's accessibility") == true)
382
- XCTAssertTrue(message?.contains("screenshot as visual truth") == true)
383
- XCTAssertNil(
384
- Self.legacyQualityMessage(
385
- SnapshotQuality(
386
- state: "healthy", backend: "tree", reason: nil, reasonCode: nil, effectiveDepth: nil,
387
- collapsedLeafIndexes: nil)
388
- )
389
- )
390
- }
391
- func testTerminalFailsClosedOnInteractiveAxFailureRegardlessOfSparseBest() {
392
- // Interactive AX failure must invalidate + fail closed; a later tier's sparse synthetic-root
393
- // "best" must never downgrade this to a returned-sparse payload (regression: best == nil guard).
394
- XCTAssertEqual(
395
- Self.resolveSnapshotPlanTerminal(terminal: .sparseWithFatalOnAXFailure, interactiveOnly: true),
396
- .failClosed
397
- )
398
- XCTAssertEqual(
399
- Self.resolveSnapshotPlanTerminal(terminal: .sparseWithFatalOnAXFailure, interactiveOnly: false),
400
- .sparseBest
401
- )
402
- XCTAssertEqual(
403
- Self.resolveSnapshotPlanTerminal(terminal: .throwOnAXFailure, interactiveOnly: true),
404
- .throwAxFailure
405
- )
406
- }
407
-
408
- func testSnapshotAccessibilityUnavailableCarriesSparseVerdict() {
409
- currentApp = app
410
- currentBundleId = "com.example.app"
411
- defer {
412
- currentApp = nil
413
- currentBundleId = nil
414
- }
415
- let payload = snapshotAccessibilityUnavailable(
416
- failure: SnapshotCaptureFailure(
417
- code: "IOS_AX_SNAPSHOT_FAILED",
418
- message: "kAXErrorIllegalArgument",
419
- hint: "use screenshot"
420
- )
421
- )
422
- XCTAssertEqual(payload.runnerFatal, true)
423
- XCTAssertEqual(payload.snapshotQuality?.state, "sparse")
424
- XCTAssertEqual(payload.snapshotQuality?.reasonCode, "ax-rejected")
425
- }
426
- }
427
- #endif