2026-02-28 13:38:59 +01:00
|
|
|
// MARK: - Wire Models
|
|
|
|
|
|
|
|
|
|
enum CommandType: String, Codable {
|
|
|
|
|
case tap
|
2026-03-23 16:01:52 +01:00
|
|
|
case mouseClick
|
2026-02-28 13:38:59 +01:00
|
|
|
case tapSeries
|
|
|
|
|
case longPress
|
2026-03-27 12:06:57 +01:00
|
|
|
case interactionFrame
|
2026-02-28 13:38:59 +01:00
|
|
|
case drag
|
|
|
|
|
case dragSeries
|
2026-05-12 15:32:21 +07:00
|
|
|
case remotePress
|
2026-02-28 13:38:59 +01:00
|
|
|
case type
|
|
|
|
|
case swipe
|
|
|
|
|
case findText
|
2026-05-19 11:36:48 +02:00
|
|
|
case querySelector
|
2026-03-25 17:13:31 +01:00
|
|
|
case readText
|
2026-02-28 13:38:59 +01:00
|
|
|
case snapshot
|
|
|
|
|
case screenshot
|
|
|
|
|
case back
|
2026-03-27 10:18:00 +01:00
|
|
|
case backInApp
|
|
|
|
|
case backSystem
|
2026-02-28 13:38:59 +01:00
|
|
|
case home
|
2026-04-01 20:34:56 +02:00
|
|
|
case rotate
|
2026-02-28 13:38:59 +01:00
|
|
|
case appSwitcher
|
2026-03-27 11:49:30 +01:00
|
|
|
case keyboardDismiss
|
2026-05-27 21:45:51 +02:00
|
|
|
case keyboardReturn
|
2026-02-28 13:38:59 +01:00
|
|
|
case alert
|
|
|
|
|
case pinch
|
2026-05-22 18:01:58 +02:00
|
|
|
case rotateGesture
|
|
|
|
|
case transformGesture
|
2026-02-28 13:38:59 +01:00
|
|
|
case recordStart
|
|
|
|
|
case recordStop
|
2026-06-01 20:40:09 -05:00
|
|
|
case status
|
2026-03-24 13:38:41 +01:00
|
|
|
case uptime
|
2026-02-28 13:38:59 +01:00
|
|
|
case shutdown
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 18:11:04 +02:00
|
|
|
/// Runner command traits — see CONTEXT.md ("Runner command traits").
|
|
|
|
|
///
|
|
|
|
|
/// Single source of truth for how the runner classifies a command across three
|
|
|
|
|
/// independent axes, replacing the three hand-maintained switches that used to live
|
|
|
|
|
/// in RunnerTests+Lifecycle.swift (isInteractionCommand / isReadOnlyCommand /
|
|
|
|
|
/// isRunnerLifecycleCommand). The classification is load-bearing for ADR-0002 session
|
|
|
|
|
/// invalidation: `readOnly` gates the retry that nulls currentApp/currentBundleId.
|
|
|
|
|
struct CommandTraits {
|
|
|
|
|
/// Whether the command needs the foreground-guard + stabilization preflight before running.
|
|
|
|
|
let isInteraction: Bool
|
|
|
|
|
/// Whether the command is eligible for the session-invalidating retry.
|
|
|
|
|
/// `.conditional` is resolved against the request (alert is read-only only for its `get` action).
|
|
|
|
|
let readOnly: ReadOnly
|
|
|
|
|
/// Whether the command skips the app-activation preflight entirely.
|
|
|
|
|
let isLifecycle: Bool
|
|
|
|
|
|
|
|
|
|
enum ReadOnly {
|
|
|
|
|
case always
|
|
|
|
|
case never
|
|
|
|
|
/// Alert-only today. Resolved in `isReadOnlyCommand` with alert's rule (read-only for the
|
|
|
|
|
/// `get` action, mutating otherwise). A new `.conditional` command would inherit that rule
|
|
|
|
|
/// until the resolver is generalized — give it explicit handling there if its semantics differ.
|
|
|
|
|
case conditional
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension CommandType {
|
|
|
|
|
/// The classification for this command. Exhaustive by construction: a new CommandType
|
|
|
|
|
/// cannot compile without being classified here, so commands can no longer silently drift
|
|
|
|
|
/// out of classification the way the parallel switches allowed.
|
|
|
|
|
var traits: CommandTraits {
|
|
|
|
|
switch self {
|
|
|
|
|
// Interaction commands: require the foreground-guard + stabilization preflight.
|
|
|
|
|
// tapSeries/dragSeries are the series forms of tap/drag; keyboardReturn is the sibling
|
|
|
|
|
// of keyboardDismiss — all three were missing from the historical switch (drift the
|
|
|
|
|
// table now prevents) and are classified as interactions here.
|
|
|
|
|
case .tap, .tapSeries, .longPress, .drag, .dragSeries, .remotePress, .type, .swipe,
|
|
|
|
|
.back, .backInApp, .backSystem, .rotate, .appSwitcher,
|
|
|
|
|
.keyboardDismiss, .keyboardReturn, .pinch, .rotateGesture, .transformGesture:
|
|
|
|
|
return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
|
|
|
|
|
|
|
|
|
|
// Read-only reads: eligible for the session-invalidating retry.
|
|
|
|
|
case .interactionFrame, .findText, .readText, .snapshot:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: false)
|
|
|
|
|
|
|
|
|
|
// Screenshot is both a read and a runner-lifecycle command (skips app-activation preflight).
|
|
|
|
|
case .screenshot:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: true)
|
|
|
|
|
|
|
|
|
|
// Alert is read-only only for its `get` action (resolved by isReadOnlyCommand).
|
|
|
|
|
case .alert:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .conditional, isLifecycle: false)
|
|
|
|
|
|
|
|
|
|
// Runner-lifecycle commands: skip the app-activation preflight.
|
|
|
|
|
case .recordStop, .uptime, .shutdown:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
|
|
|
|
|
|
2026-06-01 20:40:09 -05:00
|
|
|
case .status:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: true)
|
|
|
|
|
|
2026-06-01 18:11:04 +02:00
|
|
|
// Normal preflight, not retried.
|
|
|
|
|
// NOTE: mouseClick stays non-interaction for now — it is macOS-only and the foreground
|
|
|
|
|
// guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
|
|
|
|
|
// check first (tracked as a follow-up). Also preserved: querySelector is NOT read-only;
|
|
|
|
|
// recordStart is NOT a lifecycle command; home/alert remain non-interaction by design.
|
|
|
|
|
case .mouseClick, .querySelector, .home, .recordStart:
|
|
|
|
|
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:38:59 +01:00
|
|
|
struct Command: Codable {
|
|
|
|
|
let command: CommandType
|
2026-06-01 20:40:09 -05:00
|
|
|
let commandId: String?
|
|
|
|
|
let statusCommandId: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let appBundleId: String?
|
|
|
|
|
let text: String?
|
2026-05-19 11:36:48 +02:00
|
|
|
let selectorKey: String?
|
|
|
|
|
let selectorValue: String?
|
2026-05-27 21:45:51 +02:00
|
|
|
let allowNonHittableCoordinateFallback: Bool?
|
2026-03-27 10:43:52 +01:00
|
|
|
let delayMs: Int?
|
2026-05-19 11:36:48 +02:00
|
|
|
let textEntryMode: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let clearFirst: Bool?
|
|
|
|
|
let action: String?
|
|
|
|
|
let x: Double?
|
|
|
|
|
let y: Double?
|
2026-03-23 16:01:52 +01:00
|
|
|
let button: String?
|
2026-05-12 15:32:21 +07:00
|
|
|
let remoteButton: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let count: Double?
|
|
|
|
|
let intervalMs: Double?
|
|
|
|
|
let doubleTap: Bool?
|
|
|
|
|
let pauseMs: Double?
|
|
|
|
|
let pattern: String?
|
|
|
|
|
let x2: Double?
|
|
|
|
|
let y2: Double?
|
2026-05-22 18:01:58 +02:00
|
|
|
let dx: Double?
|
|
|
|
|
let dy: Double?
|
2026-02-28 13:38:59 +01:00
|
|
|
let durationMs: Double?
|
2026-03-27 12:06:57 +01:00
|
|
|
let direction: String?
|
2026-04-01 20:34:56 +02:00
|
|
|
let orientation: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let scale: Double?
|
2026-05-22 18:01:58 +02:00
|
|
|
let degrees: Double?
|
|
|
|
|
let velocity: Double?
|
2026-02-28 13:38:59 +01:00
|
|
|
let outPath: String?
|
|
|
|
|
let fps: Int?
|
2026-04-15 13:18:57 +02:00
|
|
|
let quality: Int?
|
2026-02-28 13:38:59 +01:00
|
|
|
let interactiveOnly: Bool?
|
|
|
|
|
let compact: Bool?
|
|
|
|
|
let depth: Int?
|
|
|
|
|
let scope: String?
|
|
|
|
|
let raw: Bool?
|
2026-03-28 19:06:52 +01:00
|
|
|
let fullscreen: Bool?
|
2026-02-28 13:38:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Response: Codable {
|
|
|
|
|
let ok: Bool
|
|
|
|
|
let data: DataPayload?
|
|
|
|
|
let error: ErrorPayload?
|
|
|
|
|
|
|
|
|
|
init(ok: Bool, data: DataPayload? = nil, error: ErrorPayload? = nil) {
|
|
|
|
|
self.ok = ok
|
|
|
|
|
self.data = data
|
|
|
|
|
self.error = error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct DataPayload: Codable {
|
|
|
|
|
let message: String?
|
2026-03-25 17:13:31 +01:00
|
|
|
let text: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let found: Bool?
|
|
|
|
|
let items: [String]?
|
|
|
|
|
let nodes: [SnapshotNode]?
|
|
|
|
|
let truncated: Bool?
|
2026-03-24 13:38:41 +01:00
|
|
|
let gestureStartUptimeMs: Double?
|
|
|
|
|
let gestureEndUptimeMs: Double?
|
|
|
|
|
let x: Double?
|
|
|
|
|
let y: Double?
|
|
|
|
|
let x2: Double?
|
|
|
|
|
let y2: Double?
|
|
|
|
|
let referenceWidth: Double?
|
|
|
|
|
let referenceHeight: Double?
|
|
|
|
|
let currentUptimeMs: Double?
|
2026-06-01 20:40:09 -05:00
|
|
|
let commandId: String?
|
|
|
|
|
let lifecycleState: String?
|
|
|
|
|
let lifecycleCommand: String?
|
|
|
|
|
let lifecycleResponseOk: Bool?
|
|
|
|
|
let lifecycleResponseJson: String?
|
|
|
|
|
let lifecycleErrorCode: String?
|
|
|
|
|
let lifecycleErrorMessage: String?
|
|
|
|
|
let lifecycleErrorHint: String?
|
2026-03-27 11:49:30 +01:00
|
|
|
let visible: Bool?
|
|
|
|
|
let wasVisible: Bool?
|
|
|
|
|
let dismissed: Bool?
|
2026-04-01 20:34:56 +02:00
|
|
|
let orientation: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
|
|
|
|
|
init(
|
|
|
|
|
message: String? = nil,
|
2026-03-25 17:13:31 +01:00
|
|
|
text: String? = nil,
|
2026-02-28 13:38:59 +01:00
|
|
|
found: Bool? = nil,
|
|
|
|
|
items: [String]? = nil,
|
|
|
|
|
nodes: [SnapshotNode]? = nil,
|
2026-03-24 13:38:41 +01:00
|
|
|
truncated: Bool? = nil,
|
|
|
|
|
gestureStartUptimeMs: Double? = nil,
|
|
|
|
|
gestureEndUptimeMs: Double? = nil,
|
|
|
|
|
x: Double? = nil,
|
|
|
|
|
y: Double? = nil,
|
|
|
|
|
x2: Double? = nil,
|
|
|
|
|
y2: Double? = nil,
|
|
|
|
|
referenceWidth: Double? = nil,
|
|
|
|
|
referenceHeight: Double? = nil,
|
2026-03-27 11:49:30 +01:00
|
|
|
currentUptimeMs: Double? = nil,
|
2026-06-01 20:40:09 -05:00
|
|
|
commandId: String? = nil,
|
|
|
|
|
lifecycleState: String? = nil,
|
|
|
|
|
lifecycleCommand: String? = nil,
|
|
|
|
|
lifecycleResponseOk: Bool? = nil,
|
|
|
|
|
lifecycleResponseJson: String? = nil,
|
|
|
|
|
lifecycleErrorCode: String? = nil,
|
|
|
|
|
lifecycleErrorMessage: String? = nil,
|
|
|
|
|
lifecycleErrorHint: String? = nil,
|
2026-03-27 11:49:30 +01:00
|
|
|
visible: Bool? = nil,
|
|
|
|
|
wasVisible: Bool? = nil,
|
2026-04-01 20:34:56 +02:00
|
|
|
dismissed: Bool? = nil,
|
|
|
|
|
orientation: String? = nil
|
2026-02-28 13:38:59 +01:00
|
|
|
) {
|
|
|
|
|
self.message = message
|
2026-03-25 17:13:31 +01:00
|
|
|
self.text = text
|
2026-02-28 13:38:59 +01:00
|
|
|
self.found = found
|
|
|
|
|
self.items = items
|
|
|
|
|
self.nodes = nodes
|
|
|
|
|
self.truncated = truncated
|
2026-03-24 13:38:41 +01:00
|
|
|
self.gestureStartUptimeMs = gestureStartUptimeMs
|
|
|
|
|
self.gestureEndUptimeMs = gestureEndUptimeMs
|
|
|
|
|
self.x = x
|
|
|
|
|
self.y = y
|
|
|
|
|
self.x2 = x2
|
|
|
|
|
self.y2 = y2
|
|
|
|
|
self.referenceWidth = referenceWidth
|
|
|
|
|
self.referenceHeight = referenceHeight
|
|
|
|
|
self.currentUptimeMs = currentUptimeMs
|
2026-06-01 20:40:09 -05:00
|
|
|
self.commandId = commandId
|
|
|
|
|
self.lifecycleState = lifecycleState
|
|
|
|
|
self.lifecycleCommand = lifecycleCommand
|
|
|
|
|
self.lifecycleResponseOk = lifecycleResponseOk
|
|
|
|
|
self.lifecycleResponseJson = lifecycleResponseJson
|
|
|
|
|
self.lifecycleErrorCode = lifecycleErrorCode
|
|
|
|
|
self.lifecycleErrorMessage = lifecycleErrorMessage
|
|
|
|
|
self.lifecycleErrorHint = lifecycleErrorHint
|
2026-03-27 11:49:30 +01:00
|
|
|
self.visible = visible
|
|
|
|
|
self.wasVisible = wasVisible
|
|
|
|
|
self.dismissed = dismissed
|
2026-04-01 20:34:56 +02:00
|
|
|
self.orientation = orientation
|
2026-02-28 13:38:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ErrorPayload: Codable {
|
2026-03-27 11:49:30 +01:00
|
|
|
let code: String?
|
2026-02-28 13:38:59 +01:00
|
|
|
let message: String
|
2026-06-01 15:10:53 +02:00
|
|
|
let hint: String?
|
2026-03-27 11:49:30 +01:00
|
|
|
|
2026-06-01 15:10:53 +02:00
|
|
|
init(code: String? = nil, message: String, hint: String? = nil) {
|
2026-03-27 11:49:30 +01:00
|
|
|
self.code = code
|
|
|
|
|
self.message = message
|
2026-06-01 15:10:53 +02:00
|
|
|
self.hint = hint
|
2026-03-27 11:49:30 +01:00
|
|
|
}
|
2026-02-28 13:38:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SnapshotRect: Codable {
|
|
|
|
|
let x: Double
|
|
|
|
|
let y: Double
|
|
|
|
|
let width: Double
|
|
|
|
|
let height: Double
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SnapshotNode: Codable {
|
|
|
|
|
let index: Int
|
|
|
|
|
let type: String
|
|
|
|
|
let label: String?
|
|
|
|
|
let identifier: String?
|
|
|
|
|
let value: String?
|
|
|
|
|
let rect: SnapshotRect
|
|
|
|
|
let enabled: Bool
|
2026-05-12 15:32:21 +07:00
|
|
|
let focused: Bool?
|
2026-05-19 11:36:48 +02:00
|
|
|
let selected: Bool?
|
2026-02-28 13:38:59 +01:00
|
|
|
let hittable: Bool
|
|
|
|
|
let depth: Int
|
2026-04-01 19:00:02 +02:00
|
|
|
let parentIndex: Int?
|
|
|
|
|
let hiddenContentAbove: Bool?
|
|
|
|
|
let hiddenContentBelow: Bool?
|
2026-02-28 13:38:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SnapshotOptions {
|
|
|
|
|
let interactiveOnly: Bool
|
|
|
|
|
let compact: Bool
|
|
|
|
|
let depth: Int?
|
|
|
|
|
let scope: String?
|
|
|
|
|
let raw: Bool
|
|
|
|
|
}
|