Skip to content

Commit 9f187e6

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/maestro-setPermissions
# Conflicts: # packages/contracts/src/client-settings.ts # packages/contracts/src/settings.ts # src/commands/capture/settings.ts
2 parents e656ff1 + 465af75 commit 9f187e6

117 files changed

Lines changed: 8532 additions & 746 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.fallowrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"exports": [
9696
"detachIosSimulatorRunnerSessionsForShutdown",
9797
"hasLiveIosRunnerSession",
98+
"releaseIosRunnerOnClose",
9899
"releaseSpeculativeIosRunnerSessionFor",
99100
"stopAllIosRunnerSessions"
100101
]
@@ -160,7 +161,6 @@
160161
"resolveRunnerAppBundleId",
161162
"detachIosSimulatorRunnerSessionsForShutdown",
162163
"getRunnerSessionSnapshot",
163-
"scheduleIosRunnerIdleStop",
164164
"stopIosRunnerSession",
165165
"stopAllIosRunnerSessions",
166166
"readStaleRunnerLease",

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
- Changed (android): the snapshot helper release manifest no longer carries `installArgs`, and the
6+
helper installs with a fixed `adb install -r` like the IME helper. The array only ever spelled
7+
`install -r` plus the `-t` that #2603 retired with the `testOnly` flag, so the manifest → flag →
8+
option → flag round trip and its allowlist carried nothing. Older manifests that still contain
9+
the field parse unchanged; the field is ignored. The adb provider `install` capability now takes
10+
only `replace` (#2364).
511
- Fixed: BrowserStack sessions honour `--provider-project`, `--provider-build`, and
612
`--provider-session-name`. The capability builder emitted the legacy JSON Wire keys `device`,
713
`os_version`, and `app` at the top level next to the W3C `bstack:options` block; the hub treats a
@@ -14,6 +20,21 @@
1420
BrowserStack runs for the session, as `bstack:options.appiumVersion`. Unset, BrowserStack falls
1521
back to Appium 1.x, which predates the `mobile:` commands the interactor issues (`deepLink`,
1622
`pressButton`, `activateApp`).
23+
- Changed (iOS runner): what recovers a stuck runner is decided by the recorded error, not by its
24+
wording. A readiness preflight marks the error it gives up with, and that marker is now the whole
25+
test for restarting the session and replaying the command — except for a request that was canceled,
26+
which that same catch also marks: a command nobody is going to send again has no restart to spend,
27+
and the session it would tear down may be one that still works. Two message checks decided it before,
28+
and a preflight reaches the caller in whatever shape its connect loop ended with — "Runner did not
29+
accept connection", "Runner endpoint probe failed", a killed `simctl` fallback, a post that ran out
30+
of its budget — so only some of those restarted and the rest failed the command. The other half is
31+
what no longer happens: when a slow
32+
boot spends the whole prepare budget, the health check reports "prepare ios-runner timed out", and
33+
that no longer wipes a restored `xcodebuild` artifact on the way to a rebuild — the runner session
34+
is invalidated and prepare retries with the artifact intact. Only a failure that indicts the
35+
artifact rebuilds it, so a runner that refuses the connection or never answers on any route still
36+
wipes it and rebuilds, which is what that rule is for.
37+
1738
- Changed (sessions): the implicit session is now keyed by workspace **and platform**, so one checkout
1839
can drive iOS and Android without inventing a `--session` name for every command (#2580). An
1940
implicit session was addressed by `cwd:<workspace>:default`, one slot per checkout, and it stayed

android/snapshot-helper/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ The final instrumentation result for the default `snapshot` mode includes:
137137
Failures return `ok=false`, `errorType`, and `message` in the final result.
138138

139139
The release manifest is a stable provider contract for the current helper protocol. Providers should
140-
resolve the APK from `apkUrl`, verify `sha256`, install using `installArgs`, and run
141-
`instrumentationRunner`. `installArgs` must start with `install`; extra arguments are limited to the
142-
allowlisted adb install flags `-r`, `-t`, `-d`, and `-g`, and the consumer appends the APK path.
140+
resolve the APK from `apkUrl`, verify `sha256`, install it with `adb install -r <apk>`, and run
141+
`instrumentationRunner`. Manifests up to 0.21.3 also carried an `installArgs` array; it only ever
142+
spelled `install -r` (plus `-t` while the helper was `testOnly`), so consumers can ignore it.

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,14 @@ extension RunnerTests {
999999
case idle
10001000
case busy(abandonedForSeconds: TimeInterval)
10011001
case wedged(abandonedForSeconds: TimeInterval)
1002+
1003+
/// Whether the main thread is occupied by watchdog-abandoned work, for the occupancy stamp that
1004+
/// every successful response carries. Wedged is still occupied: it only differs in that a
1005+
/// restart, not waiting, is the cure.
1006+
var reportsMainThreadBusy: Bool {
1007+
if case .idle = self { return false }
1008+
return true
1009+
}
10021010
}
10031011

10041012
func currentMainThreadBusyState() -> MainThreadBusyState {
@@ -2187,7 +2195,7 @@ extension RunnerTests {
21872195
code: "UNSUPPORTED_OPERATION",
21882196
message: "Unable to dismiss the iOS keyboard: the keyboard exposes no dismiss key, and background taps are never attempted (no tap outside the keyboard can be proven side-effect-free)",
21892197
hint:
2190-
"The on-screen keyboard usually does not block agent-device interactions: press the next target directly instead of retrying dismiss. If that press fails or reports no visible effect, scroll the target into view, or use keyboard enter to press the return key when submission is wanted."
2198+
"An element whose center sits behind the on-screen keyboard is refused with tap_keyboard_occludes_target; one whose center stays above the keys presses normally. To end editing, tap the app's own Done/Cancel control, or use keyboard enter to press the return key when submission is wanted."
21912199
)
21922200
)
21932201
}

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,52 @@ extension RunnerTests {
183183
XCTAssertEqual(stamped.error?.message, "boom")
184184
}
185185

186+
func testStampingCurrentMainThreadBusyPreservesPayload() {
187+
let stamped = Response(ok: true, data: DataPayload(nodes: [], truncated: false))
188+
.stampingCurrentMainThreadBusy(true)
189+
190+
XCTAssertEqual(stamped.ok, true)
191+
XCTAssertEqual(stamped.data?.runnerMainThreadBusy, true)
192+
}
193+
194+
func testStampingCurrentMainThreadBusySkipsErrorResponses() {
195+
let response = Response(ok: false, error: ErrorPayload(code: "RUNNER_BUSY", message: "busy"))
196+
let stamped = response.stampingCurrentMainThreadBusy(true)
197+
198+
XCTAssertEqual(stamped.ok, false)
199+
XCTAssertNil(stamped.data)
200+
XCTAssertEqual(stamped.error?.code, "RUNNER_BUSY")
201+
}
202+
203+
func testMainThreadBusyStateReportsOccupancy() {
204+
XCTAssertFalse(MainThreadBusyState.idle.reportsMainThreadBusy)
205+
XCTAssertTrue(MainThreadBusyState.busy(abandonedForSeconds: 5).reportsMainThreadBusy)
206+
XCTAssertTrue(MainThreadBusyState.wedged(abandonedForSeconds: 200).reportsMainThreadBusy)
207+
XCTAssertEqual(
208+
Response(ok: true).stampingCurrentMainThreadBusy(false).data?.runnerMainThreadBusy, false)
209+
}
210+
211+
func testCommandFailedResponseTagsMainThreadTimeoutWithTypedCode() {
212+
let timeout = NSError(
213+
domain: RunnerErrorDomain.general,
214+
code: RunnerErrorCode.mainThreadExecutionTimedOut,
215+
userInfo: [NSLocalizedDescriptionKey: "main thread execution timed out"]
216+
)
217+
218+
let response = commandFailedResponse(from: timeout)
219+
220+
XCTAssertEqual(response.ok, false)
221+
XCTAssertEqual(response.error?.code, RunnerWireErrorCode.mainThreadTimeout)
222+
}
223+
224+
func testCommandFailedResponseKeepsGenericCodeForOtherErrors() {
225+
let other = NSError(domain: "SomeOtherDomain", code: 99, userInfo: nil)
226+
227+
let response = commandFailedResponse(from: other)
228+
229+
XCTAssertEqual(response.error?.code, "COMMAND_FAILED")
230+
}
231+
186232
func testJournalStoredResponseStaysUnstamped() throws {
187233
let journal = RunnerCommandJournal()
188234
let recordStart = runnerJournalCommand("recordStart", id: "record-start-anchor")

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ extension Response {
229229
payload.currentUptimeMs = value
230230
return Response(ok: ok, data: payload, error: error)
231231
}
232+
233+
// The daemon reads this occupancy flag to decide whether a healthy response proves the runner
234+
// drained its watchdog-abandoned main-thread work. Only successful responses carry it; a refusal
235+
// is itself the busy signal and needs no stamp.
236+
func stampingCurrentMainThreadBusy(_ value: Bool) -> Response {
237+
guard ok else { return self }
238+
var payload = data ?? DataPayload()
239+
payload.runnerMainThreadBusy = value
240+
return Response(ok: ok, data: payload, error: error)
241+
}
232242
}
233243

234244
struct DataPayload: Codable {
@@ -276,6 +286,11 @@ struct DataPayload: Codable {
276286
var textEntryRoute: String?
277287
var runnerFatal: Bool?
278288
var runnerFatalReason: String?
289+
/// Whether main-thread XCTest work past the execution watchdog is still draining when this
290+
/// response is written. A private-AX snapshot can be served successfully while an abandoned tree
291+
/// crawl still grinds, so the healthy response must carry the live occupancy rather than let the
292+
/// daemon read `ok` as proof the runner drained (#2552).
293+
var runnerMainThreadBusy: Bool?
279294
var completedSteps: Int?
280295
var failedStepIndex: Int?
281296
var sequenceResults: [SequenceStepResult]?

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,7 @@ extension RunnerTests {
152152
self.deliverCommandResult(
153153
command: command,
154154
result: (
155-
self.jsonResponse(
156-
status: 500,
157-
response: self.errorResponse(
158-
code: "COMMAND_FAILED",
159-
message: error.localizedDescription,
160-
hint: "Check the runner log for XCTest details, then retry after the app is foregrounded if this was a timeout or activation failure."
161-
)
162-
),
155+
self.jsonResponse(status: 500, response: self.commandFailedResponse(from: error)),
163156
false
164157
),
165158
completion: completion
@@ -274,7 +267,9 @@ extension RunnerTests {
274267
// rather than pairing a stale uptime with a much-later receipt time.
275268
let stamped =
276269
response.ok
277-
? response.stampingCurrentUptimeMs(ProcessInfo.processInfo.systemUptime * 1000)
270+
? response
271+
.stampingCurrentUptimeMs(ProcessInfo.processInfo.systemUptime * 1000)
272+
.stampingCurrentMainThreadBusy(currentMainThreadBusyState().reportsMainThreadBusy)
278273
: response
279274
let encoder = JSONEncoder()
280275
let body = (try? encoder.encode(stamped)).flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
@@ -285,6 +280,32 @@ extension RunnerTests {
285280
Response(ok: false, error: ErrorPayload(code: code, message: message, hint: hint))
286281
}
287282

283+
/// Turns a thrown command error into its wire response. The execution-watchdog timeout keeps its
284+
/// own typed code so the daemon records the runner as main-thread-occupied from the stalling
285+
/// command itself, not only from a later `RUNNER_BUSY` refusal (#2552); every other throw stays the
286+
/// generic `COMMAND_FAILED`.
287+
func commandFailedResponse(from error: Error) -> Response {
288+
let nsError = error as NSError
289+
if nsError.domain == RunnerErrorDomain.general,
290+
nsError.code == RunnerErrorCode.mainThreadExecutionTimedOut
291+
{
292+
return Response(
293+
ok: false,
294+
error: ErrorPayload(
295+
code: RunnerWireErrorCode.mainThreadTimeout,
296+
message: nsError.localizedDescription,
297+
hint:
298+
"The runner abandoned this command's main-thread work past its execution watchdog and it is still draining. Wait and retry, or use a screenshot and interact by coordinates."
299+
)
300+
)
301+
}
302+
return errorResponse(
303+
code: "COMMAND_FAILED",
304+
message: error.localizedDescription,
305+
hint: "Check the runner log for XCTest details, then retry after the app is foregrounded if this was a timeout or activation failure."
306+
)
307+
}
308+
288309
private func httpResponse(status: Int, body: String) -> Data {
289310
let headers = [
290311
"HTTP/1.1 \(status) OK",

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ final class RunnerTests: XCTestCase {
2828
static let objcException = 1
2929
}
3030

31+
/// String codes the daemon keys behavior on. `RUNNER_BUSY` and `RUNNER_WEDGED` come from the busy
32+
/// gate; `MAIN_THREAD_TIMEOUT` is emitted by the transport when a command trips the execution
33+
/// watchdog, so the daemon can tell "the main thread is now occupied" from a generic failure.
34+
enum RunnerWireErrorCode {
35+
static let mainThreadTimeout = "MAIN_THREAD_TIMEOUT"
36+
}
37+
3138
static let springboardBundleId = "com.apple.springboard"
3239
// SpringBoard hosts blocking system modals on iOS/visionOS; tvOS (PineBoard/HeadBoard)
3340
// and macOS have no such host, so there is nothing to probe there.

0 commit comments

Comments
 (0)