fix(describe): stop ordering a simulator reboot on every externally-booted read - #583
Draft
latekvo wants to merge 3 commits into
Draft
fix(describe): stop ordering a simulator reboot on every externally-booted read#583latekvo wants to merge 3 commits into
latekvo wants to merge 3 commits into
Conversation
…ooted read `degraded` is set from the pre-boot accessibility prefs, which only boot-device can write, so any simulator the developer booted themselves (`xcrun simctl boot`, Xcode, `expo run:ios`) reads as degraded for its whole life. describe turned that into "You MUST call boot-device with force=true now" on every single call. Both halves of that are wrong. The flag says how the sim was booted, not whether the read worked: `IgnoreAXServerEntitlements` bypasses a check that exists on iOS 26.5+, so on earlier runtimes the tree comes back complete. On an externally-booted iOS 18.6 sim describe returns the full tree including the Maps location prompt with all three buttons — while ordering an agent to force-reboot and take the developer's Metro session, dev client and app state with it. Resolve the caveat against the tree actually returned. An empty tree keeps the actionable re-boot guidance, now naming what the reboot costs. A populated tree gets a statement of the limitation instead of an order, emitted once per device per server lifetime rather than dozens of times a session. The per-device gate sits at the `describe` tool boundary only. await-ui-element and the flow runner poll describeIos directly, read a present `hint` on an empty tree as "this emptiness is untrustworthy" (isBlindRead), and fold it into a single terminal note — so they keep every copy, and a `hidden` wait can still never resolve off a blind read. Closes #521
…blind AX read The caveat resolved against the tree being returned, so a blind accessibility read whose native-devtools fallback came back populated was downgraded to the standing note - and the per-device gate then dropped it entirely from the second call on. Those elements are the injected app's own view hierarchy. They carry no SpringBoard chrome and no system dialog at all, so a populated tree from that source is no evidence the accessibility subsystem is working. Reproduced on a fresh iOS 18.6 simulator whose AX read went blind for the whole boot (only `boot-device force=true` recovers it): with the Maps location prompt on screen, describe returned the app's 20 elements with the dialog absent, and from the second call carried no caveat at all - while the standing note it briefly carried says "everything else reads normally", which nothing here did. Resolving against the accessibility tree instead makes both wordings true of the state that produces them: the standing note is now only reachable from a populated AX read, and this path keeps the blind-read hint that names the reboot, repeated per call the way every other blind read is.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #521.
The report holds, and is worse than described
Reproduced against an unmodified
origin/maintool-server driving a simulator booted the way a developer boots one (plainxcrun simctl boot, no argent involvement), iOS 18.6:Identical on every call, as reported. The part that makes it more than a wording nit is what came back alongside it — a complete, working tree:
And the caveat's own claim does not hold either. Triggering a real system permission alert on that same externally-booted sim (
simctl privacy reset location com.apple.Maps+ launch):The system dialog reads fine, buttons and all. So an agent obeying "You MUST ... now" restarts the simulator — killing the Metro session, dev client and staged app state — to fix nothing.
Root cause
AXServiceApi.degradedis!entitlementBypassActive, read fromIgnoreAXServerEntitlements. That pref is only writable pre-boot, soboot-deviceis the only thing that ever sets it and any externally-booted sim reads as degraded for its whole life. The pref bypasses the AX-server entitlement check introduced in iOS 26.5 — on earlier runtimes it is irrelevant to whether the read works. The flag describes how the sim was booted, never whether describe succeeded, and the hint conflated the two.Change
Resolve the caveat against what the accessibility read produced:
The per-device gate is applied at the
describetool boundary only.await-ui-element, the flow runner andawait-screen-idlepolldescribeIosdirectly, treat a presenthinton an empty tree as "this emptiness is untrustworthy" (isBlindRead), and fold it into one terminal timeout note — they keep every copy, so ahiddenwait still cannot resolve off a blind read.The native-devtools fallback can fill a tree the accessibility read left empty, and that split has to key on the accessibility read rather than on the tree that comes back. Its elements are the injected app's own view hierarchy: no SpringBoard chrome, no system dialog, and no evidence the accessibility subsystem works.
Found doing exactly that on a fresh iOS 18.6 sim. Describing within seconds of
simctl bootleaves the accessibility read blind for the rest of that boot (3/3; it survives a tool-server restart, and onlyboot-device force=truerecovers it). With the Maps location prompt on screen in that state, the fallback returned the app's own 20 elements with the dialog absent — and resolving against that tree picked the standing note, which the per-device gate then deleted, so from the second call the agent had a tree silently missing a modal blocking the screen and no caveat at all. The note it briefly carried says "everything else reads normally", which nothing there did. Keying on the accessibility read makes both wordings true of the state that produces them.I skipped the suppression env var / flag the issue floated as a "maybe" — once the caveat is a single non-directive line per device, there is little left to suppress, and it seemed better not to add config surface for it. Happy to add it if you'd rather have the switch.
Verification
E2E against the branch build, same externally-booted sim:
simctl shutdown(blind) ×2boot-device force=true×2IgnoreAXServerEntitlements= 1Unit: 6 new tests in
describe-tool.test.ts. Each was mutation-checked — restoring the MUST wording, removing the dedup, collapsing the blind/standing split, keying the gate globally instead of per device, and re-resolving the caveat against the native-devtools tree each fail exactly one test and nothing else.Full gate:
vitest run4715 passed / 1 skipped,tsc --build,typecheck:tests,eslint --max-warnings 0,prettier --check,test:scripts59 passed.