fix: add iOS private AX snapshot fallback#758
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
f854218 to
2558791
Compare
2558791 to
17345dd
Compare
Code reviewVerdict: minor issues — the design is sound and defensively coded, but there are several robustness gaps in the dynamic private-API plumbing and one cross-layer inconsistency. Findings
Verified cleanSimulator-only gating is genuinely compile-time enforced ( OverallCareful, well-layered (runner private fallback + daemon retry as the real-device net) and safe to land for simulators, but it carries real maintenance weight: ~450 lines of dynamic ObjC against undocumented XCTest internals whose selectors, return shapes, and ABI details can shift with any Xcode release. Draft #761 solves the same sparse-tree symptom with public Generated by Claude Code |
… trees Four fixes that turn the #758 private AX fallback from works-on-one-tree-shape into reliable on Bluesky Home: - Depth ladder: the AX server rejects bulk snapshot requests outright (kAXErrorIllegalArgument) once requested depth crosses a tree-size-dependent limit that moves with live content. Retry at 56/40/24/12 instead of giving up after one attempt at 64. - Real attribute identifiers: the server silently ignored the raw keypath strings the bridge passed, so every node came back with a zero frame (breaking ref taps and the interactive/compact filters, which is why 'snapshot -i -c' stayed sparse). Map keypaths through XCElementSnapshot.axAttributesForElementSnapshotKeyPaths (it returns an NSSet) and drop the mapper's expensive extras (automation type, window display id, base type) that pushed deep requests past the 30s main-thread watchdog. - Viewport from the private root frame when the public windows query degrades to an infinite viewport, so off-screen drawer content stops passing the visibility filter. - Runner source fingerprint now includes .m/.h, so bridge edits stop reusing stale cached runner builds. Also hardens the bridge per review: UInt(exactly:) for untrusted element types, pid_t-sized objc_msgSend for process id matching, and objCType-checked NSValue frame decoding.
On-device validation on Bluesky Home — the fallback does not fire as shipped, fixed in a follow-up branchValidated this PR on a live Bluesky Home feed (iPhone 17 Pro simulator, iOS 27, Xcode 26.2, dev-client build from Root causes (all verified empirically with an instrumented bridge)
Results with the fixes (branch
|
| command | this PR (today, Bluesky Home) | with fixes |
|---|---|---|
snapshot |
IOS_AX_SNAPSHOT_FAILED |
184 nodes, real frames, ~16s (ladder 64→56) |
snapshot -i -c |
1 node | 43 interactive nodes with precise rects (feed tabs at x=6/141/283, drawer excluded) |
ref tap from -i -c |
n/a | lands on element centers, navigation verified |
| Settings (healthy app) | unchanged | unchanged — detector sees content, private path never fires |
Also applied the review's bridge-hardening items (UInt(exactly:), pid_t-sized msgSend, objCType-checked NSValue decode).
Cross-check vs #761
#761's recovery primitive (public typed-query sweep) is demonstrably dead on this screen: the -i -c path is that sweep, and it deadlines with zero elements while coordinate taps work. Public query recovery may still help the milder failure class from #761's repro app (sparse snapshot() but working queries), and this PR already includes a public tier for the regular path — but for Bluesky-class trees the private bridge is the only thing that produces nodes, and with the fixes above it does so reliably. Suggest merging the follow-up branch into this PR before landing.
The all-structural sparse detector misses the common large-RN-tree case where the typed-query sweep resolves one or two stray controls before its 1s deadline: the payload has 'content', so recovery never fires, yet 2 nodes is useless in practice. Treat deadline-truncated payloads with <= 8 nodes as needing recovery, and only replace the original payload when the recovered tree actually carries more nodes. Completed sweeps on legitimately minimal screens stay untouched (not truncated).
Update: raw
|
- Sync the setup metadata script's fingerprint extension list with the runtime (.m/.h were added for the ObjC bridge), fixing the cache metadata parity test. - Reduce find.ts complexity flagged by fallow: hoist the node fetcher into createFindNodeFetcher with a recoverSparseInteractiveSnapshot helper, split match disambiguation and resolution scoring into narrowMultipleMatches/resolvedTouchScore, extract rectsMatch.
Summary
Adds a simulator-only private AX snapshot fallback for iOS when XCTest returns a sparse application/window tree or fails while serializing AX snapshots.
The fallback uses XCTest private accessibility interfaces dynamically, maps the recovered tree back into existing SnapshotNode output, and keeps normal XCTest snapshots authoritative whenever they return real content. It also adds a conservative public XCTest-query recovery tier for regular sparse snapshots before falling back to private AX; compact interactive snapshots still use the private-AX/find recovery path because Bluesky showed public queries collapse there too.
Also fixes SpringBoard permission alerts in compact interactive snapshots: modal detection now runs before the compact app-tree shortcut, and permission sheets return alert text plus actionable button refs without broad SpringBoard subtree walks.
Hardens mutating find actions on iOS: when compact interactive snapshots collapse to the application root, find retries with a full snapshot, then with a query-scoped full snapshot if unscoped AX serialization fails on unrelated feed content.
Closes #701
Validation
snapshot -i -c, did not expose the permission alert, andfind Search clickfailed there.Touched files: 8. Scope covers the iOS XCTest runner snapshot path plus daemon find fallback handling for sparse compact iOS snapshots.