Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The regression test does not exercise a genuinely blocked main-thread operation, so deferred invalidation is not fully verified.
Pull request overview
Improves iOS runner resilience during hostile accessibility snapshots by suppressing known AX failures and tracking abandoned main-thread work.
Changes:
- Adds deferred invalidation and abandoned-work tracking.
- Expands AX failure suppression and snapshot recovery.
- Adds tests, documentation, changelog, and CI coverage.
File summaries
| File | Description |
|---|---|
docs/adr/0004-ios-snapshot-backend-strategy.md |
Documents snapshot recovery strategy. |
CHANGELOG.md |
Records the runner-liveness fix. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+RecordedIssueSuppressionTests.swift |
Tests AX issue classification. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift |
Skips XCTest tiers during abandoned work. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift |
Applies centralized work tracking. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+MainThreadWork.swift |
Adds main-thread work infrastructure. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift |
Defers invalidation and updates command handling. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift |
Avoids blocked viewport reads. |
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift |
Expands AX suppression behavior. |
.github/workflows/ios.yml |
Selects the new iOS regression test. |
Review details
Suppressed comments (1)
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift:853
- This regression test does not actually hold a main-thread operation behind the abandoned count; it only sets the counter manually. With the previous implementation,
runMainThreadWorkwould dispatch invalidation to a free main queue and complete before the 0.5 s assertion too, so this test would also pass after reverting the deferred branch. Please block a realrunMainThreadWorkoperation, assert the target remains cached while it is blocked, then release it and verify the queued invalidation runs.
func testSnapshotFailureInvalidationQueuesBehindAbandonedMainThreadWorkWithoutWaiting() {
currentBundleId = "com.example.stale-target"
abandonedMainThreadWorkCount = 1
defer {
abandonedMainThreadWorkCount = 0
- Files reviewed: 10/10 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Reviewed at 9844aa8. The classifier fix for
A design question: the deferred invalidation and the 330-line move into One more question on scope: after an abandoned viewport or tree slice, a slow but healthy screen now also gets privateAX output with no sweep. Did the Bluesky run, or any other screen, show that output is still complete enough? Thanks for the live Bluesky numbers (1 runner boot for 58 commands on 9844aa8). A short runner log excerpt from that run, showing the suppressed CI is green, and the iOS lane now runs the new viewport test. There are no merge conflicts. Next step: make the two tests fail when the new behavior is reverted. |
9844aa8 to
7e2b10c
Compare
|
Thanks for the close read. Both test points were right, and the stack is restructured as you suggested. Restructured into a stack. The pure move now lives in its own PR (#2630,
Tests now fail on revert.
Completeness after an abandoned read. In the session log that prompted this (1315 commands), the query sweep recovered a capture once out of 730 recoveries and timed out 344 times, always after the tree or viewport read ahead of it had already ground. The sweep is a main-thread dispatch, so once main is occupied it cannot start before its own slice expires; skipping it changes what is logged, not what is returned. The live run's first hostile capture returned 207 private-AX nodes at depth 56, the same shape the original log shows for the recoveries that survived. Runner log excerpt from the live Bluesky run on the fix (first hostile capture, bridge disabled so the runner served it): The |
There was a problem hiding this comment.
🔵 Needs a closer look
Broad XCTest lifecycle and main-thread coordination changes warrant final human review; one comment nit also remains.
Review details
Suppressed comments (1)
apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+RecordedIssueSuppressionTests.swift:51
- This comment is now inaccurate: the classifier deliberately covers both the matching-snapshot and query-resolution fetches, so describing the negative case as only being outside the matching-snapshot context no longer matches the behavior. Please update it to refer to either supported fetch context.
// A kAXError outside the matching-snapshot fetch context is not this class.
- Files reviewed: 11/11 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Reviewed The earlier review gaps are addressed:
Current CI: 11 successful checks, 2 skipped, none failing or pending. The PR's live Bluesky evidence reports one runner boot across 58 commands, zero recorded XCTest failures, and a recovered 207-node snapshot. The new query-resolution wording is unit-covered; the live run avoids that sweep through the occupancy fix. This was a source/evidence review; I did not rerun device tests locally. GitHub still reports the acknowledged merge conflicts. |
Pure move out of the 2.9k-line RunnerTests+CommandExecution.swift: runMainThreadWork, its work state, the busy/wedged state model, mainThreadExecutionTimeoutError, and their two unit tests. The suppression classifier test moves from RunnerTests+SnapshotCapturePlan.swift to UnitTests/RunnerTests+RecordedIssueSuppressionTests.swift. No behavior change. The one edit is mainThreadExecutionTimeoutError losing `private`, because its callers stay behind in the command execution file.
XCTest ends testCommand as soon as a main-thread block that recorded an
unmuted failure returns. The query-sweep tier records 19 of them per
capture on the Bluesky feed ("Failed to resolve query: ... kAXError...")
and the record(_:) suppression only knew the "Failed to get matching
snapshot" wording, so every hostile snapshot cost a runner boot and the
per-bundle penalty and depth memory died with the process.
- mute AX-server rejections in both XCTest fetch wordings; timeouts and
"Application X is not running" keep recording (these issues carry no
associatedError, so the wording is the only handle)
- one abandoned-main-thread-work counter: runMainThreadWork takes an
operation label and logs abandon/drain itself, and every bounded
dispatch counts, so an abandoned viewport read or tree XPC skips the
query sweep instead of queueing it behind the grind
- the tree slice timeout no longer penalizes from its abandonment hook,
which bypassed the fresh-process warmup exemption every other penalty
honors; the timed-out attempt penalizes instead
- CHANGELOG entry and ADR 0004 regression note
A capture that failed while abandoned main-thread work was still grinding waited a second for its cached-target drop and then added a phantom abandoned unit. Queue the drop behind that work without waiting instead: the failed capture answers now and the next command still finds the target gone.
7e2b10c to
0bdf5a0
Compare
|
Rebased onto the updated #2630 extraction; new head Conflict resolution pushed; marked |
0bdf5a0 to
ab34abc
Compare
| DispatchQueue.main.async { | ||
| self.invalidateCachedTarget(reason: "ax_snapshot_failure") | ||
| } |
|
Thanks for landing this. Two notes for the record:
|
Stacked on #2630 (pure move of the main-thread work machinery). Review this PR from its second commit.
Summary
On a live React Native feed (Bluesky Home) the iOS runner died after every snapshot: each next command paid an
xcodebuildboot.Cause: the query-sweep tier records 19 XCTest issues per capture worded
Failed to resolve query: … kAXErrorIllegalArgument ….record(_:)only muted the siblingFailed to get matching snapshotwording, and XCTest endstestCommandonce the main-thread block that recorded an unmuted failure returns.Changes, on top of the move (7 files modified, 1 added):
Application X is not runningkeep recording.runMainThreadWorktakes an operation label and logs abandon/drain. An abandoned viewport read or tree XPC now skips the query sweep: first hostile capture ~2.5 s instead of ~4.5 s. The tree slice timeout penalizes through its attempt outcome, so the warmup exemption applies.Validation
Tested commit
ab34abc27(rebased onto465af7549). Earlier evidence at7e2b10caf.pnpm check:affected --run: every runnable check passes exceptmutation-model(ownership.test.ts, "a kernel is owned by tests that reach it indirectly"), pre-existing onmain: this branch changes no TypeScript.Conflict-resolution validation
Rebased onto the updated #2630 extraction; new head
0bdf5a04a5. All three behavior/gate commits are unchanged bygit range-diff. iOS and macOS build-for-testing passed with unit tests enabled.pnpm check:affected --runstopped at the pre-existing mutation-model assertion “a kernel is owned by tests that reach it indirectly” (scripts/mutation/ownership.test.ts:40); the same failure was reproduced on unchanged main465af7549b. Earlier device evidence remains attributed to the earlier head; no new device run was performed. New-head GitHub CI is pending.