agent-device 0.21.12 → 0.21.13
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.
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.12.apk → agent-device-android-ime-helper-0.21.13.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.13.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.12.manifest.json → agent-device-android-ime-helper-0.21.13.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.12.apk → agent-device-android-snapshot-helper-0.21.13.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.13.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.12.manifest.json → agent-device-android-snapshot-helper-0.21.13.manifest.json} +6 -6
- package/apple/macos-helper/Package.swift +4 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +3 -6
- package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/ScreenshotResponseTests.swift +15 -0
- package/apple/snapshot-bridge/SnapshotBridgeRuntime.m +47 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +99 -42
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/Info.plist +23 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +17 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +21 -13
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift +1 -162
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -9
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -56
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSystemSurfaceHostPolicy.swift +0 -48
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -50
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +22 -568
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +21 -9
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift +0 -36
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandDispatch.swift +630 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +18 -2236
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -338
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -12
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+GestureExecution.swift +165 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -24
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +0 -142
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +90 -39
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+MainThreadWork.swift +78 -106
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +23 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +147 -95
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +0 -209
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +86 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift +287 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +1 -200
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -185
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +75 -285
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +23 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotBackendCapabilities.swift +0 -78
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +83 -578
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCaptureTarget.swift +106 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotExecution.swift +142 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTiming.swift +49 -78
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +0 -44
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +27 -97
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +1 -11
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +11 -87
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +2 -10
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +1 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +0 -35
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputCandidatePolicy.swift +0 -31
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +2 -18
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +34 -52
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +142 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +22 -6
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +140 -20
- package/dist/src/absence-observation-errors.js +1 -1
- package/dist/src/adapter.js +1 -1
- package/dist/src/agent-device-client.js +1 -1
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/android-adb.d.ts +1 -1
- package/dist/src/app-launch.js +2 -2
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/back-mode.js +1 -1
- package/dist/src/capture-disclosure.js +1 -1
- package/dist/src/capture.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +6 -5
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +1 -1
- package/dist/src/client-types.d.ts +4 -5
- package/dist/src/code-signature.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/command-tools.js +3 -3
- package/dist/src/connection.js +1 -1
- package/dist/src/daemon-client-lifecycle.js +1 -1
- package/dist/src/daemon-resolution.js +1 -1
- package/dist/src/device-claim-rule.js +2 -2
- package/dist/src/device-input-state.d.ts +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/devicectl.js +1 -1
- package/dist/src/display-inventory.js +1 -1
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/gesture-plan-types.d.ts +38 -0
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/helper.js +1 -1
- package/dist/src/host.js +1 -1
- package/dist/src/install-source-network-transport.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -4
- package/dist/src/interactor2.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +2 -2
- package/dist/src/inventory2.js +1 -1
- package/dist/src/ios-snapshot-acquisition.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/limrun-runtime-dependencies.js +1 -1
- package/dist/src/limrun.d.ts +435 -4
- package/dist/src/linux.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +4 -4
- package/dist/src/mobile-snapshot-semantics.js +1 -1
- package/dist/src/perf-facade.js +1 -2
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/perf-target.js +2 -0
- package/dist/src/physical-device-apps.js +1 -0
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/provider-webdriver.js +1 -1
- package/dist/src/proxy.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/registry.js +35 -35
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-dev-tools-security.js +1 -1
- package/dist/src/runner-device-readiness.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-error-classification.js +3 -0
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-presentation.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/runtime10.js +1 -1
- package/dist/src/runtime2.js +2 -2
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -1
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/screenshot3.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-command.d.ts +377 -0
- package/dist/src/scroll-command.js +1 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +9 -0
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-selectors.d.ts +23 -726
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/semantic-index.js +1 -1
- package/dist/src/server.js +1 -1
- package/dist/src/session-observation.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session2.js +2 -2
- package/dist/src/simctl-facade.js +1 -1
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-state.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/tool-provider.js +1 -1
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/tree.js +1 -1
- package/dist/src/update-check.js +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/web2.js +1 -1
- package/package.json +3 -2
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.12.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.12.apk.sha256 +0 -1
- package/dist/src/app-inventory.d.ts +0 -4
- package/dist/src/apps-simctl.js +0 -1
- package/dist/src/cloud-artifacts.d.ts +0 -61
- package/dist/src/runner-contract.js +0 -3
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import AgentDeviceSnapshotPresentation
|
|
3
|
+
|
|
4
|
+
extension RunnerTests {
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
func executeAccepted(command: Command) throws -> Response {
|
|
8
|
+
commandJournal.start(command: command)
|
|
9
|
+
pendingTargetActivation = nil
|
|
10
|
+
do {
|
|
11
|
+
let response = try executeDispatched(command: command)
|
|
12
|
+
commandJournal.finish(command: command, response: response)
|
|
13
|
+
guard let fact = pendingTargetActivation else { return response }
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
pendingTargetActivation = nil
|
|
18
|
+
return response.stampingTargetActivation(fact)
|
|
19
|
+
} catch {
|
|
20
|
+
pendingTargetActivation = nil
|
|
21
|
+
commandJournal.fail(command: command, error: error)
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func executeStatus(command: Command) -> Response {
|
|
27
|
+
guard
|
|
28
|
+
let statusCommandId = command.statusCommandId?.trimmedNonEmpty
|
|
29
|
+
else {
|
|
30
|
+
return Response(
|
|
31
|
+
ok: false,
|
|
32
|
+
error: ErrorPayload(
|
|
33
|
+
code: "INVALID_ARGS",
|
|
34
|
+
message: "status requires statusCommandId",
|
|
35
|
+
hint: "Set statusCommandId to the commandId of the runner command to inspect."
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return Response(ok: true, data: commandJournal.status(normalizedCommandId: statusCommandId))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func executeUptime() -> Response {
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Response(
|
|
46
|
+
ok: true,
|
|
47
|
+
data: DataPayload(currentUptimeMs: currentUptimeMs())
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
struct ActiveCommandContext {
|
|
52
|
+
let app: XCUIApplication
|
|
53
|
+
|
|
54
|
+
var systemSurface: SystemSurfaceHost? = nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enum ActiveCommandPreparation {
|
|
58
|
+
case response(Response)
|
|
59
|
+
case context(ActiveCommandContext)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func runnerBusyResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
|
|
63
|
+
NSLog(
|
|
64
|
+
"AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f",
|
|
65
|
+
command.command.rawValue,
|
|
66
|
+
command.commandId ?? "",
|
|
67
|
+
abandonedForSeconds
|
|
68
|
+
)
|
|
69
|
+
return Response(
|
|
70
|
+
ok: false,
|
|
71
|
+
error: ErrorPayload(
|
|
72
|
+
code: "RUNNER_BUSY",
|
|
73
|
+
message:
|
|
74
|
+
"The iOS runner is still finishing a previous command that exceeded its execution watchdog (usually an accessibility capture on a heavy or animating screen).",
|
|
75
|
+
hint:
|
|
76
|
+
"Wait a few seconds and retry. If snapshots keep failing on this screen, use screenshot as visual truth and interact by coordinates, or navigate to another screen."
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private func runnerWedgedResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
|
|
82
|
+
NSLog(
|
|
83
|
+
"AGENT_DEVICE_RUNNER_WEDGED command=%@ commandId=%@ abandonedForSeconds=%.1f",
|
|
84
|
+
command.command.rawValue,
|
|
85
|
+
command.commandId ?? "",
|
|
86
|
+
abandonedForSeconds
|
|
87
|
+
)
|
|
88
|
+
return Response(
|
|
89
|
+
ok: false,
|
|
90
|
+
error: ErrorPayload(
|
|
91
|
+
code: "RUNNER_WEDGED",
|
|
92
|
+
message:
|
|
93
|
+
"The iOS runner main thread has been stuck in abandoned work for \(Int(abandonedForSeconds)) seconds and cannot recover on its own.",
|
|
94
|
+
hint:
|
|
95
|
+
"The runner session will be restarted. Retry the command after the restart; if this screen keeps wedging captures, use screenshot as visual truth and interact by coordinates."
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private func runnerUnavailableResponse(command: Command) -> Response? {
|
|
101
|
+
switch currentMainThreadBusyState() {
|
|
102
|
+
case .idle:
|
|
103
|
+
return nil
|
|
104
|
+
case .busy(let abandonedForSeconds):
|
|
105
|
+
return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
106
|
+
case .wedged(let abandonedForSeconds):
|
|
107
|
+
return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func executeDispatched(command: Command) throws -> Response {
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if let unavailable = runnerUnavailableResponse(command: command) {
|
|
117
|
+
return unavailable
|
|
118
|
+
}
|
|
119
|
+
let alertDeadline = command.command == .alert
|
|
120
|
+
? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs))
|
|
121
|
+
: nil
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command)
|
|
126
|
+
if let unavailable = runnerUnavailableResponse(command: command) {
|
|
127
|
+
return unavailable
|
|
128
|
+
}
|
|
129
|
+
if command.command == .snapshot {
|
|
130
|
+
return try executeSnapshotDispatched(command: command)
|
|
131
|
+
}
|
|
132
|
+
if command.command == .alert, let deadline = alertDeadline {
|
|
133
|
+
return try runMainThreadWork(
|
|
134
|
+
"command_execution",
|
|
135
|
+
timeout: max(0.001, deadline.timeIntervalSinceNow),
|
|
136
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
137
|
+
) {
|
|
138
|
+
try self.executeOnMainSafely(
|
|
139
|
+
command: command,
|
|
140
|
+
alertDeadline: deadline,
|
|
141
|
+
routeToSpringboard: routeToSpringboard
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return try runMainThreadWork(
|
|
146
|
+
"command_execution",
|
|
147
|
+
timeout: mainThreadExecutionTimeout,
|
|
148
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
149
|
+
) {
|
|
150
|
+
try self.executeOnMainSafely(command: command, routeToSpringboard: routeToSpringboard)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
private func executeOnMainSafely(
|
|
157
|
+
command: Command,
|
|
158
|
+
alertDeadline: Date? = nil,
|
|
159
|
+
routeToSpringboard: Bool
|
|
160
|
+
) throws -> Response {
|
|
161
|
+
var hasRetried = false
|
|
162
|
+
while true {
|
|
163
|
+
var response: Response?
|
|
164
|
+
var swiftError: Error?
|
|
165
|
+
let failureCountBefore = currentXCTestFailureCount()
|
|
166
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
167
|
+
do {
|
|
168
|
+
response = try self.executeOnMain(
|
|
169
|
+
command: command,
|
|
170
|
+
alertDeadline: alertDeadline,
|
|
171
|
+
routeToSpringboard: routeToSpringboard
|
|
172
|
+
)
|
|
173
|
+
} catch {
|
|
174
|
+
swiftError = error
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if let exceptionMessage {
|
|
179
|
+
invalidateCachedTarget(reason: "objc_exception")
|
|
180
|
+
if !hasRetried, shouldRetryException(command, message: exceptionMessage) {
|
|
181
|
+
NSLog(
|
|
182
|
+
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=objc_exception",
|
|
183
|
+
command.command.rawValue
|
|
184
|
+
)
|
|
185
|
+
hasRetried = true
|
|
186
|
+
sleepFor(retryCooldown)
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
throw NSError(
|
|
190
|
+
domain: RunnerErrorDomain.exception,
|
|
191
|
+
code: RunnerErrorCode.objcException,
|
|
192
|
+
userInfo: [NSLocalizedDescriptionKey: exceptionMessage]
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
if let swiftError {
|
|
196
|
+
throw swiftError
|
|
197
|
+
}
|
|
198
|
+
guard let response else {
|
|
199
|
+
throw NSError(
|
|
200
|
+
domain: RunnerErrorDomain.general,
|
|
201
|
+
code: RunnerErrorCode.commandReturnedNoResponse,
|
|
202
|
+
userInfo: [NSLocalizedDescriptionKey: "command returned no response"]
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if didRecordXCTestFailure(since: failureCountBefore),
|
|
220
|
+
let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
|
|
221
|
+
{
|
|
222
|
+
invalidateCachedTarget(reason: "xctest_recorded_failure")
|
|
223
|
+
return failureResponse
|
|
224
|
+
}
|
|
225
|
+
if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
|
|
226
|
+
NSLog(
|
|
227
|
+
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
|
|
228
|
+
command.command.rawValue
|
|
229
|
+
)
|
|
230
|
+
hasRetried = true
|
|
231
|
+
invalidateCachedTarget(reason: "response_unavailable")
|
|
232
|
+
sleepFor(retryCooldown)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
return response
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
func executeDispatchedWithRecovery(
|
|
243
|
+
command: Command,
|
|
244
|
+
perform: () throws -> Response
|
|
245
|
+
) throws -> Response {
|
|
246
|
+
var hasRetried = false
|
|
247
|
+
while true {
|
|
248
|
+
let failureCountBefore = try runMainThreadWork(
|
|
249
|
+
"recorded_failure_count",
|
|
250
|
+
timeout: mainThreadExecutionTimeout,
|
|
251
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
252
|
+
) {
|
|
253
|
+
self.currentXCTestFailureCount()
|
|
254
|
+
}
|
|
255
|
+
let response = try perform()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if hasAbandonedMainThreadWork() {
|
|
259
|
+
NSLog(
|
|
260
|
+
"AGENT_DEVICE_RUNNER_DISPATCH_RECOVERY_SKIPPED_XCTEST_OCCUPIED command=%@",
|
|
261
|
+
command.command.rawValue
|
|
262
|
+
)
|
|
263
|
+
return response
|
|
264
|
+
}
|
|
265
|
+
let recordedFailureResponse = try runMainThreadWork(
|
|
266
|
+
"recorded_failure_count",
|
|
267
|
+
timeout: mainThreadExecutionTimeout,
|
|
268
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
269
|
+
) {
|
|
270
|
+
self.didRecordXCTestFailure(since: failureCountBefore)
|
|
271
|
+
? self.xctestRecordedFailureResponse(command: command, response: response)
|
|
272
|
+
: nil
|
|
273
|
+
}
|
|
274
|
+
if let recordedFailureResponse {
|
|
275
|
+
try runMainThreadWork(
|
|
276
|
+
"target_invalidation",
|
|
277
|
+
timeout: mainThreadExecutionTimeout,
|
|
278
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
279
|
+
) {
|
|
280
|
+
self.invalidateCachedTarget(reason: "xctest_recorded_failure")
|
|
281
|
+
}
|
|
282
|
+
return recordedFailureResponse
|
|
283
|
+
}
|
|
284
|
+
if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
|
|
285
|
+
NSLog(
|
|
286
|
+
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
|
|
287
|
+
command.command.rawValue
|
|
288
|
+
)
|
|
289
|
+
hasRetried = true
|
|
290
|
+
try runMainThreadWork(
|
|
291
|
+
"target_invalidation",
|
|
292
|
+
timeout: mainThreadExecutionTimeout,
|
|
293
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
294
|
+
) {
|
|
295
|
+
self.invalidateCachedTarget(reason: "response_unavailable")
|
|
296
|
+
self.sleepFor(self.retryCooldown)
|
|
297
|
+
}
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
return response
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private func executeOnMain(
|
|
305
|
+
command: Command,
|
|
306
|
+
alertDeadline: Date?,
|
|
307
|
+
routeToSpringboard: Bool
|
|
308
|
+
) throws -> Response {
|
|
309
|
+
let preparation = prepareActiveCommandContext(
|
|
310
|
+
command: command,
|
|
311
|
+
routeToSpringboard: routeToSpringboard
|
|
312
|
+
)
|
|
313
|
+
let activeApp: XCUIApplication
|
|
314
|
+
switch preparation {
|
|
315
|
+
case .response(let response):
|
|
316
|
+
return response
|
|
317
|
+
case .context(let context):
|
|
318
|
+
activeApp = context.app
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
switch command.command {
|
|
322
|
+
case .status:
|
|
323
|
+
return executeStatus(command: command)
|
|
324
|
+
case .targetReset:
|
|
325
|
+
return resetTargetAfterExternalRelaunch()
|
|
326
|
+
case .shutdown:
|
|
327
|
+
stopRecordingIfNeeded()
|
|
328
|
+
return Response(ok: true, data: DataPayload(message: "shutdown"))
|
|
329
|
+
case .recordStart:
|
|
330
|
+
guard
|
|
331
|
+
let requestedOutPath = command.outPath?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
332
|
+
!requestedOutPath.isEmpty
|
|
333
|
+
else {
|
|
334
|
+
return Response(ok: false, error: ErrorPayload(message: "recordStart requires outPath"))
|
|
335
|
+
}
|
|
336
|
+
let hasAppBundleId = !(command.appBundleId?
|
|
337
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
338
|
+
.isEmpty ?? true)
|
|
339
|
+
guard hasAppBundleId else {
|
|
340
|
+
return Response(ok: false, error: ErrorPayload(message: "recordStart requires appBundleId"))
|
|
341
|
+
}
|
|
342
|
+
if activeRecording != nil {
|
|
343
|
+
return Response(ok: false, error: ErrorPayload(message: "recording already in progress"))
|
|
344
|
+
}
|
|
345
|
+
if let requestedFps = command.fps, (requestedFps < minRecordingFps || requestedFps > maxRecordingFps) {
|
|
346
|
+
return Response(ok: false, error: ErrorPayload(message: "recordStart fps must be between \(minRecordingFps) and \(maxRecordingFps)"))
|
|
347
|
+
}
|
|
348
|
+
do {
|
|
349
|
+
let resolvedOutPath = resolveRecordingOutPath(requestedOutPath)
|
|
350
|
+
let fpsLabel = command.fps.map(String.init) ?? String(RunnerTests.defaultRecordingFps)
|
|
351
|
+
NSLog(
|
|
352
|
+
"AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@",
|
|
353
|
+
requestedOutPath,
|
|
354
|
+
resolvedOutPath,
|
|
355
|
+
fpsLabel
|
|
356
|
+
)
|
|
357
|
+
let recorder = ScreenRecorder(
|
|
358
|
+
outputPath: resolvedOutPath,
|
|
359
|
+
fps: command.fps.map { Int32($0) }
|
|
360
|
+
)
|
|
361
|
+
try startRecording(recorder) { [weak self] in
|
|
362
|
+
guard let self else { return .failure(.unresolvedScreen) }
|
|
363
|
+
return self.captureRunnerFrameResult(app: activeApp)
|
|
364
|
+
}
|
|
365
|
+
activeRecording = recorder
|
|
366
|
+
return Response(ok: true, data: DataPayload(message: "recording started"))
|
|
367
|
+
} catch {
|
|
368
|
+
activeRecording = nil
|
|
369
|
+
return Response(ok: false, error: Self.recordingStartErrorPayload(for: error))
|
|
370
|
+
}
|
|
371
|
+
case .recordStop:
|
|
372
|
+
guard let recorder = activeRecording else {
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
return Response(ok: true, data: DataPayload(message: "recording already stopped"))
|
|
378
|
+
}
|
|
379
|
+
do {
|
|
380
|
+
try recorder.stop()
|
|
381
|
+
activeRecording = nil
|
|
382
|
+
return Response(ok: true, data: DataPayload(message: "recording stopped"))
|
|
383
|
+
} catch {
|
|
384
|
+
activeRecording = nil
|
|
385
|
+
return Response(ok: false, error: ErrorPayload(message: "failed to stop recording: \(error.localizedDescription)"))
|
|
386
|
+
}
|
|
387
|
+
case .uptime:
|
|
388
|
+
return executeUptime()
|
|
389
|
+
case .activate:
|
|
390
|
+
guard
|
|
391
|
+
let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
392
|
+
!bundleId.isEmpty
|
|
393
|
+
else {
|
|
394
|
+
return Response(ok: false, error: ErrorPayload(message: "activate requires appBundleId"))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
return Response(ok: true, data: DataPayload(message: "app activated"))
|
|
399
|
+
case .terminate:
|
|
400
|
+
guard
|
|
401
|
+
let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
402
|
+
!bundleId.isEmpty
|
|
403
|
+
else {
|
|
404
|
+
return Response(ok: false, error: ErrorPayload(message: "terminate requires appBundleId"))
|
|
405
|
+
}
|
|
406
|
+
XCUIApplication(bundleIdentifier: bundleId).terminate()
|
|
407
|
+
if currentBundleId == bundleId {
|
|
408
|
+
invalidateCachedTarget(reason: "target_terminated")
|
|
409
|
+
}
|
|
410
|
+
return Response(ok: true, data: DataPayload(message: "app terminated"))
|
|
411
|
+
default:
|
|
412
|
+
break
|
|
413
|
+
}
|
|
414
|
+
return try executeOnMainPrepared(
|
|
415
|
+
command: command,
|
|
416
|
+
activeApp: activeApp,
|
|
417
|
+
alertDeadline: alertDeadline
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
func prepareActiveCommandContext(
|
|
422
|
+
command: Command,
|
|
423
|
+
routeToSpringboard: Bool = false
|
|
424
|
+
) -> ActiveCommandPreparation {
|
|
425
|
+
var activeApp = currentApp ?? app
|
|
426
|
+
var systemSurface: SystemSurfaceHost? = nil
|
|
427
|
+
if routeToSpringboard {
|
|
428
|
+
activeApp = springboard
|
|
429
|
+
} else if shouldSkipAppActivationPreflight(command) {
|
|
430
|
+
activeApp = resolveAppWithoutActivation(command: command)
|
|
431
|
+
} else if let presented = presentedSystemSurfaceHost() {
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
activeApp = presented.app
|
|
436
|
+
systemSurface = presented.host
|
|
437
|
+
if isInteractionCommand(command.command) {
|
|
438
|
+
applyInteractionStabilizationIfNeeded()
|
|
439
|
+
}
|
|
440
|
+
} else if !isRunnerLifecycleCommand(command.command) {
|
|
441
|
+
let normalizedBundleId = command.appBundleId?
|
|
442
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
443
|
+
let requestedBundleId = (normalizedBundleId?.isEmpty == true) ? nil : normalizedBundleId
|
|
444
|
+
if let bundleId = requestedBundleId,
|
|
445
|
+
let notRunning = notRunningReadResponse(command: command, bundleId: bundleId)
|
|
446
|
+
{
|
|
447
|
+
return .response(notRunning)
|
|
448
|
+
}
|
|
449
|
+
if let bundleId = requestedBundleId {
|
|
450
|
+
if currentBundleId != bundleId || currentApp == nil {
|
|
451
|
+
_ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
|
|
452
|
+
} else {
|
|
453
|
+
refreshCachedTargetIfProcessChanged(bundleId: bundleId)
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
|
|
457
|
+
invalidateCachedTarget(reason: "missing_app_bundle")
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
activeApp = currentApp ?? app
|
|
461
|
+
if let bundleId = requestedBundleId, targetNeedsActivation(activeApp) {
|
|
462
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "stale_target")
|
|
463
|
+
} else if requestedBundleId == nil, targetNeedsActivation(activeApp) {
|
|
464
|
+
ensureRunnerHostAppActive(reason: "missing_app_bundle")
|
|
465
|
+
activeApp = app
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let skipExistenceWait = canUseFastForegroundAppGuard(
|
|
469
|
+
activeApp: activeApp,
|
|
470
|
+
requestedBundleId: requestedBundleId
|
|
471
|
+
)
|
|
472
|
+
if !skipExistenceWait && !activeApp.waitForExistence(timeout: appExistenceTimeout) {
|
|
473
|
+
if let bundleId = requestedBundleId {
|
|
474
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "missing_after_wait")
|
|
475
|
+
guard activeApp.waitForExistence(timeout: appExistenceTimeout) else {
|
|
476
|
+
return .response(Response(ok: false, error: .targetAppUnavailable(bundleId: bundleId)))
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
return .response(Response(ok: false, error: .targetAppUnavailable(bundleId: nil)))
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if isInteractionCommand(command.command) {
|
|
484
|
+
if let bundleId = requestedBundleId, activeApp.state != .runningForeground {
|
|
485
|
+
activeApp = activateTarget(bundleId: bundleId, reason: "interaction_foreground_guard")
|
|
486
|
+
} else if requestedBundleId == nil, activeApp.state != .runningForeground {
|
|
487
|
+
ensureRunnerHostAppActive(reason: "interaction_missing_app_bundle")
|
|
488
|
+
activeApp = app
|
|
489
|
+
}
|
|
490
|
+
let skipInteractionExistenceWait = canUseFastForegroundAppGuard(
|
|
491
|
+
activeApp: activeApp,
|
|
492
|
+
requestedBundleId: requestedBundleId
|
|
493
|
+
)
|
|
494
|
+
if !skipInteractionExistenceWait && !activeApp.waitForExistence(timeout: 2) {
|
|
495
|
+
return .response(
|
|
496
|
+
Response(ok: false, error: .targetAppUnavailable(bundleId: requestedBundleId))
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
applyInteractionStabilizationIfNeeded()
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return .context(ActiveCommandContext(app: activeApp, systemSurface: systemSurface))
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
private func presentedSystemSurfaceHost() -> (host: SystemSurfaceHost, app: XCUIApplication)? {
|
|
510
|
+
#if os(iOS)
|
|
511
|
+
for host in SystemSurfaceHostRegistry.hosts {
|
|
512
|
+
let candidate = XCUIApplication(bundleIdentifier: host.bundleId)
|
|
513
|
+
if candidate.state == .runningForeground {
|
|
514
|
+
return (host, candidate)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return nil
|
|
518
|
+
#else
|
|
519
|
+
return nil
|
|
520
|
+
#endif
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
func currentXCTestFailureCount() -> Int {
|
|
524
|
+
return testRun?.failureCount ?? 0
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
func didRecordXCTestFailure(since failureCountBefore: Int) -> Bool {
|
|
528
|
+
return currentXCTestFailureCount() > failureCountBefore
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
func xctestRecordedFailureResponse(command: Command, response: Response) -> Response? {
|
|
532
|
+
guard response.ok else { return nil }
|
|
533
|
+
if response.data?.runnerFatal == true {
|
|
534
|
+
return nil
|
|
535
|
+
}
|
|
536
|
+
guard !isReadOnlyCommand(command), !isRunnerLifecycleCommand(command.command) else {
|
|
537
|
+
return nil
|
|
538
|
+
}
|
|
539
|
+
return Response(
|
|
540
|
+
ok: false,
|
|
541
|
+
error: ErrorPayload(
|
|
542
|
+
code: "XCTEST_RECORDED_FAILURE",
|
|
543
|
+
message: "XCTest recorded a failure while executing \(command.command.rawValue); the action may not have been performed.",
|
|
544
|
+
hint: "The iOS runner session was invalidated. Re-observe with a fresh snapshot before retrying; if the accessibility tree is unavailable, use screenshot plus coordinate commands instead of retrying the tap blindly."
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
|
|
550
|
+
#if os(iOS)
|
|
551
|
+
if command.command == .alert {
|
|
552
|
+
return true
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
if command.command == .actionButton {
|
|
560
|
+
return true
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
guard command.text == nil, command.selectorKey == nil else { return false }
|
|
567
|
+
guard hasCachedTargetForActivationSkip(command: command) else { return false }
|
|
568
|
+
return isCoordinateOnlyTap(command)
|
|
569
|
+
#else
|
|
570
|
+
return false
|
|
571
|
+
#endif
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
func shouldRouteToSpringboardBlockingSystemModal(
|
|
575
|
+
_ command: Command
|
|
576
|
+
) -> Bool {
|
|
577
|
+
#if os(iOS)
|
|
578
|
+
guard isCoordinateOnlyTap(command) else {
|
|
579
|
+
return false
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
let probeDeadline = Date().addingTimeInterval(systemModalProbeBudget)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
return boundedBlockingSystemAlertSnapshot(
|
|
592
|
+
deadline: probeDeadline,
|
|
593
|
+
penaltyTarget: .mainOwnedTarget
|
|
594
|
+
) != nil
|
|
595
|
+
#else
|
|
596
|
+
return false
|
|
597
|
+
#endif
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private func isCoordinateOnlyTap(_ command: Command) -> Bool {
|
|
601
|
+
return command.command == .tap
|
|
602
|
+
&& command.text == nil
|
|
603
|
+
&& command.selectorKey == nil
|
|
604
|
+
&& command.x != nil
|
|
605
|
+
&& command.y != nil
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
|
|
609
|
+
guard let currentApp, currentApp.state == .runningForeground else { return false }
|
|
610
|
+
guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
611
|
+
!bundleId.isEmpty
|
|
612
|
+
else {
|
|
613
|
+
return true
|
|
614
|
+
}
|
|
615
|
+
return currentBundleId == bundleId
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
func resolveAppWithoutActivation(command: Command) -> XCUIApplication {
|
|
619
|
+
guard let bundleId = command.appBundleId?
|
|
620
|
+
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
621
|
+
!bundleId.isEmpty
|
|
622
|
+
else {
|
|
623
|
+
return currentApp ?? app
|
|
624
|
+
}
|
|
625
|
+
if currentBundleId == bundleId, let currentApp {
|
|
626
|
+
return currentApp
|
|
627
|
+
}
|
|
628
|
+
return XCUIApplication(bundleIdentifier: bundleId)
|
|
629
|
+
}
|
|
630
|
+
}
|