Skip to content

feat(ax-bridge): walker candidate diagnostics on --debug (#660)#691

Merged
shaun0927 merged 1 commit into
developfrom
feat/660-c-walker-debug-events
Apr 29, 2026
Merged

feat(ax-bridge): walker candidate diagnostics on --debug (#660)#691
shaun0927 merged 1 commit into
developfrom
feat/660-c-walker-debug-events

Conversation

@shaun0927
Copy link
Copy Markdown
Owner

Summary

  • Issue feat(ax-bridge): enumerate UNUserNotificationCenter permission sheet (#651 follow-up) #660 PR C-prime: extends the existing --debug machinery so the next live ko-KR capture produces the failure-tree data shaun0927's 2026-04-27 status comment identified as the prerequisite to safely landing a SpringBoard-explicit walker change. Stdout (the JSON dump payload) is unchanged.
  • Adds four new stderr JSON-line events emitted from findMatchingWindow and findDeviceContentRecursively:
    • walker_app_windows_enumerated — every AXWindow under Simulator.app
    • walker_top_candidates — top-5 candidates by score with role/label/depth/score/appSemanticsCount
    • walker_overlay_roles_seen — every overlay-suspect role (AXSheet, AXAlert, AXSystemDialog, AXSystemFloatingWindow, AXDialog, AXSystemAlert) encountered during the walk
    • walker_winner / walker_winner_none — the chosen content root or an explicit no-winner event
  • Volume bounded by WALKER_DEBUG_CANDIDATE_CAP=256 and WALKER_DEBUG_OVERLAY_CAP=32.

Why this is safe

  • All emission is gated on debugEnabled, which only flips when --debug or --verbose is passed.
  • No changes to scoring, candidate selection, early-exit logic, or the JSON dump contract.
  • Existing findDeviceContentRecursively happy path is preserved verbatim — the patch only adds tracking arrays and a single emission call after the recursion returns.

Why this advances #660 without speculative Swift

The remaining plan items in #660 (SpringBoard explicit entry-point + integration test + Exit criteria) all require the failure tree from a live ko-KR cold-launch. PR A (#685) added the JSON-stderr framework. PR B (#686) bounded AX messaging at 1.5s. This PR closes the diagnostic gap so a future focused session can:

  1. Capture --debug stderr against a sheet-visible cold-launch
  2. Read walker_overlay_roles_seen.count to decide whether the overlay subtree is reachable from the matched window
  3. Use walker_app_windows_enumerated to confirm whether SpringBoard hosts the sheet in a sibling window the current walker never visits
  4. Write the actual walker change against real evidence rather than speculation

Test plan

  • swiftc -O src/native/ax-bridge.swift compiles clean
  • 51 unit tests in tests/unit/ax-bridge-{content-root,wrapper,help}.test.ts, tests/unit/native-accessibility-bridge.test.ts, and tests/unit/accessibility-bridge.test.ts pass
  • Live dump --debug on a booted ko-KR iPhone 17 Pro / iOS 26.4 emits all four new events
  • Stdout payload unchanged outside expected app-state churn

🤖 Generated with Claude Code

Issue #660 PR A added top-level milestone events; PR B added the AX
messaging timeout. The remaining empirical work — landing the
SpringBoard-explicit walker change — was deferred in shaun0927's
2026-04-27 status comment because the failure-tree data needed to
write it correctly was not available.

This change extends the existing --debug machinery so the next live
capture on a ko-KR Flutter cold-launch with the UNUserNotificationCenter
sheet visible produces exactly that data. Stdout (the JSON dump payload)
is unchanged — the additions are stderr JSON-line events only.

New events:

- walker_app_windows_enumerated: every AXWindow under Simulator.app with
  title/identifier/role/subrole. Proves whether a SpringBoard-hosted
  overlay renders in a sibling window the walker never visits.
- walker_top_candidates: top-5 ContentCandidates by score with role,
  label, depth, score, and appSemanticsCount. Reveals the scoring
  landscape so a walker change can be evaluated against actual data
  rather than speculation.
- walker_overlay_roles_seen: every AXSheet/AXAlert/AXSystemDialog/
  AXSystemFloatingWindow/AXDialog/AXSystemAlert encountered during the
  walk regardless of whether it won the score race. This is the load-
  bearing signal: if the count is non-zero on a failing capture, the
  overlay subtree IS reachable from the matched window and the walker
  just needs to widen its content selection. If the count is zero, the
  overlay must live in a sibling window or a separate macOS process and
  the change has to enumerate AXWindows / CGWindowListCopyWindowInfo.
- walker_winner / walker_winner_none: the chosen content root with its
  role/label/score, or an explicit "no winner" event matching the
  DEVICE_CONTENT_ROOT_EMPTY exit path.

Bounded with WALKER_DEBUG_CANDIDATE_CAP=256 and
WALKER_DEBUG_OVERLAY_CAP=32 so a degenerate tree cannot blow stderr
volume. All emission gated on debugEnabled.

Verified locally:
- swiftc -O compiles clean
- 51 unit tests across ax-bridge-content-root, ax-bridge-wrapper,
  ax-bridge-help, native-accessibility-bridge, and accessibility-bridge
  pass
- Live dump --debug against a booted ko-KR iPhone 17 Pro emits all four
  new events; stdout payload unchanged outside expected app-state churn

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces enhanced diagnostic logging for the accessibility walker in ax-bridge.swift to help investigate issues with SpringBoard-hosted overlays. Key changes include enumerating all Simulator windows, tracking specific overlay-related roles, and caching metadata for content candidates to emit detailed debug events. These diagnostics are gated by a debug flag and include safety caps to limit resource usage. I have no feedback to provide.

@shaun0927 shaun0927 merged commit da06083 into develop Apr 29, 2026
6 checks passed
shaun0927 added a commit that referenced this pull request Apr 29, 2026
* test(integration): add gated ko-KR push-permission live suite (#660)

Issue #660 plan item: "Add integration test gated by a ko-KR simulator
availability check in CI". Encodes the three Exit criteria from the
issue body as Jest assertions:

  1. app_handle_alert { action: 'dismiss' } returns dismissed: true,
     strategy: 'ax-scan'
  2. visibleButtons contains the localized labels (허용, 허용 안 함)
  3. ax-bridge-native dump --debug exits 0 and emits the walker debug
     events added in #691

The suite is gated behind explicit opt-in (OPENSAFARI_KOKR_PUSH_DIALOG=1
+ OSF_DEVICE_ID + OSF_PERMISSION_BUNDLE) because the cold-launch step
requires the host shell to have Full Disk Access for the simulator's
TCC sandbox so `simctl privacy reset notifications` succeeds. Without
the FDA grant, the suite skips with a structured warning instead of
failing CI on a host environment issue. See
docs/recipes/transient-simctl-errors.md for the FDA setup the suite
expects.

Default behavior (no opt-in) is a single-test "suite skipped"
assertion, so the file integrates cleanly into the existing
tests/integration set without needing a CI flag flip. When a focused
session has the FDA grant, opting in turns the same file into a
binary verification of the issue's Exit criteria.

The walker_* event assertions in the second test depend on the
diagnostic events landed in #691; opt-in runs against a develop branch
that includes #691 will pass cleanly, runs against an earlier develop
will hit a partial-fail on those events but only when opt-in is set.

Verified locally:
- Default-skip run: 1 test passes, prints opt-in skip warning
- ESLint clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* test(660): throw on opt-in env failure; canonicalize visibleButtons

Address gemini-code-assist (3) and chatgpt-codex-connector (1) review
items on PR #692:

- gemini@:204, :232, :246: once OPENSAFARI_KOKR_PUSH_DIALOG=1 has been
  flipped, environmental failures (TCC reset error, empty tool body,
  missing permission sheet) are setup errors that the operator alone
  can resolve and must surface as test failures rather than silent
  console.warn skips. Skip-with-warn behavior is preserved for the
  default no-opt-in path.

- codex@:253: collectVisibleButtonLabels in app_handle_alert annotates
  non-ASCII whitespace as "<original> (norm: <normalized>)" (slice 2
  of #642), and SpringBoard ko-KR permission sheets use NBSP between
  the syllables of 허용 안 함. Strict equality against the raw constant
  would falsely fail when the diagnostic suffix is present or when
  whitespace is U+00A0. Introduces stripDiagnosticAnnotation +
  normalizeWhitespace + canonicalLabel mirroring the regex from
  src/tools/app-handle-alert.ts so the assertion compares against
  canonicalized labels.

Verified locally:
- Default-skip run passes 1 test with the structured warning
- ESLint clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* test(660): cover U+2060 in canonicalLabel whitespace normalization (codex P2)

Address chatgpt-codex-connector P2 on PR #692 (commit d9f1599, line 80):
JavaScript's `\s` regex class does NOT match U+2060 WORD JOINER, even
though `src/tools/app-handle-alert-labels.ts` explicitly treats it as
fancy whitespace alongside U+00A0 / U+202F / U+2007 / U+2028 / U+2029.
A SpringBoard label like "허용⁠안⁠함" (with U+2060 between syllables)
would slip past `\s+` normalization untouched and never equal the
plain ASCII-spaced constant `허용 안 함`, producing a false-regression
report on a passing behavior.

Mirror the FANCY_WHITESPACE codepoint set from the source file 1:1 and
strip those characters before applying the standard `\s+` collapse.
The escape sequence form is used so the regex character class is
unambiguous in source — avoids the editor-level invisibility of literal
control characters.

Verified locally:
- Default-skip run passes 1 test
- ESLint clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant