agent-device 0.16.9 → 0.16.11

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 (54) hide show
  1. package/README.md +1 -0
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.9.apk → agent-device-android-multitouch-helper-0.16.11.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.11.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.9.manifest.json → agent-device-android-multitouch-helper-0.16.11.manifest.json} +4 -4
  5. package/android-snapshot-helper/README.md +6 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.11.apk +0 -0
  7. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.11.apk.sha256 +1 -0
  8. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.9.manifest.json → agent-device-android-snapshot-helper-0.16.11.manifest.json} +6 -6
  9. package/dist/src/1352.js +1 -1
  10. package/dist/src/221.js +6 -6
  11. package/dist/src/2415.js +27 -27
  12. package/dist/src/2805.js +1 -1
  13. package/dist/src/4778.js +1 -0
  14. package/dist/src/5792.js +1 -1
  15. package/dist/src/6085.js +1 -1
  16. package/dist/src/6232.js +1 -1
  17. package/dist/src/8699.js +1 -1
  18. package/dist/src/9238.js +4 -0
  19. package/dist/src/9533.js +1 -1
  20. package/dist/src/9542.js +3 -3
  21. package/dist/src/apple.js +1 -1
  22. package/dist/src/apps.js +2 -2
  23. package/dist/src/args.js +54 -25
  24. package/dist/src/batch.d.ts +1 -0
  25. package/dist/src/cli.js +19 -19
  26. package/dist/src/command-metadata.js +1 -1
  27. package/dist/src/command-surface.js +1 -1
  28. package/dist/src/contracts.d.ts +1 -0
  29. package/dist/src/contracts.js +1 -1
  30. package/dist/src/generic.js +10 -10
  31. package/dist/src/index.d.ts +9 -0
  32. package/dist/src/input-actions.js +1 -1
  33. package/dist/src/record-trace.js +3 -3
  34. package/dist/src/session.js +9 -9
  35. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +7 -0
  36. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +109 -0
  37. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +282 -226
  38. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +282 -0
  39. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +29 -0
  40. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +44 -34
  41. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +41 -1
  42. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -20
  43. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +10 -50
  44. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +3 -23
  45. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +64 -22
  46. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +7 -4
  47. package/package.json +1 -1
  48. package/server.json +2 -2
  49. package/skills/dogfood/SKILL.md +1 -1
  50. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.9.apk.sha256 +0 -1
  51. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.9.apk +0 -0
  52. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.9.apk.sha256 +0 -1
  53. package/dist/src/2842.js +0 -1
  54. package/dist/src/8114.js +0 -4
@@ -0,0 +1,282 @@
1
+ import Foundation
2
+ import XCTest
3
+
4
+ enum RunnerCommandLifecycleState: String {
5
+ case notAccepted
6
+ case accepted
7
+ case started
8
+ case completed
9
+ case failed
10
+ }
11
+
12
+ struct RunnerCommandJournalEntry {
13
+ let commandId: String
14
+ let command: String
15
+ var state: RunnerCommandLifecycleState
16
+ var responseOk: Bool?
17
+ var responseJson: String?
18
+ var error: ErrorPayload?
19
+ }
20
+
21
+ final class RunnerCommandJournal {
22
+ private let lock = NSLock()
23
+ private let maxEntries = 64
24
+ private let maxResponseJsonBytes = 16 * 1024
25
+ private var entries: [String: RunnerCommandJournalEntry] = [:]
26
+ private var order: [String] = []
27
+
28
+ func accept(command: Command) {
29
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
30
+ lock.lock()
31
+ defer { lock.unlock() }
32
+ entries[commandId] = RunnerCommandJournalEntry(
33
+ commandId: commandId,
34
+ command: command.command.rawValue,
35
+ state: .accepted,
36
+ responseOk: nil,
37
+ responseJson: nil,
38
+ error: nil
39
+ )
40
+ order.removeAll { $0 == commandId }
41
+ order.append(commandId)
42
+ pruneIfNeeded()
43
+ }
44
+
45
+ func start(command: Command) {
46
+ update(command: command, state: .started, responseOk: nil, responseJson: nil, error: nil)
47
+ }
48
+
49
+ func finish(command: Command, response: Response) {
50
+ update(
51
+ command: command,
52
+ state: response.ok ? .completed : .failed,
53
+ responseOk: response.ok,
54
+ responseJson: encodeResponseJson(command: command, response: response),
55
+ error: response.error
56
+ )
57
+ }
58
+
59
+ func fail(command: Command, error: Error) {
60
+ update(
61
+ command: command,
62
+ state: .failed,
63
+ responseOk: nil,
64
+ responseJson: nil,
65
+ error: ErrorPayload(message: error.localizedDescription)
66
+ )
67
+ }
68
+
69
+ func status(commandId: String) -> DataPayload {
70
+ guard let normalized = normalizedCommandId(commandId) else {
71
+ return DataPayload(lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue)
72
+ }
73
+ lock.lock()
74
+ let entry = entries[normalized]
75
+ lock.unlock()
76
+ guard let entry else {
77
+ return DataPayload(
78
+ commandId: normalized,
79
+ lifecycleState: RunnerCommandLifecycleState.notAccepted.rawValue
80
+ )
81
+ }
82
+ return DataPayload(
83
+ commandId: entry.commandId,
84
+ lifecycleState: entry.state.rawValue,
85
+ lifecycleCommand: entry.command,
86
+ lifecycleResponseOk: entry.responseOk,
87
+ lifecycleResponseJson: entry.responseJson,
88
+ lifecycleErrorCode: entry.error?.code,
89
+ lifecycleErrorMessage: entry.error?.message,
90
+ lifecycleErrorHint: entry.error?.hint
91
+ )
92
+ }
93
+
94
+ private func update(
95
+ command: Command,
96
+ state: RunnerCommandLifecycleState,
97
+ responseOk: Bool?,
98
+ responseJson: String?,
99
+ error: ErrorPayload?
100
+ ) {
101
+ guard let commandId = normalizedCommandId(command.commandId) else { return }
102
+ lock.lock()
103
+ defer { lock.unlock() }
104
+ var entry = entries[commandId] ?? RunnerCommandJournalEntry(
105
+ commandId: commandId,
106
+ command: command.command.rawValue,
107
+ state: .accepted,
108
+ responseOk: nil,
109
+ responseJson: nil,
110
+ error: nil
111
+ )
112
+ entry.state = state
113
+ entry.responseOk = responseOk
114
+ entry.responseJson = responseJson
115
+ entry.error = error
116
+ entries[commandId] = entry
117
+ order.removeAll { $0 == commandId }
118
+ order.append(commandId)
119
+ pruneIfNeeded()
120
+ }
121
+
122
+ private func pruneIfNeeded() {
123
+ while order.count > maxEntries {
124
+ let removed = order.removeFirst()
125
+ entries.removeValue(forKey: removed)
126
+ }
127
+ }
128
+
129
+ private func normalizedCommandId(_ value: String?) -> String? {
130
+ guard let value else { return nil }
131
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
132
+ return trimmed.isEmpty ? nil : trimmed
133
+ }
134
+
135
+ private func encodeResponseJson(command: Command, response: Response) -> String? {
136
+ guard shouldRetainResponseJson(command: command) else { return nil }
137
+ guard let data = try? JSONEncoder().encode(response) else { return nil }
138
+ guard data.count <= maxResponseJsonBytes else { return nil }
139
+ return String(data: data, encoding: .utf8)
140
+ }
141
+
142
+ private func shouldRetainResponseJson(command: Command) -> Bool {
143
+ switch command.command {
144
+ case .snapshot, .screenshot:
145
+ return false
146
+ case .tap, .mouseClick, .tapSeries, .longPress, .interactionFrame, .drag, .dragSeries,
147
+ .remotePress, .type, .swipe, .findText, .querySelector, .readText, .back, .backInApp,
148
+ .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn, .alert,
149
+ .pinch, .rotateGesture, .transformGesture, .recordStart, .recordStop, .status, .uptime,
150
+ .shutdown:
151
+ return true
152
+ }
153
+ }
154
+ }
155
+
156
+ extension RunnerTests {
157
+ func testCommandJournalRetentionPolicy() throws {
158
+ let journal = RunnerCommandJournal()
159
+
160
+ let uptime = runnerJournalCommand("uptime", id: "small-scalar")
161
+ journal.accept(command: uptime)
162
+ journal.finish(
163
+ command: uptime,
164
+ response: Response(ok: true, data: DataPayload(currentUptimeMs: 12.5))
165
+ )
166
+
167
+ let scalarStatus = journal.status(commandId: "small-scalar")
168
+ XCTAssertEqual(scalarStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
169
+ XCTAssertEqual(scalarStatus.lifecycleResponseOk, true)
170
+ XCTAssertNotNil(scalarStatus.lifecycleResponseJson)
171
+ let scalarResponse = try decodeRunnerJournalResponse(scalarStatus.lifecycleResponseJson)
172
+ XCTAssertEqual(scalarResponse.data?.currentUptimeMs, 12.5)
173
+
174
+ let querySelector = runnerJournalCommand("querySelector", id: "small-object")
175
+ journal.accept(command: querySelector)
176
+ journal.finish(
177
+ command: querySelector,
178
+ response: Response(ok: true, data: DataPayload(found: true, nodes: [runnerJournalNode()]))
179
+ )
180
+
181
+ let objectStatus = journal.status(commandId: "small-object")
182
+ XCTAssertNotNil(objectStatus.lifecycleResponseJson)
183
+ let objectResponse = try decodeRunnerJournalResponse(objectStatus.lifecycleResponseJson)
184
+ XCTAssertEqual(objectResponse.data?.found, true)
185
+ XCTAssertEqual(objectResponse.data?.nodes?.count, 1)
186
+
187
+ let snapshot = runnerJournalCommand("snapshot", id: "snapshot-tree")
188
+ journal.accept(command: snapshot)
189
+ journal.finish(
190
+ command: snapshot,
191
+ response: Response(ok: true, data: DataPayload(nodes: [runnerJournalNode()], truncated: false))
192
+ )
193
+
194
+ let snapshotStatus = journal.status(commandId: "snapshot-tree")
195
+ XCTAssertEqual(snapshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
196
+ XCTAssertEqual(snapshotStatus.lifecycleResponseOk, true)
197
+ XCTAssertNil(snapshotStatus.lifecycleResponseJson)
198
+
199
+ let screenshot = runnerJournalCommand("screenshot", id: "screenshot-artifact")
200
+ journal.accept(command: screenshot)
201
+ journal.finish(
202
+ command: screenshot,
203
+ response: Response(ok: true, data: DataPayload(message: "tmp/screenshot-1.png"))
204
+ )
205
+
206
+ let screenshotStatus = journal.status(commandId: "screenshot-artifact")
207
+ XCTAssertEqual(screenshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
208
+ XCTAssertEqual(screenshotStatus.lifecycleResponseOk, true)
209
+ XCTAssertNil(screenshotStatus.lifecycleResponseJson)
210
+
211
+ let largeRead = runnerJournalCommand("readText", id: "large-read")
212
+ journal.accept(command: largeRead)
213
+ journal.finish(
214
+ command: largeRead,
215
+ response: Response(ok: true, data: DataPayload(text: String(repeating: "x", count: 17 * 1024)))
216
+ )
217
+
218
+ let largeReadStatus = journal.status(commandId: "large-read")
219
+ XCTAssertEqual(largeReadStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
220
+ XCTAssertEqual(largeReadStatus.lifecycleResponseOk, true)
221
+ XCTAssertNil(largeReadStatus.lifecycleResponseJson)
222
+ }
223
+
224
+ func testCommandJournalKeepsErrorMetadataWhenResponseJsonIsDropped() {
225
+ let journal = RunnerCommandJournal()
226
+ let snapshot = runnerJournalCommand("snapshot", id: "snapshot-error")
227
+ let hint = "Try a smaller read such as snapshot -s <visible label or id> -d 8."
228
+
229
+ journal.accept(command: snapshot)
230
+ journal.finish(
231
+ command: snapshot,
232
+ response: Response(
233
+ ok: false,
234
+ error: ErrorPayload(
235
+ code: "IOS_AX_SNAPSHOT_FAILED",
236
+ message: "iOS XCTest snapshot failed while serializing the accessibility tree.",
237
+ hint: hint
238
+ )
239
+ )
240
+ )
241
+
242
+ let status = journal.status(commandId: "snapshot-error")
243
+ XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.failed.rawValue)
244
+ XCTAssertEqual(status.lifecycleResponseOk, false)
245
+ XCTAssertNil(status.lifecycleResponseJson)
246
+ XCTAssertEqual(status.lifecycleErrorCode, "IOS_AX_SNAPSHOT_FAILED")
247
+ XCTAssertEqual(
248
+ status.lifecycleErrorMessage,
249
+ "iOS XCTest snapshot failed while serializing the accessibility tree."
250
+ )
251
+ XCTAssertEqual(status.lifecycleErrorHint, hint)
252
+ }
253
+
254
+ private func runnerJournalCommand(_ command: String, id: String) -> Command {
255
+ let json = #"{"command":"\#(command)","commandId":"\#(id)"}"#
256
+ return try! JSONDecoder().decode(Command.self, from: Data(json.utf8))
257
+ }
258
+
259
+ private func runnerJournalNode() -> SnapshotNode {
260
+ SnapshotNode(
261
+ index: 0,
262
+ type: "button",
263
+ label: "Continue",
264
+ identifier: "continue",
265
+ value: nil,
266
+ rect: SnapshotRect(x: 10, y: 20, width: 100, height: 44),
267
+ enabled: true,
268
+ focused: nil,
269
+ selected: nil,
270
+ hittable: true,
271
+ depth: 0,
272
+ parentIndex: nil,
273
+ hiddenContentAbove: nil,
274
+ hiddenContentBelow: nil
275
+ )
276
+ }
277
+
278
+ private func decodeRunnerJournalResponse(_ responseJson: String?) throws -> Response {
279
+ let responseJson = try XCTUnwrap(responseJson)
280
+ return try JSONDecoder().decode(Response.self, from: Data(responseJson.utf8))
281
+ }
282
+ }
@@ -0,0 +1,29 @@
1
+ import XCTest
2
+
3
+ extension RunnerTests {
4
+ /// Runs `block` and returns its value. If it raises an Objective-C exception, logs the message
5
+ /// under `AGENT_DEVICE_RUNNER_<tag>_IGNORED_EXCEPTION` and returns `fallback`.
6
+ ///
7
+ /// Consolidates the catch-log-and-default band-aid the runner uses around exception-prone
8
+ /// XCUITest queries (flaky `allElementsBoundByIndex` snapshots, stale element reads), giving the
9
+ /// "silently logged and continued" path one searchable format and one place to add per-tag
10
+ /// exception telemetry later. `RunnerObjCExceptionCatcher.catchException` takes a non-escaping
11
+ /// block, so `block` may capture `inout` state.
12
+ func safely<T>(_ tag: String, _ fallback: T, _ block: () -> T) -> T {
13
+ var result = fallback
14
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
15
+ result = block()
16
+ })
17
+ if let exceptionMessage {
18
+ NSLog("AGENT_DEVICE_RUNNER_%@_IGNORED_EXCEPTION=%@", tag, exceptionMessage)
19
+ return fallback
20
+ }
21
+ return result
22
+ }
23
+
24
+ /// Optional-returning convenience: returns `nil` on exception (matching the common
25
+ /// `var x: T?` + catch-and-return-nil shape).
26
+ func safely<T>(_ tag: String, _ block: () -> T?) -> T? {
27
+ safely(tag, nil, block)
28
+ }
29
+ }
@@ -285,14 +285,13 @@ extension RunnerTests {
285
285
  }
286
286
 
287
287
  private func textInputCandidatesAt(app: XCUIApplication, point: CGPoint) -> [XCUIElement] {
288
- var candidates: [XCUIElement] = []
289
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
288
+ safely("TEXT_INPUT_AT_POINT", []) {
290
289
  // Query the text-input element types directly instead of enumerating the entire tree
291
290
  // (app.descendants(.any).allElementsBoundByIndex snapshots every element and is ~10x
292
291
  // slower — it dominated fill latency because resolveTextEntryElement re-runs this on
293
292
  // each verify/repair poll once the focused field reference goes stale).
294
293
  // Prefer the smallest matching field so nested editable controls win over large containers.
295
- candidates = [
294
+ [
296
295
  app.textFields,
297
296
  app.secureTextFields,
298
297
  app.searchFields,
@@ -318,15 +317,7 @@ extension RunnerTests {
318
317
  }
319
318
  return left.elementType.rawValue < right.elementType.rawValue
320
319
  }
321
- })
322
- if let exceptionMessage {
323
- NSLog(
324
- "AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
325
- exceptionMessage
326
- )
327
- return []
328
320
  }
329
- return candidates
330
321
  }
331
322
 
332
323
  private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
@@ -431,9 +422,8 @@ extension RunnerTests {
431
422
 
432
423
  private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
433
424
  #if os(iOS)
434
- var matches: [XCUIElement] = []
435
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
436
- matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
425
+ let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
426
+ app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
437
427
  guard element.exists else { return false }
438
428
  switch element.elementType {
439
429
  case .textField, .secureTextField, .searchField, .textView:
@@ -442,13 +432,6 @@ extension RunnerTests {
442
432
  return false
443
433
  }
444
434
  }
445
- })
446
- if let exceptionMessage {
447
- NSLog(
448
- "AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@",
449
- exceptionMessage
450
- )
451
- return nil
452
435
  }
453
436
  return matches.count == 1 ? matches[0] : nil
454
437
  #else
@@ -651,6 +634,42 @@ extension RunnerTests {
651
634
  return performCoordinateDrag(app: app, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
652
635
  }
653
636
 
637
+ func synthesizedDragAt(
638
+ app: XCUIApplication,
639
+ x: Double,
640
+ y: Double,
641
+ x2: Double,
642
+ y2: Double,
643
+ durationMs: Double
644
+ ) -> RunnerInteractionOutcome {
645
+ #if os(iOS)
646
+ if let message = RunnerSynthesizedGesture.synthesizeSwipe(
647
+ withApplication: app,
648
+ x: x,
649
+ y: y,
650
+ x2: x2,
651
+ y2: y2,
652
+ durationMs: durationMs
653
+ ) {
654
+ return .unsupported(
655
+ message: message,
656
+ hint: "Falling back to XCTest coordinate drag may be slower; update Xcode if this persists."
657
+ )
658
+ }
659
+ return .performed
660
+ #elseif os(tvOS)
661
+ return .unsupported(
662
+ message: "coordinate drag is not supported on tvOS",
663
+ hint: "tvOS has no coordinate input; use remote-driven swipe/scroll to move focus instead."
664
+ )
665
+ #else
666
+ return .unsupported(
667
+ message: "coordinate drag is not supported on macOS",
668
+ hint: "macOS automation has no touchscreen; use mouse-driven interactions instead."
669
+ )
670
+ #endif
671
+ }
672
+
654
673
  func keyboardAvoidingDragPoints(
655
674
  app: XCUIApplication,
656
675
  x: Double,
@@ -784,22 +803,13 @@ extension RunnerTests {
784
803
 
785
804
  private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
786
805
  #if os(iOS)
787
- var frame: CGRect?
788
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
806
+ return safely("KEYBOARD_FRAME") {
789
807
  let keyboard = app.keyboards.firstMatch
790
- guard keyboard.exists else { return }
808
+ guard keyboard.exists else { return nil }
791
809
  let keyboardFrame = keyboard.frame
792
- guard !keyboardFrame.isEmpty else { return }
793
- frame = keyboardFrame
794
- })
795
- if let exceptionMessage {
796
- NSLog(
797
- "AGENT_DEVICE_RUNNER_KEYBOARD_FRAME_IGNORED_EXCEPTION=%@",
798
- exceptionMessage
799
- )
800
- return nil
810
+ guard !keyboardFrame.isEmpty else { return nil }
811
+ return keyboardFrame
801
812
  }
802
- return frame
803
813
  #else
804
814
  return nil
805
815
  #endif
@@ -30,6 +30,7 @@ enum CommandType: String, Codable {
30
30
  case transformGesture
31
31
  case recordStart
32
32
  case recordStop
33
+ case status
33
34
  case uptime
34
35
  case shutdown
35
36
  }
@@ -91,6 +92,9 @@ extension CommandType {
91
92
  case .recordStop, .uptime, .shutdown:
92
93
  return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
93
94
 
95
+ case .status:
96
+ return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: true)
97
+
94
98
  // Normal preflight, not retried.
95
99
  // NOTE: mouseClick stays non-interaction for now — it is macOS-only and the foreground
96
100
  // guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
@@ -104,6 +108,8 @@ extension CommandType {
104
108
 
105
109
  struct Command: Codable {
106
110
  let command: CommandType
111
+ let commandId: String?
112
+ let statusCommandId: String?
107
113
  let appBundleId: String?
108
114
  let text: String?
109
115
  let selectorKey: String?
@@ -141,6 +147,7 @@ struct Command: Codable {
141
147
  let scope: String?
142
148
  let raw: Bool?
143
149
  let fullscreen: Bool?
150
+ let synthesized: Bool?
144
151
  }
145
152
 
146
153
  struct Response: Codable {
@@ -171,10 +178,21 @@ struct DataPayload: Codable {
171
178
  let referenceWidth: Double?
172
179
  let referenceHeight: Double?
173
180
  let currentUptimeMs: Double?
181
+ let commandId: String?
182
+ let lifecycleState: String?
183
+ let lifecycleCommand: String?
184
+ let lifecycleResponseOk: Bool?
185
+ let lifecycleResponseJson: String?
186
+ let lifecycleErrorCode: String?
187
+ let lifecycleErrorMessage: String?
188
+ let lifecycleErrorHint: String?
174
189
  let visible: Bool?
175
190
  let wasVisible: Bool?
176
191
  let dismissed: Bool?
177
192
  let orientation: String?
193
+ let gestureFallback: String?
194
+ let gestureFallbackMessage: String?
195
+ let gestureFallbackHint: String?
178
196
 
179
197
  init(
180
198
  message: String? = nil,
@@ -192,10 +210,21 @@ struct DataPayload: Codable {
192
210
  referenceWidth: Double? = nil,
193
211
  referenceHeight: Double? = nil,
194
212
  currentUptimeMs: Double? = nil,
213
+ commandId: String? = nil,
214
+ lifecycleState: String? = nil,
215
+ lifecycleCommand: String? = nil,
216
+ lifecycleResponseOk: Bool? = nil,
217
+ lifecycleResponseJson: String? = nil,
218
+ lifecycleErrorCode: String? = nil,
219
+ lifecycleErrorMessage: String? = nil,
220
+ lifecycleErrorHint: String? = nil,
195
221
  visible: Bool? = nil,
196
222
  wasVisible: Bool? = nil,
197
223
  dismissed: Bool? = nil,
198
- orientation: String? = nil
224
+ orientation: String? = nil,
225
+ gestureFallback: String? = nil,
226
+ gestureFallbackMessage: String? = nil,
227
+ gestureFallbackHint: String? = nil
199
228
  ) {
200
229
  self.message = message
201
230
  self.text = text
@@ -212,10 +241,21 @@ struct DataPayload: Codable {
212
241
  self.referenceWidth = referenceWidth
213
242
  self.referenceHeight = referenceHeight
214
243
  self.currentUptimeMs = currentUptimeMs
244
+ self.commandId = commandId
245
+ self.lifecycleState = lifecycleState
246
+ self.lifecycleCommand = lifecycleCommand
247
+ self.lifecycleResponseOk = lifecycleResponseOk
248
+ self.lifecycleResponseJson = lifecycleResponseJson
249
+ self.lifecycleErrorCode = lifecycleErrorCode
250
+ self.lifecycleErrorMessage = lifecycleErrorMessage
251
+ self.lifecycleErrorHint = lifecycleErrorHint
215
252
  self.visible = visible
216
253
  self.wasVisible = wasVisible
217
254
  self.dismissed = dismissed
218
255
  self.orientation = orientation
256
+ self.gestureFallback = gestureFallback
257
+ self.gestureFallbackMessage = gestureFallbackMessage
258
+ self.gestureFallbackHint = gestureFallbackHint
219
259
  }
220
260
  }
221
261
 
@@ -354,14 +354,7 @@ extension RunnerTests {
354
354
  }
355
355
 
356
356
  private func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
357
- var viewport = CGRect.infinite
358
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
359
- viewport = snapshotViewport(app: app)
360
- })
361
- if let exceptionMessage {
362
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_VIEWPORT_IGNORED_EXCEPTION=%@", exceptionMessage)
363
- }
364
- return viewport
357
+ safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
365
358
  }
366
359
 
367
360
  private func describeSnapshotError(_ error: Error) -> String {
@@ -718,18 +711,7 @@ extension RunnerTests {
718
711
  }
719
712
 
720
713
  private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
721
- var elements: [XCUIElement] = []
722
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
723
- elements = fetch()
724
- })
725
- if let exceptionMessage {
726
- NSLog(
727
- "AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@",
728
- exceptionMessage
729
- )
730
- return []
731
- }
732
- return elements
714
+ safely("SNAPSHOT_QUERY", [], fetch)
733
715
  }
734
716
 
735
717
  private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool {
@@ -77,33 +77,11 @@ extension RunnerTests {
77
77
  }
78
78
 
79
79
  func safeElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
80
- var elements: [XCUIElement] = []
81
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
82
- elements = fetch()
83
- })
84
- if let exceptionMessage {
85
- NSLog(
86
- "AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@",
87
- exceptionMessage
88
- )
89
- return []
90
- }
91
- return elements
80
+ safely("MODAL_QUERY", [], fetch)
92
81
  }
93
82
 
94
83
  private func safeIsBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
95
- var isBlocking = false
96
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
97
- isBlocking = isBlockingSystemModal(element, in: springboard)
98
- })
99
- if let exceptionMessage {
100
- NSLog(
101
- "AGENT_DEVICE_RUNNER_MODAL_CHECK_IGNORED_EXCEPTION=%@",
102
- exceptionMessage
103
- )
104
- return false
105
- }
106
- return isBlocking
84
+ safely("MODAL_CHECK", false) { isBlockingSystemModal(element, in: springboard) }
107
85
  }
108
86
 
109
87
  private func isBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
@@ -134,25 +112,16 @@ extension RunnerTests {
134
112
  }
135
113
 
136
114
  private func safeIsActionableCandidate(_ candidate: XCUIElement, seen: inout Set<String>) -> Bool {
137
- var include = false
138
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
139
- if !candidate.exists || !candidate.isHittable { return }
140
- if !actionableTypes.contains(candidate.elementType) { return }
115
+ safely("MODAL_ACTION", false) {
116
+ if !candidate.exists || !candidate.isHittable { return false }
117
+ if !actionableTypes.contains(candidate.elementType) { return false }
141
118
  let frame = candidate.frame
142
- if frame.isNull || frame.isEmpty { return }
119
+ if frame.isNull || frame.isEmpty { return false }
143
120
  let key = "\(candidate.elementType.rawValue)-\(frame.origin.x)-\(frame.origin.y)-\(frame.size.width)-\(frame.size.height)-\(candidate.label)"
144
- if seen.contains(key) { return }
121
+ if seen.contains(key) { return false }
145
122
  seen.insert(key)
146
- include = true
147
- })
148
- if let exceptionMessage {
149
- NSLog(
150
- "AGENT_DEVICE_RUNNER_MODAL_ACTION_IGNORED_EXCEPTION=%@",
151
- exceptionMessage
152
- )
153
- return false
123
+ return true
154
124
  }
155
- return include
156
125
  }
157
126
 
158
127
  private func preferredSystemModalTitle(_ element: XCUIElement) -> String {
@@ -205,9 +174,8 @@ extension RunnerTests {
205
174
  depth: Int,
206
175
  hittableOverride: Bool? = nil
207
176
  ) -> SnapshotNode? {
208
- var node: SnapshotNode?
209
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
210
- node = makeSnapshotNode(
177
+ safely("MODAL_NODE") {
178
+ makeSnapshotNode(
211
179
  element: element,
212
180
  index: index,
213
181
  type: type,
@@ -216,14 +184,6 @@ extension RunnerTests {
216
184
  depth: depth,
217
185
  hittableOverride: hittableOverride
218
186
  )
219
- })
220
- if let exceptionMessage {
221
- NSLog(
222
- "AGENT_DEVICE_RUNNER_MODAL_NODE_IGNORED_EXCEPTION=%@",
223
- exceptionMessage
224
- )
225
- return nil
226
187
  }
227
- return node
228
188
  }
229
189
  }