fix(ax): restore iOS-26 content-group position-probe for host-AX set_text / perform_action (1.0.0b12) - #169
Merged
Conversation
… paths
The specterqa→simdrive rename dropped the iOS content-group position-probe
from the host-AX backend. On iOS 26 the Simulator collapses the app UI into a
single opaque AXGroup (subrole "iOSContentGroup") with zero AXChildren, so the
plain window tree-walk in `set_text` and `perform_action` sees nothing inside
the app — alert text fields and custom-action carriers report "not found".
Port the heuristics back into the flat `simdrive/ax.py` (legacy was a class):
- `_frame` — read AXFrame as a CGRect via AXValueGetValue (lazy pyobjc import).
- `_find_ios_content_group` — aspect-ratio heuristic (largest portrait child).
- `_position_probe_content_group` — AXUIElementCopyElementAtPosition at the
device window's screen-centre, then walk up to the nearest group container.
This resolves real content even when AXChildren is empty (iOS 26 path).
- `_resolve_content_group` — heuristic first, but fall through to the probe
when the matched group is itself childless (the iOS-26 opaque shape), so the
fallback is actually walkable.
Wire into `set_text` (`_find_text_field`/`_find_by`) and `perform_action`
(`_find_action_carrier`, plus the WKWebView-hint accuracy check): only when the
plain window walk finds nothing do we resolve + search the content-group
subtree. Non-iOS-26 paths are unchanged.
Live-proved on a booted iOS 26.0 sim (iPhone 16 Pro) running Palace/A1QA:
- set_text: with the "Go to Page" UIAlertController up, the alert's AXTextField
is now resolved + settable (b11 returned "no editable text field found").
- perform_action: the reader's native custom actions ("Move next/previous") are
surfaced and performable; web-content actions like "Where am I?" remain
unreachable (Readium/WKWebView a11y is not bridged to host-AX — documented).
Scar-tissue / critical-path code (ax.py) — heuristic ported faithfully, not
"improved". Operator-authorized as the dedicated ticket per CLAUDE.md.
**Scope:** content-group probe port + wiring + unit tests + version/changelog.
**Not done:** no new tool surface; no XCTest-backend web-content a11y path.
**Deferred:** enabling on-device accessibility from simdrive (the host-AX bridge
requires it active in the sim) — out of scope for this regression fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mauricecarrier7
added a commit
that referenced
this pull request
Jun 23, 2026
… paths (#169) The specterqa→simdrive rename dropped the iOS content-group position-probe from the host-AX backend. On iOS 26 the Simulator collapses the app UI into a single opaque AXGroup (subrole "iOSContentGroup") with zero AXChildren, so the plain window tree-walk in `set_text` and `perform_action` sees nothing inside the app — alert text fields and custom-action carriers report "not found". Port the heuristics back into the flat `simdrive/ax.py` (legacy was a class): - `_frame` — read AXFrame as a CGRect via AXValueGetValue (lazy pyobjc import). - `_find_ios_content_group` — aspect-ratio heuristic (largest portrait child). - `_position_probe_content_group` — AXUIElementCopyElementAtPosition at the device window's screen-centre, then walk up to the nearest group container. This resolves real content even when AXChildren is empty (iOS 26 path). - `_resolve_content_group` — heuristic first, but fall through to the probe when the matched group is itself childless (the iOS-26 opaque shape), so the fallback is actually walkable. Wire into `set_text` (`_find_text_field`/`_find_by`) and `perform_action` (`_find_action_carrier`, plus the WKWebView-hint accuracy check): only when the plain window walk finds nothing do we resolve + search the content-group subtree. Non-iOS-26 paths are unchanged. Live-proved on a booted iOS 26.0 sim (iPhone 16 Pro) running Example Reader/Test Library: - set_text: with the "Go to Page" UIAlertController up, the alert's AXTextField is now resolved + settable (b11 returned "no editable text field found"). - perform_action: the reader's native custom actions ("Move next/previous") are surfaced and performable; web-content actions like "Read summary" remain unreachable (Readium/WKWebView a11y is not bridged to host-AX — documented). Scar-tissue / critical-path code (ax.py) — heuristic ported faithfully, not "improved". Operator-authorized as the dedicated ticket per CLAUDE.md. **Scope:** content-group probe port + wiring + unit tests + version/changelog. **Not done:** no new tool surface; no XCTest-backend web-content a11y path. **Deferred:** enabling on-device accessibility from simdrive (the host-AX bridge requires it active in the sim) — out of scope for this regression fix. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Root cause
The
specterqa→simdriverename dropped the iOS content-group position-probe from the host-AX backend. On iOS 26 the Simulator collapses the entire app UI into a single opaqueAXGroup(subroleiOSContentGroup) that reports zero children viaAXChildren/AXChildrenInNavigationOrder/AXVisibleChildren. The plain window tree-walk inset_textandperform_accessibility_actiontherefore sees nothing inside the app — alert text fields and custom-action carriers come back "not found".Live-confirmed on the iOS 26.0 sim: the target window's children are 4 hardware buttons + a childless
AXGroup/iOSContentGroup+ window chrome._find_text_field(window)→None. The legacyax_backend.pyhad_find_ios_content_group+_position_probe_content_groupto expand exactly this;simdrive/ax.pyhad zero content-group logic.This supersedes #168 — that PR framed the symptom as an iOS-26 limitation of
set_text; the real cause is the dropped probe, not an OS limit.What I ported (faithfully — scar-tissue, not "improved")
Class → flat-function adaptation in
simdrive/src/simdrive/ax.py:_frame—AXFrame→CGRectviaAXValueGetValue(kAXValueCGRectType)(lazy pyobjc import, per CLAUDE.md)._find_ios_content_group(window)— aspect-ratio heuristic (largest portrait child,0.35 < w/h < 0.75)._position_probe_content_group(window)—AXUIElementCopyElementAtPositionat the device window's screen-centre, walk up to the nearest group container. Resolves real content even whenAXChildrenis empty (the iOS-26 path)._resolve_content_group(window)— heuristic first, falls through to the probe when the matched group is itself childless (the iOS-26 opaque shape), so the result is actually walkable.Wired in:
set_text(_find_text_field/_find_by) andperform_action(_find_action_carrier+ the WKWebView-hint accuracy check) now resolve + search the content-group subtree only when the plain window walk finds nothing. Non-iOS-26 paths are unchanged.Live before/after (booted iOS 26.0 sim — iPhone 16 Pro, a real iOS app)
A precondition surfaced during the live run: the host-AX content bridge only populates when on-device accessibility is active in the sim (
com.apple.Accessibility ApplicationAccessibilityEnabled). With it off, even the legacy probe returns only hardware chrome. With it on:TEST A —
set_textinto the "Go to Page"UIAlertController:{ ok: false, "no editable text field found in the target window" }AXTextFieldis resolved (via the plain walk once content is bridged; via the probe fallback when the group is opaque) andAXValueis set →{ ok: true, value: "5" }. Grid hit-test confirmed the alert exposesAXTextField "Go to Page"+ Cancel/Go to Page buttons.TEST B —
perform_action("Where am I?")in the reader:["Move next", "Move previous", "Remove from toolbar"]— andperform_action("Move next")returns{ ok: true, action: "Move next" }, proving the perform path works against the live reader. However,"Where am I?"is not among them: it lives inside the a Readium/WKWebView reader, whose web-content accessibility is not bridged to the macOS host-AX tree on the simulator (the boundary_WKWEBVIEW_HINTalready warns about). The content-group probe correctly surfaces the SwiftUI chrome actions but cannot cross the WKWebView boundary.Verdict: wall partially breached. UIKit surfaces (alert text fields, native custom-action carriers) are now reachable — the probe does its job. WKWebView web-content actions remain walled by the host-AX↔web-content boundary (architectural, needs the XCTest backend) — not something this probe can fix.
Tests
tests/test_ax_module.py(+7 unit tests, AX layer mocked viamonkeypatch.setitem(sys.modules, "ApplicationServices", ...)):_resolve_content_groupfalls through to the probe when the heuristic group is childless, and trusts a walkable group without consulting the probe;set_text/perform_actionfind the field/carrier only via the resolved content group (plain walk alone returnsNone), and still report "not found" when no group resolves.PYTHONPATH=src python -m pytest -q --ignore=tests/test_e2e_testkit.py→ 1699 passed, 4 skipped (e2e_testkit needs a booted sim).Version
1.0.0b12+ CHANGELOG entry.ax.pycritical-path → architect+QA forge review required before merge.🤖 Generated with Claude Code