@swmansion/argent 0.23.0 → 0.23.1-next.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 (44) hide show
  1. package/README.md +1 -1
  2. package/bin/argent-android-devtools-0.1.0.apk +0 -0
  3. package/bin/darwin/ax-service +0 -0
  4. package/bin/darwin/tvos-ax-service +0 -0
  5. package/bin/darwin/tvos-hid-daemon +0 -0
  6. package/bin/tcp/ax-service +0 -0
  7. package/dist/cli-cmds.mjs +17 -2
  8. package/dist/installer.mjs +17 -2
  9. package/dist/ios-device-runner/ArgentRunner/ArgentRunner/RunnerHostApp.swift +37 -0
  10. package/dist/ios-device-runner/ArgentRunner/ArgentRunner.xcodeproj/project.pbxproj +389 -0
  11. package/dist/ios-device-runner/ArgentRunner/ArgentRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  12. package/dist/ios-device-runner/ArgentRunner/ArgentRunner.xcodeproj/xcshareddata/xcschemes/ArgentRunner.xcscheme +88 -0
  13. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.h +16 -0
  14. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentExceptionGuard.m +16 -0
  15. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Commands.swift +280 -0
  16. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Gestures.swift +149 -0
  17. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Screenshot.swift +21 -0
  18. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+Snapshot.swift +327 -0
  19. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession+TextEntry.swift +157 -0
  20. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerSession.swift +410 -0
  21. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/ArgentRunnerUITests-Bridging-Header.h +1 -0
  22. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/CommandJournal.swift +134 -0
  23. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/MainThreadGate.swift +111 -0
  24. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/RunnerHTTPServer.swift +267 -0
  25. package/dist/ios-device-runner/ArgentRunner/ArgentRunnerUITests/RunnerProtocol.swift +331 -0
  26. package/dist/mcp-server.mjs +16 -1
  27. package/dist/tool-server.cjs +4224 -1388
  28. package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
  29. package/dylibs/libKeyboardPatch.dylib +0 -0
  30. package/dylibs/libNativeDevtoolsIos.dylib +0 -0
  31. package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
  32. package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
  33. package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
  34. package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
  35. package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
  36. package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
  37. package/package.json +1 -1
  38. package/rules/argent.md +8 -3
  39. package/skills/argent-create-flow/SKILL.md +1 -0
  40. package/skills/argent-device-interact/SKILL.md +2 -0
  41. package/skills/argent-ios-device-interact/SKILL.md +18 -0
  42. package/skills/argent-ios-device-setup/SKILL.md +20 -0
  43. package/skills/argent-qa-flows/SKILL.md +2 -0
  44. package/skills/argent-screenshot-diff/SKILL.md +2 -0
@@ -0,0 +1,267 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// Minimal single-purpose HTTP/1.1 endpoint on NWListener: each connection
5
+ /// carries one POSTed JSON command and receives one JSON reply, then closes.
6
+ ///
7
+ /// The listener is unauthenticated, so it binds loopback-only. The host
8
+ /// reaches the runner through a usbmux forwarded stream that terminates on
9
+ /// the device's own loopback. Bodies are opaque bytes handed to the
10
+ /// dispatch closure.
11
+ final class RunnerHTTPServer {
12
+ struct Reply {
13
+ let status: Int
14
+ let body: Data
15
+ /// When set, the server invokes `onFinish` after this reply has been
16
+ /// flushed. The shutdown acknowledgement must reach the client before
17
+ /// the session ends.
18
+ let finishAfterSend: Bool
19
+
20
+ init(status: Int, body: Data, finishAfterSend: Bool = false) {
21
+ self.status = status
22
+ self.body = body
23
+ self.finishAfterSend = finishAfterSend
24
+ }
25
+ }
26
+
27
+ /// Upper bound on the size of one request. Each request carries a single
28
+ /// command, so a larger body indicates a client bug.
29
+ private static let maxRequestBytes = 2 * 1024 * 1024
30
+ private let queue = DispatchQueue(label: "argent.runner.transport")
31
+ private let dispatch: (Data, @escaping (Reply) -> Void) -> Void
32
+ /// The clean end of the session, after a shutdown reply has been flushed.
33
+ private let onFinish: () -> Void
34
+ /// The listener can no longer accept connections (the port is in use, the
35
+ /// interface is unavailable). Carries the failure description.
36
+ private let onListenerFailure: (String) -> Void
37
+ private var listener: NWListener?
38
+
39
+ init(
40
+ dispatch: @escaping (Data, @escaping (Reply) -> Void) -> Void,
41
+ onFinish: @escaping () -> Void,
42
+ onListenerFailure: @escaping (String) -> Void
43
+ ) {
44
+ self.dispatch = dispatch
45
+ self.onFinish = onFinish
46
+ self.onListenerFailure = onListenerFailure
47
+ }
48
+
49
+ /// Starts listening on loopback, on the given port or a system-assigned
50
+ /// one when the port is 0.
51
+ func start(port: UInt16) throws {
52
+ // Restrict to the loopback interface rather than pinning a 127.0.0.1
53
+ // endpoint. The interface restriction admits either address family and
54
+ // keeps port-0 auto-assignment and the .port readback working.
55
+ let parameters = NWParameters.tcp
56
+ parameters.requiredInterfaceType = .loopback
57
+ let listener: NWListener
58
+
59
+ if port > 0, let nwPort = NWEndpoint.Port(rawValue: port) {
60
+ listener = try NWListener(using: parameters, on: nwPort)
61
+ } else {
62
+ listener = try NWListener(using: parameters)
63
+ }
64
+
65
+ listener.stateUpdateHandler = { [weak self] state in
66
+ switch state {
67
+ case .ready:
68
+ NSLog(
69
+ "ARGENT_RUNNER_LISTENING port=%d",
70
+ Int(self?.listener?.port?.rawValue ?? 0)
71
+ )
72
+ case .failed(let error):
73
+ // A failed listener can never receive another command. Hand the
74
+ // failure to the session so it ends as a failed test, and the
75
+ // host observes a failing exit instead of a silent hang or a
76
+ // green run that never served.
77
+ let description = String(describing: error)
78
+ NSLog("ARGENT_RUNNER_LISTENER_FAILED error=%@", description)
79
+ self?.onListenerFailure(description)
80
+ default:
81
+ break
82
+ }
83
+ }
84
+
85
+ listener.newConnectionHandler = { [weak self] connection in
86
+ guard let self else { return }
87
+ connection.start(queue: self.queue)
88
+ self.receive(on: connection, buffered: Data())
89
+ }
90
+
91
+ self.listener = listener
92
+ listener.start(queue: queue)
93
+ }
94
+
95
+ /// Stops accepting connections.
96
+ func stop() {
97
+ listener?.cancel()
98
+ listener = nil
99
+ }
100
+
101
+ /// Accumulates bytes from one connection until the buffered request is
102
+ /// complete, then dispatches its body.
103
+ private func receive(on connection: NWConnection, buffered: Data) {
104
+ connection.receive(minimumIncompleteLength: 1, maximumLength: 1 << 20) {
105
+ [weak self] data, _, _, _ in
106
+
107
+ guard let self, let data, !data.isEmpty else {
108
+ connection.cancel()
109
+ return
110
+ }
111
+
112
+ var buffer = buffered
113
+ buffer.append(data)
114
+
115
+ if buffer.count > Self.maxRequestBytes {
116
+ self.send(
117
+ Reply(
118
+ status: 413,
119
+ body: Self.oversizedRequestBody(
120
+ limit: Self.maxRequestBytes
121
+ )
122
+ ),
123
+ over: connection
124
+ )
125
+ return
126
+ }
127
+
128
+ switch Self.requestVerdict(in: buffer) {
129
+ case .complete(let body):
130
+ self.dispatch(body) { reply in
131
+ self.send(reply, over: connection)
132
+ }
133
+ case .incomplete:
134
+ self.receive(on: connection, buffered: buffer)
135
+ case .malformed:
136
+ self.send(
137
+ Reply(status: 400, body: Self.malformedRequestBody()),
138
+ over: connection
139
+ )
140
+ case .oversized:
141
+ self.send(
142
+ Reply(
143
+ status: 413,
144
+ body: Self.oversizedRequestBody(
145
+ limit: Self.maxRequestBytes
146
+ )
147
+ ),
148
+ over: connection
149
+ )
150
+ }
151
+ }
152
+ }
153
+
154
+ /// Writes the HTTP reply, closes the connection, and triggers `onFinish`
155
+ /// for a reply marked `finishAfterSend`.
156
+ private func send(_ reply: Reply, over connection: NWConnection) {
157
+ let head = [
158
+ "HTTP/1.1 \(reply.status) \(reply.status == 200 ? "OK" : "Error")",
159
+ "Content-Type: application/json",
160
+ "Content-Length: \(reply.body.count)",
161
+ "Connection: close",
162
+ "",
163
+ "",
164
+ ].joined(separator: "\r\n")
165
+
166
+ var payload = Data(head.utf8)
167
+ payload.append(reply.body)
168
+
169
+ connection.send(
170
+ content: payload,
171
+ isComplete: true,
172
+ completion: .contentProcessed { [weak self] error in
173
+ if let error {
174
+ NSLog(
175
+ "ARGENT_RUNNER_SEND_FAILED error=%@",
176
+ String(describing: error)
177
+ )
178
+ }
179
+
180
+ connection.cancel()
181
+
182
+ if reply.finishAfterSend {
183
+ self?.onFinish()
184
+ }
185
+ }
186
+ )
187
+ }
188
+
189
+ /// Verdict on the bytes buffered so far for one request.
190
+ enum RequestVerdict: Equatable {
191
+ /// The header block or declared body has not fully arrived. Keep
192
+ /// receiving.
193
+ case incomplete
194
+ /// A full request. The payload is the raw body bytes.
195
+ case complete(Data)
196
+ /// The header block ended without a usable Content-Length. Later bytes
197
+ /// cannot repair a finished header block, so waiting for more would
198
+ /// hang the connection.
199
+ case malformed
200
+ /// The declared Content-Length exceeds `maxRequestBytes`. The request
201
+ /// is rejected before its body arrives.
202
+ case oversized
203
+ }
204
+
205
+ /// Classifies the buffered bytes as an incomplete, complete, malformed,
206
+ /// or oversized request.
207
+ static func requestVerdict(in buffer: Data) -> RequestVerdict {
208
+ guard let headEnd = buffer.range(of: Data("\r\n\r\n".utf8)) else {
209
+ return .incomplete
210
+ }
211
+
212
+ let head = String(
213
+ decoding: buffer.subdata(
214
+ in: buffer.startIndex..<headEnd.lowerBound
215
+ ),
216
+ as: UTF8.self
217
+ )
218
+
219
+ var contentLength: Int?
220
+
221
+ for line in head.split(separator: "\r\n") {
222
+ let parts = line.split(separator: ":", maxSplits: 1)
223
+ guard parts.count == 2 else { continue }
224
+
225
+ if parts[0].trimmingCharacters(in: .whitespaces).lowercased()
226
+ == "content-length"
227
+ {
228
+ contentLength = Int(
229
+ parts[1].trimmingCharacters(in: .whitespaces)
230
+ )
231
+ }
232
+ }
233
+
234
+ guard let contentLength, contentLength >= 0 else { return .malformed }
235
+ guard contentLength <= maxRequestBytes else { return .oversized }
236
+
237
+ let bodyStart = headEnd.upperBound
238
+
239
+ guard
240
+ buffer.distance(from: bodyStart, to: buffer.endIndex)
241
+ >= contentLength
242
+ else { return .incomplete }
243
+
244
+ return .complete(
245
+ buffer.subdata(
246
+ in: bodyStart..<buffer.index(bodyStart, offsetBy: contentLength)
247
+ )
248
+ )
249
+ }
250
+
251
+ /// The INVALID_REQUEST body for a request that exceeds the size limit.
252
+ private static func oversizedRequestBody(limit: Int) -> Data {
253
+ Data(
254
+ #"{"ok":false,"error":{"code":"INVALID_REQUEST","message":"request body exceeds \#(limit) bytes"}}"#
255
+ .utf8
256
+ )
257
+ }
258
+
259
+ /// The INVALID_REQUEST body for a header block without a usable
260
+ /// Content-Length.
261
+ private static func malformedRequestBody() -> Data {
262
+ Data(
263
+ #"{"ok":false,"error":{"code":"INVALID_REQUEST","message":"request headers lack a usable Content-Length"}}"#
264
+ .utf8
265
+ )
266
+ }
267
+ }
@@ -0,0 +1,331 @@
1
+ import Foundation
2
+
3
+ // MARK: - Argent iOS runner wire protocol (v1)
4
+ //
5
+ // One HTTP POST per command: the request body is a JSON `CommandRequest`, the
6
+ // reply a JSON `Envelope`. PROTOCOL.md at the package root documents the
7
+ // contract. The TypeScript client under
8
+ // packages/tool-server/src/utils/ios-device mirrors these shapes.
9
+
10
+ enum CommandKind: String, Codable {
11
+ case status
12
+ case viewport
13
+ case tap
14
+ case longPress
15
+ case drag
16
+ case type
17
+ case keyboardReturn
18
+ case keyboardDelete
19
+ case button
20
+ case snapshot
21
+ case screenshot
22
+ case shutdown
23
+ }
24
+
25
+ extension CommandKind {
26
+ /// Commands that read state without side effects. The client retries these
27
+ /// freely. Mutating commands are sent exactly once and recovered through
28
+ /// the journal via `status` with `statusCommandId`.
29
+ var isReadOnly: Bool {
30
+ switch self {
31
+ case .status, .viewport, .snapshot, .screenshot:
32
+ return true
33
+ case .tap, .longPress, .drag, .type, .keyboardReturn, .keyboardDelete,
34
+ .button, .shutdown:
35
+ return false
36
+ }
37
+ }
38
+
39
+ /// Commands that target an app and therefore require `appBundleId`. A
40
+ /// missing target is treated as a caller error, never redirected to the
41
+ /// runner's own host app.
42
+ var requiresAppBundleId: Bool {
43
+ switch self {
44
+ case .viewport, .tap, .longPress, .drag, .type, .keyboardReturn,
45
+ .keyboardDelete, .snapshot:
46
+ return true
47
+ case .status, .button, .screenshot, .shutdown:
48
+ return false
49
+ }
50
+ }
51
+
52
+ /// Whether the journal keeps the full response JSON for `status` recovery.
53
+ var retainsResponseInJournal: Bool {
54
+ switch self {
55
+ // Snapshot and screenshot replies are large and read-only. The client
56
+ // reruns them instead of recovering them from the journal, which keeps
57
+ // them from evicting journaled mutation results.
58
+ case .snapshot, .screenshot:
59
+ return false
60
+ default:
61
+ return true
62
+ }
63
+ }
64
+
65
+ /// Main-thread watchdog budget for one command execution. PROTOCOL.md's
66
+ /// "Timeout budgets" table is the authoritative pairing with the client
67
+ /// windows.
68
+ var executionTimeout: TimeInterval {
69
+ switch self {
70
+ // XCTest types long strings in real time, so typing needs extra room.
71
+ case .type: return 55
72
+ // Gestures must outlast XCTest's ~60s pre-event idle wait. A screen that
73
+ // never reports quiescent stalls the wait until XCTest gives up and
74
+ // synthesizes the event anyway, and a shorter budget would abandon a
75
+ // command that still completes.
76
+ case .tap, .longPress, .drag: return 75
77
+ default: return 30
78
+ }
79
+ }
80
+ }
81
+
82
+ /// Hardware buttons the `button` command accepts, mapped onto
83
+ /// `XCUIDevice.Button` in ArgentRunnerSession+Commands.swift so this file
84
+ /// needs no XCTest import. The power/lock button and the app switcher have no
85
+ /// public XCUIDevice API, and `camera` would pin the runner to a newer Xcode.
86
+ enum DeviceButton: String, Codable {
87
+ case home
88
+ case volumeUp
89
+ case volumeDown
90
+ case actionButton
91
+ }
92
+
93
+ struct CommandRequest: Codable {
94
+ let command: CommandKind
95
+ let commandId: String?
96
+ /// `status` only: the commandId whose journaled fate is requested.
97
+ let statusCommandId: String?
98
+ /// The app the command targets. Required when `command.requiresAppBundleId`.
99
+ let appBundleId: String?
100
+ /// `tap`/`longPress`: absolute point (in points) in the app's coordinate space.
101
+ let x: Double?
102
+ let y: Double?
103
+ /// `tap`: number of taps. Defaults to 1; 2 is the native double-tap.
104
+ /// Higher counts loop single taps on the device: there is no native N-tap
105
+ /// API, so they land as separate taps, not one multi-tap gesture.
106
+ let numberOfTaps: Int?
107
+ /// `drag`: absolute start and end points.
108
+ let fromX: Double?
109
+ let fromY: Double?
110
+ let toX: Double?
111
+ let toY: Double?
112
+ /// `longPress`: press duration. `drag`: duration of the movement.
113
+ let durationMs: Double?
114
+ /// `drag`: how long the touch rests at the start point before it moves
115
+ /// (default 50 ms). A long-press pickup, the way a list item is lifted for
116
+ /// reordering, needs about 500 ms or more.
117
+ let holdMs: Double?
118
+ /// `drag`: rest the touch at the destination before lifting so the release
119
+ /// velocity is near zero and the scroll view does not fling. Mirrors the
120
+ /// simulator's ease-out `settle` swipe.
121
+ let settle: Bool?
122
+ /// `type`: the text delivered to the focused input.
123
+ let text: String?
124
+ /// `button`: which hardware button to press.
125
+ let button: DeviceButton?
126
+
127
+ /// The trimmed commandId, nil when absent or blank.
128
+ var normalizedCommandId: String? {
129
+ guard
130
+ let trimmed = commandId?.trimmingCharacters(
131
+ in: .whitespacesAndNewlines
132
+ ),
133
+ !trimmed.isEmpty
134
+ else { return nil }
135
+ return trimmed
136
+ }
137
+
138
+ /// The trimmed appBundleId, nil when absent or blank.
139
+ var normalizedAppBundleId: String? {
140
+ guard
141
+ let trimmed = appBundleId?.trimmingCharacters(
142
+ in: .whitespacesAndNewlines
143
+ ),
144
+ !trimmed.isEmpty
145
+ else { return nil }
146
+ return trimmed
147
+ }
148
+ }
149
+
150
+ /// Stable error codes shared with the TypeScript client. Only RUNNER_BUSY is
151
+ /// client-retryable. RUNNER_WEDGED tells the host to recycle the session.
152
+ enum RunnerErrorCode: String {
153
+ case invalidRequest = "INVALID_REQUEST"
154
+ case appBundleIdRequired = "APP_BUNDLE_ID_REQUIRED"
155
+ case appNotAvailable = "APP_NOT_AVAILABLE"
156
+ case appBackgrounded = "APP_BACKGROUNDED"
157
+ case textInputNotFocused = "TEXT_INPUT_NOT_FOCUSED"
158
+ /// Hardware or an API this device lacks: an absent button, a tap count
159
+ /// XCUICoordinate cannot execute as one gesture. An XCTest call that
160
+ /// throws mid-command is COMMAND_FAILED, never this.
161
+ case unsupportedOperation = "UNSUPPORTED_OPERATION"
162
+ case runnerBusy = "RUNNER_BUSY"
163
+ case runnerWedged = "RUNNER_WEDGED"
164
+ case xctestRecordedFailure = "XCTEST_RECORDED_FAILURE"
165
+ case snapshotFailed = "SNAPSHOT_FAILED"
166
+ case commandTimedOut = "COMMAND_TIMED_OUT"
167
+ case commandFailed = "COMMAND_FAILED"
168
+ }
169
+
170
+ struct ErrorPayload: Encodable {
171
+ let code: String
172
+ let message: String
173
+ let hint: String?
174
+ }
175
+
176
+ /// Type-erased Encodable wrapper that lets `Envelope` carry any per-command
177
+ /// payload.
178
+ struct AnyEncodable: Encodable {
179
+ private let encodeInto: (Encoder) throws -> Void
180
+ init<T: Encodable>(_ value: T) { self.encodeInto = value.encode(to:) }
181
+ func encode(to encoder: Encoder) throws { try encodeInto(encoder) }
182
+ }
183
+
184
+ struct Envelope: Encodable {
185
+ let ok: Bool
186
+ let data: AnyEncodable?
187
+ let error: ErrorPayload?
188
+ /// True only when the runner re-fronted a backgrounded target app before
189
+ /// executing the command (see `foregroundTarget`), on ok and error
190
+ /// envelopes alike: the re-front precedes the command, so a command that
191
+ /// then failed changed the foreground screen too. nil is omitted from the
192
+ /// encoded JSON.
193
+ let reactivated: Bool?
194
+ /// Advisory on an otherwise-ok reply: set when a mutating command succeeded
195
+ /// while the suppressed-issue counter grew, meaning the gesture may have
196
+ /// missed. nil is omitted from the encoded JSON.
197
+ let warning: String?
198
+
199
+ /// An ok envelope carrying the payload.
200
+ static func success<T: Encodable>(_ payload: T) -> Envelope {
201
+ Envelope(
202
+ ok: true,
203
+ data: AnyEncodable(payload),
204
+ error: nil,
205
+ reactivated: nil,
206
+ warning: nil
207
+ )
208
+ }
209
+
210
+ /// An error envelope with a stable code, a message, and an optional hint.
211
+ static func failure(
212
+ _ code: RunnerErrorCode,
213
+ _ message: String,
214
+ hint: String? = nil
215
+ ) -> Envelope {
216
+ Envelope(
217
+ ok: false,
218
+ data: nil,
219
+ error: ErrorPayload(
220
+ code: code.rawValue,
221
+ message: message,
222
+ hint: hint
223
+ ),
224
+ reactivated: nil,
225
+ warning: nil
226
+ )
227
+ }
228
+
229
+ /// A copy of this reply with `reactivated: true`, marking that the target
230
+ /// app was backgrounded and had to be re-fronted before the command ran.
231
+ func withReactivated() -> Envelope {
232
+ Envelope(
233
+ ok: ok,
234
+ data: data,
235
+ error: error,
236
+ reactivated: true,
237
+ warning: warning
238
+ )
239
+ }
240
+
241
+ /// A copy of this reply carrying an advisory `warning`. Composes with
242
+ /// `withReactivated()`, so a reply can carry both markers.
243
+ func withWarning(_ warning: String) -> Envelope {
244
+ Envelope(
245
+ ok: ok,
246
+ data: data,
247
+ error: error,
248
+ reactivated: reactivated,
249
+ warning: warning
250
+ )
251
+ }
252
+ }
253
+
254
+ // MARK: - Per-command payloads
255
+
256
+ struct MessagePayload: Encodable {
257
+ let message: String
258
+ }
259
+
260
+ struct HealthPayload: Encodable {
261
+ let uptimeMs: Double
262
+ /// "idle" | "busy" | "wedged", the main-thread gate's view of the runner.
263
+ let state: String
264
+ /// XCTIssues muted as accessibility noise since launch. Suppression matches
265
+ /// Apple-owned wording in `ArgentRunnerSession.SuppressedIssueWording`, so
266
+ /// a count stuck at zero while `recordedFailures` climbs means an Xcode
267
+ /// release reworded the strings.
268
+ let suppressedIssues: Int
269
+ /// XCTest's cumulative recorded-failure count for the session, read from
270
+ /// `testRun.totalFailureCount`. A failure that gets past suppression turns
271
+ /// a successful mutation into XCTEST_RECORDED_FAILURE.
272
+ let recordedFailures: Int
273
+ }
274
+
275
+ struct CommandStatusPayload: Encodable {
276
+ let commandId: String
277
+ /// "notAccepted" | "accepted" | "started" | "completed" | "failed"
278
+ let state: String
279
+ let command: String?
280
+ let responseOk: Bool?
281
+ /// The completed command's full JSON envelope, when retained.
282
+ let responseJson: String?
283
+ let errorCode: String?
284
+ let errorMessage: String?
285
+ let errorHint: String?
286
+ }
287
+
288
+ struct ViewportPayload: Encodable {
289
+ let x: Double
290
+ let y: Double
291
+ let width: Double
292
+ let height: Double
293
+ }
294
+
295
+ struct ScreenshotPayload: Encodable {
296
+ let imageBase64: String
297
+ }
298
+
299
+ struct SnapshotRect: Encodable {
300
+ let x: Double
301
+ let y: Double
302
+ let width: Double
303
+ let height: Double
304
+ }
305
+
306
+ struct SnapshotNodePayload: Encodable {
307
+ let index: Int
308
+ let type: String
309
+ let label: String?
310
+ let identifier: String?
311
+ let value: String?
312
+ let rect: SnapshotRect
313
+ let enabled: Bool
314
+ let focused: Bool?
315
+ let selected: Bool?
316
+ let depth: Int
317
+ let parentIndex: Int?
318
+ }
319
+
320
+ struct SnapshotQualityPayload: Encodable {
321
+ /// "healthy" | "degraded"
322
+ let state: String
323
+ let backend: String
324
+ let reason: String?
325
+ let reasonCode: String?
326
+ }
327
+
328
+ struct SnapshotPayload: Encodable {
329
+ let nodes: [SnapshotNodePayload]
330
+ let quality: SnapshotQualityPayload
331
+ }
@@ -16474,6 +16474,18 @@ var FAILURE_CODES = {
16474
16474
  SIMULATOR_COMMAND_REJECTED: "SIMULATOR_COMMAND_REJECTED",
16475
16475
  SIMULATOR_COMMAND_ACK_TIMEOUT: "SIMULATOR_COMMAND_ACK_TIMEOUT",
16476
16476
  SIMULATOR_COMMAND_TRANSPORT_FAILED: "SIMULATOR_COMMAND_TRANSPORT_FAILED",
16477
+ // Physical iOS devices: the XCUITest-runner blueprint lifecycle, the runner
16478
+ // command client, and the `xcrun devicectl` subprocess wrapper (simulators
16479
+ // use the SIMULATOR_* set).
16480
+ IOS_DEVICE_RUNNER_FACTORY_OPTIONS_MISSING: "IOS_DEVICE_RUNNER_FACTORY_OPTIONS_MISSING",
16481
+ IOS_DEVICE_RUNNER_NOT_READY: "IOS_DEVICE_RUNNER_NOT_READY",
16482
+ IOS_DEVICE_RUNNER_TERMINATED: "IOS_DEVICE_RUNNER_TERMINATED",
16483
+ IOS_DEVICE_RUNNER_EXITED: "IOS_DEVICE_RUNNER_EXITED",
16484
+ // The runner answered a command with an ok:false envelope.
16485
+ IOS_DEVICE_RUNNER_COMMAND_FAILED: "IOS_DEVICE_RUNNER_COMMAND_FAILED",
16486
+ // The usbmux or HTTP exchange with a reachable runner failed.
16487
+ IOS_DEVICE_RUNNER_TRANSPORT_FAILED: "IOS_DEVICE_RUNNER_TRANSPORT_FAILED",
16488
+ IOS_DEVICECTL_COMMAND_FAILED: "IOS_DEVICECTL_COMMAND_FAILED",
16477
16489
  AX_QUERY_TIMEOUT: "AX_QUERY_TIMEOUT",
16478
16490
  AX_DAEMON_READY_TIMEOUT: "AX_DAEMON_READY_TIMEOUT",
16479
16491
  AX_DAEMON_EXITED_BEFORE_READY: "AX_DAEMON_EXITED_BEFORE_READY",
@@ -16644,6 +16656,7 @@ var FAILURE_CODES = {
16644
16656
  KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
16645
16657
  KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
16646
16658
  KEYBOARD_TEXT_AND_KEY_COMBINED: "KEYBOARD_TEXT_AND_KEY_COMBINED",
16659
+ KEYBOARD_INPUT_NOT_FOCUSED: "KEYBOARD_INPUT_NOT_FOCUSED",
16647
16660
  SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
16648
16661
  SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
16649
16662
  BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
@@ -16688,7 +16701,9 @@ var FAILURE_COMMANDS = [
16688
16701
  "electron",
16689
16702
  "npm",
16690
16703
  "npx",
16691
- "unknown"
16704
+ "unknown",
16705
+ "devicectl",
16706
+ "xcodebuild"
16692
16707
  ];
16693
16708
  var FAILURE_SIGNAL_NAMES = [
16694
16709
  "SIGABRT",