agent-device 0.19.3 → 0.20.1

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 (179) hide show
  1. package/README.md +62 -70
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.apk +0 -0
  3. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.apk.sha256 +1 -0
  4. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.manifest.json +10 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.apk +0 -0
  6. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.apk.sha256 +1 -0
  7. package/{android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.manifest.json → android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.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 +5 -10
  11. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +74 -174
  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 +325 -123
  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 +149 -105
  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 +51 -26
  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 -107
  22. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +130 -13
  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+TextEntry.swift +1 -1
  28. package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +16 -1
  29. package/dist/src/agent-browser-tool.js +1 -1
  30. package/dist/src/agent-device-client.js +3 -0
  31. package/dist/src/android-adb.d.ts +1 -1
  32. package/dist/src/android-adb.js +1 -1
  33. package/dist/src/android-input-method-overlays.js +1 -0
  34. package/dist/src/android-input-ownership.js +1 -6
  35. package/dist/src/android-system-chrome.js +1 -0
  36. package/dist/src/android.js +1 -1
  37. package/dist/src/app-lifecycle.js +5 -5
  38. package/dist/src/app-log-request-scope.js +1 -0
  39. package/dist/src/app-resolution.js +1 -0
  40. package/dist/src/apple-multitouch-support.js +1 -0
  41. package/dist/src/apps.js +5 -5
  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 +131 -40
  45. package/dist/src/cli-runner.js +1 -1
  46. package/dist/src/cli.js +15 -11
  47. package/dist/src/command-metadata.js +1 -1
  48. package/dist/src/command-schema.js +3 -3
  49. package/dist/src/compatibility-policy.js +1 -0
  50. package/dist/src/context.js +1 -0
  51. package/dist/src/contracts.d.ts +2 -2
  52. package/dist/src/debug-symbols.js +1 -1
  53. package/dist/src/device-input-state.js +1 -1
  54. package/dist/src/devices2.js +1 -1
  55. package/dist/src/devices4.js +1 -0
  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 +533 -279
  61. package/dist/src/index.js +1 -1
  62. package/dist/src/input-actions.js +4 -3
  63. package/dist/src/install-artifact.js +1 -0
  64. package/dist/src/install-source.d.ts +5 -1
  65. package/dist/src/install-source.js +1 -1
  66. package/dist/src/install-source2.js +1 -1
  67. package/dist/src/interaction-snapshot.js +1 -1
  68. package/dist/src/interaction.d.ts +230 -0
  69. package/dist/src/interaction.js +1 -1
  70. package/dist/src/interactor.js +2 -2
  71. package/dist/src/interactor2.js +1 -0
  72. package/dist/src/internal/bin.js +1 -1
  73. package/dist/src/internal/daemon.js +42 -20
  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/method-scope.js +1 -0
  82. package/dist/src/metro-types.d.ts +52 -0
  83. package/dist/src/metro.d.ts +3 -3
  84. package/dist/src/notifications.js +1 -1
  85. package/dist/src/png-worker-contract.js +1 -1
  86. package/dist/src/process-lock.js +1 -0
  87. package/dist/src/react-native.js +1 -1
  88. package/dist/src/record-trace.js +1 -1
  89. package/dist/src/recording-export-quality.js +15 -0
  90. package/dist/src/register-builtins.js +1 -0
  91. package/dist/src/registry.js +24 -24
  92. package/dist/src/remote-config.d.ts +1 -1
  93. package/dist/src/reporting.js +8 -7
  94. package/dist/src/runner-client.js +1 -1
  95. package/dist/src/runner-disposal.js +2 -0
  96. package/dist/src/runner-lease.js +1 -0
  97. package/dist/src/runner-provider.js +1 -1
  98. package/dist/src/runner-sequence.js +1 -1
  99. package/dist/src/runtime.js +3 -3
  100. package/dist/src/runtime2.js +1 -0
  101. package/dist/src/sdk-android-adb.d.ts +97 -33
  102. package/dist/src/sdk-android-adb.js +4 -4
  103. package/dist/src/sdk-batch-runner.d.ts +2 -1
  104. package/dist/src/sdk-batch-runner.js +3 -2
  105. package/dist/src/sdk-contracts.d.ts +70 -149
  106. package/dist/src/sdk-contracts.js +1 -1
  107. package/dist/src/sdk-device.js +1 -1
  108. package/dist/src/sdk-finders.d.ts +5 -4
  109. package/dist/src/sdk-finders.js +1 -1
  110. package/dist/src/sdk-io.d.ts +1 -1
  111. package/dist/src/sdk-remote-config.d.ts +19 -96
  112. package/dist/src/sdk-remote-config.js +1 -1
  113. package/dist/src/sdk-selectors.js +1 -1
  114. package/dist/src/selector-runtime.js +1 -1
  115. package/dist/src/selector-vocabulary.js +1 -0
  116. package/dist/src/selectors.d.ts +12 -3
  117. package/dist/src/selectors.js +1 -1
  118. package/dist/src/server.js +3 -3
  119. package/dist/src/session-target-evidence.js +1 -0
  120. package/dist/src/session.js +8 -10
  121. package/dist/src/simctl.js +1 -1
  122. package/dist/src/simulator.js +1 -1
  123. package/dist/src/snapshot-diagnostics.js +5 -2
  124. package/dist/src/snapshot-quality.js +1 -1
  125. package/dist/src/snapshot.js +1 -1
  126. package/dist/src/snapshot2.js +1 -2
  127. package/dist/src/tool-provider3.js +1 -0
  128. package/dist/src/unsupported-interactor.js +1 -0
  129. package/dist/src/url.js +1 -1
  130. package/dist/src/web.js +1 -1
  131. package/package.json +34 -27
  132. package/server.json +2 -2
  133. package/skills/agent-device/SKILL.md +5 -3
  134. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk +0 -0
  135. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk.sha256 +0 -1
  136. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.manifest.json +0 -10
  137. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk +0 -0
  138. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk.sha256 +0 -1
  139. package/dist/src/apple-runner-options.js +0 -1
  140. package/dist/src/cli-output.js +0 -1
  141. package/dist/src/client.js +0 -3
  142. package/dist/src/handler-utils.js +0 -1
  143. package/dist/src/output-path.js +0 -1
  144. package/dist/src/points.js +0 -4
  145. package/dist/src/provider.js +0 -1
  146. package/dist/src/providers.js +0 -1
  147. package/dist/src/record-trace-recording.js +0 -23
  148. package/dist/src/recording-provider.js +0 -1
  149. package/dist/src/sdk-batch.js +0 -2
  150. package/dist/src/selectors2.js +0 -1
  151. package/dist/src/ttl-memo.js +0 -1
  152. package/dist/src/tv-remote.js +0 -14
  153. package/dist/src/xml.js +0 -2
  154. /package/{macos-helper → apple/macos-helper}/Package.swift +0 -0
  155. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/AudioProbe.swift +0 -0
  156. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +0 -0
  157. /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/main.swift +0 -0
  158. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  159. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  160. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  161. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  162. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  163. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  164. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  165. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  166. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -0
  167. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +0 -0
  168. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  169. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  170. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -0
  171. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +0 -0
  172. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  173. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +0 -0
  174. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  175. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  176. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  177. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  178. /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
  179. /package/{src/platforms/linux → linux}/atspi-dump.py +0 -0
@@ -24,23 +24,19 @@ enum CommandType: String, Codable {
24
24
  case keyboardDismiss
25
25
  case keyboardReturn
26
26
  case alert
27
- case pinch
28
27
  case sequence
29
- case rotateGesture
30
- case transformGesture
28
+ case gesture
29
+ case gestureViewport
31
30
  case recordStart
32
31
  case recordStop
33
32
  case status
34
33
  case uptime
34
+ case activate
35
+ case terminate
36
+ case targetReset
35
37
  case shutdown
36
38
  }
37
39
 
38
- enum SynthesizedDragSemantics: String, Codable {
39
- case swipe
40
- case pan
41
- case fling
42
- }
43
-
44
40
  /// Runner command traits — see CONTEXT.md ("Runner command traits").
45
41
  ///
46
42
  /// Single source of truth for how the runner classifies a command across three
@@ -80,11 +76,11 @@ extension CommandType {
80
76
  // .sequence is the fused multi-step gesture batch.
81
77
  case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll, .desktopScroll,
82
78
  .back, .backInApp, .backSystem, .rotate, .appSwitcher,
83
- .keyboardDismiss, .keyboardReturn, .pinch, .sequence, .rotateGesture, .transformGesture:
79
+ .keyboardDismiss, .keyboardReturn, .sequence, .gesture:
84
80
  return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
85
81
 
86
82
  // Read-only reads: eligible for the session-invalidating retry.
87
- case .findText, .readText, .snapshot:
83
+ case .findText, .readText, .snapshot, .gestureViewport:
88
84
  return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: false)
89
85
 
90
86
  // Screenshot is both a read and a runner-lifecycle command (skips app-activation preflight).
@@ -96,7 +92,7 @@ extension CommandType {
96
92
  return CommandTraits(isInteraction: false, readOnly: .conditional, isLifecycle: false)
97
93
 
98
94
  // Runner-lifecycle commands: skip the app-activation preflight.
99
- case .recordStop, .uptime, .shutdown:
95
+ case .recordStop, .uptime, .terminate, .targetReset, .shutdown:
100
96
  return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
101
97
 
102
98
  case .status:
@@ -107,7 +103,7 @@ extension CommandType {
107
103
  // guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
108
104
  // check first (tracked as a follow-up). Also preserved: querySelector is NOT read-only;
109
105
  // recordStart is NOT a lifecycle command; home/alert remain non-interaction by design.
110
- case .mouseClick, .querySelector, .home, .recordStart:
106
+ case .mouseClick, .querySelector, .home, .recordStart, .activate:
111
107
  return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: false)
112
108
  }
113
109
  }
@@ -124,7 +120,6 @@ struct Command: Codable {
124
120
  let allowNonHittableCoordinateFallback: Bool?
125
121
  let delayMs: Int?
126
122
  let textEntryMode: String?
127
- let clearFirst: Bool?
128
123
  let action: String?
129
124
  let x: Double?
130
125
  let y: Double?
@@ -132,16 +127,13 @@ struct Command: Codable {
132
127
  let remoteButton: String?
133
128
  let x2: Double?
134
129
  let y2: Double?
135
- let dx: Double?
136
- let dy: Double?
137
130
  let durationMs: Double?
131
+ let timeoutMs: Double?
138
132
  let direction: String?
139
133
  let amount: Double?
140
134
  let pixels: Double?
141
135
  let orientation: String?
142
- let scale: Double?
143
- let degrees: Double?
144
- let velocity: Double?
136
+ let gesturePlan: RunnerGesturePlan?
145
137
  let outPath: String?
146
138
  let fps: Int?
147
139
  let maxSize: Int?
@@ -150,12 +142,43 @@ struct Command: Codable {
150
142
  let scope: String?
151
143
  let raw: Bool?
152
144
  let fullscreen: Bool?
145
+ let inlineScreenshot: Bool?
153
146
  let synthesized: Bool?
154
- /// Preserves the public gesture's timing semantics after it is lowered to runner `drag`.
155
- let dragSemantics: SynthesizedDragSemantics?
156
147
  let steps: [SequenceStep]?
157
148
  }
158
149
 
150
+ /// Canonical one- or two-pointer plan produced by the portable TypeScript planner.
151
+ struct RunnerGesturePlan: Codable {
152
+ let topology: String
153
+ let intent: String
154
+ let executionProfile: String?
155
+ let durationMs: Double
156
+ let viewport: RunnerGestureViewport
157
+ let pointers: [RunnerGesturePointer]
158
+ }
159
+
160
+ struct RunnerGestureViewport: Codable {
161
+ let x: Double
162
+ let y: Double
163
+ let width: Double
164
+ let height: Double
165
+ }
166
+
167
+ struct RunnerGesturePointer: Codable {
168
+ let pointerId: Int
169
+ let samples: [RunnerGestureSample]
170
+ }
171
+
172
+ struct RunnerGestureSample: Codable {
173
+ let offsetMs: Double
174
+ let point: RunnerGesturePoint
175
+ }
176
+
177
+ struct RunnerGesturePoint: Codable {
178
+ let x: Double
179
+ let y: Double
180
+ }
181
+
159
182
  /// One allowlisted coordinate gesture step inside a fused `sequence` command.
160
183
  /// `kind` is decoded as a raw String (not an enum) so the runner can return a clear
161
184
  /// INVALID_ARGS for an unknown kind instead of a generic decode failure.
@@ -163,15 +186,11 @@ struct SequenceStep: Codable {
163
186
  let kind: String
164
187
  let x: Double?
165
188
  let y: Double?
166
- let x2: Double?
167
- let y2: Double?
168
189
  let durationMs: Double?
169
190
  let pauseMs: Double?
170
- /// For `tap`/`drag` steps on iOS non-tv: use the synthesized HID fast path instead of the
191
+ /// For `tap` steps on iOS non-tv: use the synthesized HID fast path instead of the
171
192
  /// drag-based XCUICoordinate path, matching the individual command behavior.
172
193
  let synthesized: Bool?
173
- /// Preserves one-shot swipe timing when repeated swipes are fused into a sequence.
174
- let dragSemantics: SynthesizedDragSemantics?
175
194
  }
176
195
 
177
196
  /// Per-step result for a `sequence` response. `ok:false` carries the failing step's
@@ -211,6 +230,7 @@ extension Response {
211
230
 
212
231
  struct DataPayload: Codable {
213
232
  let message: String?
233
+ let imageBase64: String?
214
234
  let text: String?
215
235
  let found: Bool?
216
236
  let items: [String]?
@@ -241,6 +261,7 @@ struct DataPayload: Codable {
241
261
  let gestureFallback: String?
242
262
  let gestureFallbackMessage: String?
243
263
  let gestureFallbackHint: String?
264
+ let maestroNonHittableCoordinateFallbackUsed: Bool?
244
265
  let runnerFatal: Bool?
245
266
  let runnerFatalReason: String?
246
267
  let completedSteps: Int?
@@ -249,6 +270,7 @@ struct DataPayload: Codable {
249
270
 
250
271
  init(
251
272
  message: String? = nil,
273
+ imageBase64: String? = nil,
252
274
  text: String? = nil,
253
275
  found: Bool? = nil,
254
276
  items: [String]? = nil,
@@ -279,6 +301,7 @@ struct DataPayload: Codable {
279
301
  gestureFallback: String? = nil,
280
302
  gestureFallbackMessage: String? = nil,
281
303
  gestureFallbackHint: String? = nil,
304
+ maestroNonHittableCoordinateFallbackUsed: Bool? = nil,
282
305
  runnerFatal: Bool? = nil,
283
306
  runnerFatalReason: String? = nil,
284
307
  completedSteps: Int? = nil,
@@ -286,6 +309,7 @@ struct DataPayload: Codable {
286
309
  sequenceResults: [SequenceStepResult]? = nil
287
310
  ) {
288
311
  self.message = message
312
+ self.imageBase64 = imageBase64
289
313
  self.text = text
290
314
  self.found = found
291
315
  self.items = items
@@ -316,6 +340,7 @@ struct DataPayload: Codable {
316
340
  self.gestureFallback = gestureFallback
317
341
  self.gestureFallbackMessage = gestureFallbackMessage
318
342
  self.gestureFallbackHint = gestureFallbackHint
343
+ self.maestroNonHittableCoordinateFallbackUsed = maestroNonHittableCoordinateFallbackUsed
319
344
  self.runnerFatal = runnerFatal
320
345
  self.runnerFatalReason = runnerFatalReason
321
346
  self.completedSteps = completedSteps
@@ -1,10 +1,10 @@
1
1
  import XCTest
2
2
 
3
- // Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
3
+ // Swift port of buildScrollGesturePlan from src/contracts/scroll-gesture.ts.
4
4
  //
5
5
  // This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
6
6
  // recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
7
- // parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
7
+ // parity test vectors at the bottom of this file mirror src/contracts/scroll-gesture.test.ts —
8
8
  // if you change the math in either language, update the other and both vector sets.
9
9
  //
10
10
  // All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
@@ -9,7 +9,7 @@ extension RunnerTests {
9
9
 
10
10
  /// Allowlisted step kinds. Validated on both sides so an unsupported kind is rejected with a
11
11
  /// clear INVALID_ARGS naming the step index, executing nothing.
12
- private var sequenceableStepKinds: Set<String> { ["tap", "doubleTap", "longPress", "drag"] }
12
+ private var sequenceableStepKinds: Set<String> { ["tap", "doubleTap", "longPress"] }
13
13
 
14
14
  /// Per-step outcome carried by `assembleSequenceExecution`. The timing is captured by the
15
15
  /// executor closure (via performGesture) so ordering/stop-on-failure stay device-free testable.
@@ -127,17 +127,12 @@ extension RunnerTests {
127
127
  private func validateSequenceStep(_ step: SequenceStep, index: Int) -> Response? {
128
128
  guard sequenceableStepKinds.contains(step.kind) else {
129
129
  return sequenceInvalidArgs(
130
- "sequence step \(index) has unsupported kind \"\(step.kind)\"; allowed: tap, doubleTap, longPress, drag"
130
+ "sequence step \(index) has unsupported kind \"\(step.kind)\"; allowed: tap, doubleTap, longPress"
131
131
  )
132
132
  }
133
133
  guard let x = step.x, let y = step.y, x.isFinite, y.isFinite else {
134
134
  return sequenceInvalidArgs("sequence step \(index) (\(step.kind)) requires finite x and y")
135
135
  }
136
- if step.kind == "drag" {
137
- guard let x2 = step.x2, let y2 = step.y2, x2.isFinite, y2.isFinite else {
138
- return sequenceInvalidArgs("sequence step \(index) (drag) requires finite x2 and y2")
139
- }
140
- }
141
136
  return nil
142
137
  }
143
138
 
@@ -194,92 +189,6 @@ extension RunnerTests {
194
189
  #endif
195
190
  // Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
196
191
  }
197
- if step.kind == "drag", step.synthesized == true {
198
- let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
199
- let dragPoints: DragPoints
200
- let dragContext: SynthesizedCoordinateContext?
201
- #if os(iOS)
202
- guard let dragPlan = axFreeSynthesizedDragPlan(
203
- app: activeApp,
204
- x: x,
205
- y: y,
206
- x2: step.x2 ?? x,
207
- y2: step.y2 ?? y,
208
- context: synthesizedContext
209
- ) else {
210
- let nowMs = ProcessInfo.processInfo.systemUptime * 1000
211
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
212
- return SequenceStepOutcome(
213
- outcome: .unsupported(
214
- message: "synthesized coordinate drag could not resolve a finite coordinate frame",
215
- hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
216
- ),
217
- gestureStartUptimeMs: nowMs,
218
- gestureEndUptimeMs: nowMs
219
- )
220
- }
221
- dragPoints = dragPlan.points
222
- dragContext = dragPlan.context
223
- #else
224
- dragPoints = keyboardAvoidingDragPoints(
225
- app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
226
- dragContext = nil
227
- #endif
228
- let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
229
- let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
230
- synthesizedDragAt(
231
- app: activeApp,
232
- x: dragPoints.x,
233
- y: dragPoints.y,
234
- x2: dragPoints.x2,
235
- y2: dragPoints.y2,
236
- durationMs: durationMs,
237
- semantics: step.dragSemantics,
238
- context: dragContext
239
- )
240
- }
241
- if case .performed = outcome {
242
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
243
- if let pauseMs = step.pauseMs, pauseMs > 0 {
244
- sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
245
- }
246
- return SequenceStepOutcome(
247
- outcome: outcome,
248
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
249
- gestureEndUptimeMs: timing.gestureEndUptimeMs
250
- )
251
- }
252
- #if os(iOS)
253
- guard dragContext?.allowsXCTestCoordinateFallback == true else {
254
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
255
- return SequenceStepOutcome(
256
- outcome: outcome,
257
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
258
- gestureEndUptimeMs: timing.gestureEndUptimeMs
259
- )
260
- }
261
- #endif
262
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: true)
263
- let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
264
- let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
265
- dragAt(
266
- app: activeApp,
267
- x: dragPoints.x,
268
- y: dragPoints.y,
269
- x2: dragPoints.x2,
270
- y2: dragPoints.y2,
271
- holdDuration: fallbackHoldDuration
272
- )
273
- }
274
- if case .performed = fallbackOutcome, let pauseMs = step.pauseMs, pauseMs > 0 {
275
- sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
276
- }
277
- return SequenceStepOutcome(
278
- outcome: fallbackOutcome,
279
- gestureStartUptimeMs: fallbackTiming.gestureStartUptimeMs,
280
- gestureEndUptimeMs: fallbackTiming.gestureEndUptimeMs
281
- )
282
- }
283
192
  let (timing, outcome) = performGesture(activeApp) {
284
193
  switch step.kind {
285
194
  case "doubleTap":
@@ -288,20 +197,6 @@ extension RunnerTests {
288
197
  case "longPress":
289
198
  let duration = min(max(step.durationMs ?? 800, 16), 10000) / 1000.0
290
199
  return longPressAt(app: activeApp, x: x, y: y, duration: duration)
291
- case "drag":
292
- // Route through keyboardAvoidingDragPoints for parity with the individual `.drag` command.
293
- // The non-synthesized coordinate-drag path ignores durationMs, matching that command's
294
- // non-synthesized branch.
295
- let dragPoints = keyboardAvoidingDragPoints(
296
- app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
297
- return dragAt(
298
- app: activeApp,
299
- x: dragPoints.x,
300
- y: dragPoints.y,
301
- x2: dragPoints.x2,
302
- y2: dragPoints.y2,
303
- holdDuration: coordinateDragHoldDuration()
304
- )
305
200
  default:
306
201
  return tapAt(app: activeApp, x: x, y: y)
307
202
  }
@@ -57,12 +57,16 @@ extension RunnerTests {
57
57
  case .collectionView: return "CollectionView"
58
58
  case .table: return "Table"
59
59
  case .scrollView: return "ScrollView"
60
+ case .toolbar: return "Toolbar"
60
61
  case .searchField: return "SearchField"
61
62
  case .segmentedControl: return "SegmentedControl"
62
63
  case .stepper: return "Stepper"
63
64
  case .picker: return "Picker"
65
+ case .activityIndicator: return "ActivityIndicator"
66
+ case .progressIndicator: return "ProgressIndicator"
64
67
  case .checkBox: return "CheckBox"
65
68
  case .menuItem: return "MenuItem"
69
+ case .webView: return "WebView"
66
70
  case .other: return "Other"
67
71
  default:
68
72
  switch type.rawValue {
@@ -70,8 +74,6 @@ extension RunnerTests {
70
74
  return "Keyboard"
71
75
  case 20:
72
76
  return "Key"
73
- case 24:
74
- return "SearchField"
75
77
  default:
76
78
  return "Element(\(type.rawValue))"
77
79
  }
@@ -101,15 +103,22 @@ extension RunnerTests {
101
103
 
102
104
  static let flatInteractiveFallbackBudget: TimeInterval = 1.0
103
105
 
106
+ // The single production entry point -- always compiled, no unit-test overload. A unit test
107
+ // exercises this exact function; the only injectable seam lives inside
108
+ // `boundedBlockingSystemAlertSnapshot`'s probe closure (see `systemModalProbeOverrideForTesting`
109
+ // in RunnerTests.swift), so reverting this entry point to bypass the bounded probe fails the
110
+ // regression test.
104
111
  func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
105
- if let blocking = blockingSystemAlertSnapshot() {
112
+ let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
113
+ if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
106
114
  return blocking
107
115
  }
108
116
  return try runSnapshotCapturePlan(
109
117
  Self.regularVisiblePlan,
110
118
  app: app,
111
119
  options: options,
112
- terminal: .sparseWithFatalOnAXFailure
120
+ terminal: .sparseWithFatalOnAXFailure,
121
+ deadline: deadline
113
122
  )
114
123
  }
115
124
 
@@ -189,7 +198,12 @@ extension RunnerTests {
189
198
  regularSnapshot: true
190
199
  )
191
200
 
192
- let key = "\(snapshot.elementType)-\(evaluation.label)-\(evaluation.identifier)-\(snapshot.frame.origin.x)-\(snapshot.frame.origin.y)"
201
+ let key = Self.snapshotTraversalIdentity(
202
+ elementType: snapshot.elementType,
203
+ label: evaluation.label,
204
+ identifier: evaluation.identifier,
205
+ frame: snapshot.frame
206
+ )
193
207
  let isDuplicate = seen.contains(key)
194
208
  if !isDuplicate {
195
209
  seen.insert(key)
@@ -245,18 +259,98 @@ extension RunnerTests {
245
259
  )
246
260
  }
247
261
 
262
+ // See `snapshotFast` above: the single production entry point, no unit-test overload.
248
263
  func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
249
- if let blocking = blockingSystemAlertSnapshot() {
264
+ let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
265
+ if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
250
266
  return blocking
251
267
  }
252
268
  return try runSnapshotCapturePlan(
253
269
  Self.rawDiagnosticPlan,
254
270
  app: app,
255
271
  options: options,
256
- terminal: .throwOnAXFailure
272
+ terminal: .throwOnAXFailure,
273
+ deadline: deadline
257
274
  )
258
275
  }
259
276
 
277
+ /// Runs the pre-plan SpringBoard system-modal probe as a bounded capture tier sharing the plan
278
+ /// deadline, so a slow alert enumeration cannot bypass the snapshot timeout and stall (#1244).
279
+ func boundedBlockingSystemAlertSnapshot(deadline: Date) -> DataPayload? {
280
+ boundedBlockingSystemAlertSnapshotBody(deadline: deadline) { probeDeadline in
281
+ return self.blockingSystemAlertSnapshot(deadline: probeDeadline)
282
+ }
283
+ }
284
+
285
+ /// The real bounding/hook machinery used by `boundedBlockingSystemAlertSnapshot` above: the
286
+ /// probe closure it's given always calls `self.blockingSystemAlertSnapshot` in production, and
287
+ /// in unit-test builds may first consult `systemModalProbeOverrideForTesting`. Keeping this in
288
+ /// one place means the `runMainThreadWork` wrap and the `onAbandoned`/`onDrained` hooks can
289
+ /// never drift between what production runs and what the unit tests exercise.
290
+ private func boundedBlockingSystemAlertSnapshotBody(
291
+ deadline: Date,
292
+ probe: @escaping (Date) -> DataPayload?
293
+ ) -> DataPayload? {
294
+ #if os(macOS)
295
+ return nil
296
+ #else
297
+ let slice = Self.systemModalProbeSlice(
298
+ budget: systemModalProbeBudget,
299
+ deadlineRemaining: deadline.timeIntervalSinceNow
300
+ )
301
+ guard slice > 0 else {
302
+ NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_SKIPPED reason=budget_exhausted")
303
+ return nil
304
+ }
305
+ let probeDeadline = Date().addingTimeInterval(slice)
306
+ let startedAt = Date()
307
+ do {
308
+ return try runMainThreadWork(
309
+ command: nil,
310
+ timeout: slice,
311
+ timeoutError: {
312
+ SnapshotCaptureFailure(
313
+ code: Self.xCTestSnapshotTimeoutCode,
314
+ message: "the system-modal probe exceeded its \(slice)s time slice",
315
+ hint: "The capture plan recovers through non-XCTest snapshot tiers while the modal probe drains."
316
+ )
317
+ },
318
+ onAbandoned: {
319
+ self.retainAbandonedXCTestChannelWork()
320
+ NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_TIMEOUT slice=%.1f", slice)
321
+ self.penalizeSnapshotXCTestChannel(
322
+ bundleId: self.currentBundleId,
323
+ reason: "system_modal_probe_timeout"
324
+ )
325
+ },
326
+ onDrained: {
327
+ self.releaseAbandonedXCTestChannelWork()
328
+ NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_DRAINED")
329
+ }
330
+ ) {
331
+ probe(probeDeadline)
332
+ }
333
+ } catch {
334
+ NSLog(
335
+ "AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_ABORTED elapsedMs=%d error=%@",
336
+ Int(Date().timeIntervalSince(startedAt) * 1000),
337
+ String(describing: error)
338
+ )
339
+ return nil
340
+ }
341
+ #endif
342
+ }
343
+
344
+ /// The probe gets its own budget, clamped by whatever remains of the shared plan deadline, and
345
+ /// 0 (skip entirely) once that deadline is already spent.
346
+ static func systemModalProbeSlice(
347
+ budget: TimeInterval,
348
+ deadlineRemaining: TimeInterval
349
+ ) -> TimeInterval {
350
+ guard deadlineRemaining > 0 else { return 0 }
351
+ return min(budget, deadlineRemaining)
352
+ }
353
+
260
354
  func rawTreeSnapshotPayload(
261
355
  context: SnapshotTraversalContext,
262
356
  options: SnapshotOptions
@@ -566,6 +660,20 @@ extension RunnerTests {
566
660
  return abandonedTreeCaptureCount > 0
567
661
  }
568
662
 
663
+ /// The watchdog abandoned one unit of XCTest main-thread capture work that is still draining on
664
+ /// main; XCTest-backed snapshot tiers skip (`hasAbandonedTreeCapture`) until a matching release.
665
+ func retainAbandonedXCTestChannelWork() {
666
+ treeCaptureLock.lock()
667
+ abandonedTreeCaptureCount += 1
668
+ treeCaptureLock.unlock()
669
+ }
670
+
671
+ func releaseAbandonedXCTestChannelWork() {
672
+ treeCaptureLock.lock()
673
+ abandonedTreeCaptureCount -= 1
674
+ treeCaptureLock.unlock()
675
+ }
676
+
569
677
  /// Runs the blocking tree-snapshot XPC on the main thread bounded by `sliceSeconds`. On
570
678
  /// timeout the XPC keeps running on main (it cannot be cancelled); the capture is marked
571
679
  /// abandoned so plans avoid XCTest-backed tiers until it drains, the tree backend is penalized
@@ -583,9 +691,7 @@ extension RunnerTests {
583
691
  timeout: sliceSeconds,
584
692
  timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds),
585
693
  onAbandoned: {
586
- self.treeCaptureLock.lock()
587
- self.abandonedTreeCaptureCount += 1
588
- self.treeCaptureLock.unlock()
694
+ self.retainAbandonedXCTestChannelWork()
589
695
  NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_SLICE_TIMEOUT slice=%.1f", sliceSeconds)
590
696
  self.penalizeSnapshotXCTestChannel(
591
697
  bundleId: self.currentBundleId,
@@ -593,9 +699,7 @@ extension RunnerTests {
593
699
  )
594
700
  },
595
701
  onDrained: {
596
- self.treeCaptureLock.lock()
597
- self.abandonedTreeCaptureCount -= 1
598
- self.treeCaptureLock.unlock()
702
+ self.releaseAbandonedXCTestChannelWork()
599
703
  NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_DRAINED")
600
704
  }
601
705
  ) {
@@ -789,6 +893,19 @@ extension RunnerTests {
789
893
  return .infinite
790
894
  }
791
895
 
896
+ static func snapshotTraversalIdentity(
897
+ elementType: XCUIElement.ElementType,
898
+ label: String,
899
+ identifier: String,
900
+ frame: CGRect
901
+ ) -> String {
902
+ #if os(iOS)
903
+ "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)-\(frame.width)-\(frame.height)"
904
+ #else
905
+ return "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)"
906
+ #endif
907
+ }
908
+
792
909
  private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {
793
910
  if depth > 4 { return nil }
794
911
  let text = snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -118,6 +118,17 @@ extension RunnerTests {
118
118
  )
119
119
  }
120
120
 
121
+ func clearSnapshotXCTestChannelPenalty(reason: String) {
122
+ snapshotXCTestChannelPenaltyLock.lock()
123
+ let hadActivePenalty = Date() < snapshotXCTestChannelPenaltyUntil
124
+ snapshotXCTestChannelPenaltyBundleId = nil
125
+ snapshotXCTestChannelPenaltyUntil = Date.distantPast
126
+ snapshotXCTestChannelPenaltyLock.unlock()
127
+ if hadActivePenalty {
128
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_XCTEST_CHANNEL_PENALTY_CLEARED reason=%@", reason)
129
+ }
130
+ }
131
+
121
132
  func isSnapshotXCTestChannelPenalized(bundleId: String?) -> Bool {
122
133
  snapshotXCTestChannelPenaltyLock.lock()
123
134
  defer { snapshotXCTestChannelPenaltyLock.unlock() }
@@ -127,6 +138,12 @@ extension RunnerTests {
127
138
  return penalized == bundleId
128
139
  }
129
140
 
141
+ func consumeSnapshotXCTestPenaltyWarmupExemption() -> Bool {
142
+ let pending = snapshotXCTestPenaltyWarmupExemptionPending
143
+ snapshotXCTestPenaltyWarmupExemptionPending = false
144
+ return pending
145
+ }
146
+
130
147
  /// Pure plan-reorder rule: a penalized XCTest accessibility channel uses independent backends
131
148
  /// when the platform has one, otherwise it keeps XCTest work on a short probe. The raw
132
149
  /// diagnostic plan keeps tree-first errors, and unknown plans are left untouched.
@@ -168,12 +185,15 @@ extension RunnerTests {
168
185
  _ plan: [SnapshotBackendKind],
169
186
  app: XCUIApplication,
170
187
  options: SnapshotOptions,
171
- terminal: SnapshotCaptureTerminalPolicy
188
+ terminal: SnapshotCaptureTerminalPolicy,
189
+ deadline: Date? = nil
172
190
  ) throws -> DataPayload {
173
191
  var best: (kind: SnapshotBackendKind, capture: SnapshotBackendCapture)?
174
192
  var firstFailure: (reason: String, code: String)?
175
193
  var axFailure: SnapshotCaptureFailure?
176
- let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
194
+ // A caller may share the pre-plan system-modal probe's deadline; otherwise own the full budget (#1244).
195
+ let deadline = deadline ?? Date().addingTimeInterval(Self.snapshotPlanBudget)
196
+ let suppressXCTestPenalty = consumeSnapshotXCTestPenaltyWarmupExemption()
177
197
 
178
198
  // Reorder is iOS-only because hostile screens can make XCTest tree/query work grind while
179
199
  // the app remains visually responsive. Simulators can avoid that channel through private AX;
@@ -237,14 +257,30 @@ extension RunnerTests {
237
257
  treeCaptureSliceBudgetOverride: effective.treeCaptureSliceBudgetOverride
238
258
  )
239
259
  else {
240
- recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
260
+ recordSlowXCTestSnapshotBackendIfNeeded(
261
+ kind,
262
+ startedAt: backendStartedAt,
263
+ penaltySuppressed: suppressXCTestPenalty
264
+ )
241
265
  continue
242
266
  }
243
267
  capture = result
244
- recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
268
+ recordSlowXCTestSnapshotBackendIfNeeded(
269
+ kind,
270
+ startedAt: backendStartedAt,
271
+ penaltySuppressed: suppressXCTestPenalty
272
+ )
245
273
  } catch let failure as SnapshotCaptureFailure {
246
- recordXCTestSnapshotBackendFailureIfNeeded(kind, failure: failure)
247
- recordSlowXCTestSnapshotBackendIfNeeded(kind, startedAt: backendStartedAt)
274
+ recordXCTestSnapshotBackendFailureIfNeeded(
275
+ kind,
276
+ failure: failure,
277
+ penaltySuppressed: suppressXCTestPenalty
278
+ )
279
+ recordSlowXCTestSnapshotBackendIfNeeded(
280
+ kind,
281
+ startedAt: backendStartedAt,
282
+ penaltySuppressed: suppressXCTestPenalty
283
+ )
248
284
  if Self.isAxSnapshotFailure(failure) { axFailure = failure }
249
285
  if firstFailure == nil {
250
286
  firstFailure = (failure.message, Self.isAxSnapshotFailure(failure) ? "ax-rejected" : "capture-failed")
@@ -313,7 +349,12 @@ extension RunnerTests {
313
349
 
314
350
  /// Marks XCTest-backed snapshot tiers as penalized when one attempt ground past the slow-capture
315
351
  /// threshold — even a successful one: the next capture of this screen must not re-grind.
316
- private func recordSlowXCTestSnapshotBackendIfNeeded(_ kind: SnapshotBackendKind, startedAt: Date) {
352
+ private func recordSlowXCTestSnapshotBackendIfNeeded(
353
+ _ kind: SnapshotBackendKind,
354
+ startedAt: Date,
355
+ penaltySuppressed: Bool
356
+ ) {
357
+ guard !penaltySuppressed else { return }
317
358
  guard kind.usesXCTestAccessibilityChannel else { return }
318
359
  let elapsed = Date().timeIntervalSince(startedAt)
319
360
  guard elapsed > snapshotXCTestSlowCaptureThreshold else { return }
@@ -325,8 +366,10 @@ extension RunnerTests {
325
366
 
326
367
  private func recordXCTestSnapshotBackendFailureIfNeeded(
327
368
  _ kind: SnapshotBackendKind,
328
- failure: SnapshotCaptureFailure
369
+ failure: SnapshotCaptureFailure,
370
+ penaltySuppressed: Bool
329
371
  ) {
372
+ guard !penaltySuppressed else { return }
330
373
  guard kind.usesXCTestAccessibilityChannel, failure.code == Self.xCTestSnapshotTimeoutCode else { return }
331
374
  penalizeSnapshotXCTestChannel(
332
375
  bundleId: currentBundleId,