Skip to content

fix(mobile): make the a11y focus helper's contract observable and cover the status hook - #8580

Merged
iscekic merged 6 commits into
mainfrom
drain/a11y-focus-request-contract
Oct 23, 2026
Merged

iscekic merged 6 commits into
mainfrom
drain/a11y-focus-request-contract

Conversation

@iscekic

@iscekic iscekic commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Evidence

Evidence: demonstrated — at this head the affected suites pass and fail when the behaviours they pin are removed, and an Android emulator run of the real reaction picker shows the renamed helper reaching the native accessibility layer, where the platform may still ignore the request.

What was wrong

  • The focus helper's documentation promised that true meant the platform accepted the focus move. The implementation can only know that a mounted host existed and that the native dispatch did not throw, so a caller could read "dispatched" as "focus moved" and skip its fallback.
  • The status-announcement suite only exercised the pure transition helper, never the hook that owns the platform split and the cross-render dedupe.

Verification at head 665c090

Commands ran on the drain work VM against this checkout.

  • pnpm exec vitest run src/lib/a11y src/components/pr-review/discussion/reactions-row.test.tsx src/components/pr-review/pr-review-pending-comment-row.test.ts src/components/agents/blocking-card-state-a11y.test.ts → 9 files, 60 tests passed; the mounted announcement suite alone is 9 tests.
  • The affected consumer suites (attachment-preview-strip, permission-card, question-card, question-card-actions, session-detail-content, session-detail-queue, session-list-content, session-preview-overlay, session-history-screen) → 9 files, 308 tests passed.
  • pnpm --filter kilo-app run typecheck → clean. Oxlint and oxfmt --list-different on the changed test → clean.
  • Discrimination, both reverted before committing:
    • Deleted the Platform.OS === 'ios' gate from the announcement hook → the mounted suite fails on the Android silence case.
    • Deleted the picker's once-per-presentation guard → the reactions-row suite fails with expected "vi.fn()" to be called 1 times, but got 2 times.
    • Removed the swallow around the native announcement call → the mounted suite fails on the throwing-announcement case.
    • Removed the confirmed-removal gate from the pending-comment helper → its new suite fails on the silent-removal case.

Device run (Android emulator, local stack)

  • Metro served a fresh full bundle after a cache-clearing restart: Android Bundled … apps/mobile/index.js (7954 modules).
  • The development question-card fixture in the real app presents the real QuestionCard and the real reaction picker. Picker opened twice in one recording session:
    • First presentation: the sheet window changed, no TYPE_VIEW_FOCUSED was emitted.
    • Second presentation: TYPE_VIEW_FOCUSED … ClassName: android.widget.TextView; Text: [Reactions] — the focus request reached the native view.
  • Mounting the blocking card in the same fixture emitted TYPE_ANNOUNCEMENT … Text: [Agent needs your input. The composer is paused while you answer.].
  • Zero ReactNativeJS error lines during the run.
  • The fixture keeps every outcome in component state; no API call, no account, no production write. No screenshot: the claim is an accessibility-event observation, which an image cannot show.

Limitations stated plainly

  • The emulator has no screen reader attached, so spoken output and confirmed platform focus are not observable. The observed claim is narrower: a request reaches the native layer, and one of the two presentations produced no focus event, which is exactly the distinction the corrected contract now documents.
  • No iOS device run: this branch changes no iOS-only runtime line or platform branch. The iOS half of the unchanged announcement hook is covered by the mounted suite with a mocked platform, which is a stub harness rather than device execution.
  • ReactionsRow's close-then-restore caller is not mounted by the development fixture, so its dispatch and its retry path are proven by its mounted suite, not by the device.

Changes

  • Rename moveA11yFocus to requestA11yFocus throughout the mobile callers and the test mocks; no compatibility alias remains.
  • Document the boolean as request dispatch: true means a mounted host existed and the native dispatch did not throw, false means the host was missing or the native call threw and was swallowed. There is no native acceptance signal.
  • Keep caller retry and latch behaviour unchanged, and describe it as dispatch: the reaction picker retries from the next real signal until a request is dispatched, then seals the presentation. Extend that suite so a later layout signal cannot ask the platform again.
  • Cover the third caller the finding names: the helper that runs after a confirmed pending-comment removal now has a suite for its stale-id gate and for announcing before it requests composer focus.
  • Add status-announcement.mounted.test.tsx in the existing mounted project: iOS announces a newly appearing status once, stays silent for an identical or whitespace-equivalent re-render, announces a changed status once, and stays silent for null, clearing and reappearing; Android stays silent through the same transitions. Keep the pure transition cases. One case covers a native announcement that throws: the helper swallows it and the hook still announces the next status.
  • Resolve the earlier conflicts with main while retaining its host-instance accessibility API, and drop obsolete native mocks from the announcement suites.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Focus helper: moveA11yFocus returns true only when a node handle existed and setAccessibilityFocus did not throw; the doc says true means the platform accepted the move. Which fix?
    Answer: Rename to requestA11yFocus + fix docs (Recommended)
    Confidence: 0.60 · Precedent: Fix the root cause and the class of the issue: session 00eb2a1d3e4b, "if anything repeats the same mistake over and over again, fix the class". The name change stops future callers from misreading the boolean.
  2. Question: Hook tests: Where should the new useStatusAnnouncement render coverage live? The work-item brief names status-announcement.test.ts, but that file runs in the mobile-pure project, which by config comment does no React mounting; mounted suites are src/**/*.mounted.test.tsx in the mobile-mounted project (precedent: src/lib/a11y/motion.mounted.test.tsx).
    Answer: New status-announcement.mounted.test.tsx (Recommended)
    Confidence: 0.75 · Precedent: Trust the code over documents: session 269375d6e466, "those documents may be stale, only trust code". The project config puts mounted suites in *.mounted.test.tsx files.
  3. Question: Reviewer recovery: head dc68b84 has green CI and an approving human review, but the reviewer job failed twice on this pull request with an infrastructure error (its own environment preparation timed out); other pull requests' reviews succeed, and a session token cannot re-request the run.
    Answer: Do not push a padding commit, and do not just wait. Re-run the failed reviewer job directly with a check rerun or a re-request comment, and keep working while it runs. If it cannot be re-triggered, the pull request already meets human-ready: CI is green, it is approved, there are no conflicts, and no review comments are open. Mark it ready for review and add a note that the reviewer failed with an environment-preparation timeout. Treat the stalled readiness judge as a workflow bug and fix it rather than end on it. Do not merge.
    Confidence: 0.50 · Precedent: Never accept blocked as an end state; treat it as a workflow bug and record any real external gap in the pull request. Session 55c9c80332a7: "blocked is not an acceptable end state".
  4. Question: The session end is refused until the reviewer finishes a review of the failing head, the reviewer cannot be re-requested by a session token, and a comment did not re-trigger it. How to proceed?
    Answer: Land one genuine coverage commit (recommended)
    Confidence: 0.72 · Precedent: Never accept "blocked" as an end state (session 2f71af3a787e: "a work item must converge"); do the least work possible with no drop in quality (session 7f3470e0ba6f). The test is real coverage for a caller the brief names, not padding.

requestA11yFocus (formerly moveA11yFocus) reports only what it can see: a
mounted node handle and a dispatched focus request. Callers latch and retry on
that signal, so the doc text saying the platform accepted the move described an
outcome React Native does not expose.

Add render coverage for useStatusAnnouncement: the platform gate and the
cross-render dedupe now have iOS and Android cases beside the pure
nextAnnouncement table.
@iscekic iscekic added drain Work item from the findings drain janitor:maintainability Maintainability finding from the janitor labels Oct 22, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental delta is test-only: the reaction picker suite now pins the request latch after a dispatched focus request, a new pure suite covers focusAfterPendingCommentRemoval, and the mounted announcement suite covers a throwing native announcement. Each case exercises the current production contract, and no runtime code changed in this delta.

Assumptions: [INFERENCE] I could not execute vitest, pnpm typecheck, or pnpm lint in this read-only sandbox, so verification is static: the new suite matches the mobile-pure include src/components/**/*.test.ts and not the mounted glob, the latch test's mockReturnValueOnce(false) is consumed inside openPicker before the retry assertion and mockClear preserves the default true, and announceForA11y wraps the native call in try/catch so the new throwing-announcement case depends on real production behavior rather than test scaffolding.

Files Reviewed (3 files)
  • apps/mobile/src/components/pr-review/discussion/reactions-row.test.tsx
  • apps/mobile/src/components/pr-review/pr-review-pending-comment-row.test.ts
  • apps/mobile/src/lib/a11y/status-announcement.mounted.test.tsx
Previous Review Summaries (2 snapshots, latest commit 82b567c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 82b567c)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental delta is test-only cleanup: five announcement suites drop findNodeHandle/setAccessibilityFocus mocks that the Expo SDK 58 helper no longer uses, the Android render case now exercises real transitions instead of only a first mount, and two stale comments/test titles are corrected. The rename stays complete against the merged base, and no runtime behavior changed.

Assumptions: [INFERENCE] I could not execute vitest, pnpm typecheck, or pnpm lint in this read-only sandbox. Verification is static: the final requestA11yFocus body at HEAD matches main's HostInstance/sendAccessibilityEvent implementation, zero moveA11yFocus references remain in apps/mobile, every partial @/lib/a11y/announce mock now exports the new name, and the new suite matches only the mobile-mounted glob (src/**/*.mounted.test.tsx) because the pure project's src/lib/a11y/**/*.test.ts pattern does not match .tsx.

Files Reviewed (27 files)
  • apps/mobile/src/lib/a11y/announce.ts - rename with dispatch-only contract
  • apps/mobile/src/lib/a11y/announce.test.ts - renamed describe/assertions
  • apps/mobile/src/lib/a11y/status-announcement.mounted.test.tsx - iOS/Android hook coverage; Android case now walks real transitions
  • apps/mobile/src/lib/a11y/status-announcement.test.ts - dropped unused native mocks
  • apps/mobile/src/lib/a11y/announcing-toast.test.ts - dropped unused native mocks
  • apps/mobile/src/app/_layout.tsx - import/comment updated
  • apps/mobile/src/components/agents/attachment-preview-strip.tsx - import/call updated
  • apps/mobile/src/components/agents/attachment-preview-strip.mounted.test.tsx - mock/assertions updated
  • apps/mobile/src/components/agents/blocking-card-state.ts - dependency doc updated
  • apps/mobile/src/components/agents/permission-card.tsx - import/call updated
  • apps/mobile/src/components/agents/permission-card.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/question-card.tsx - import/call updated
  • apps/mobile/src/components/agents/question-card.mounted.test.tsx - mock/assertions updated
  • apps/mobile/src/components/agents/question-card-actions.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-detail-content.tsx - import/call/comment updated
  • apps/mobile/src/components/agents/session-detail-content.test.ts - mock updated
  • apps/mobile/src/components/agents/session-detail-queue.test.ts - mock updated
  • apps/mobile/src/components/agents/session-history-screen.pull-failure.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-list-content.tsx - import/param type/comment/call updated
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-preview-overlay.tsx - import/calls updated
  • apps/mobile/src/components/agents/session-preview-overlay.test-helpers.tsx - mock rename and comment correction
  • apps/mobile/src/components/agents/session-preview-overlay.mounted.test.tsx - assertions updated
  • apps/mobile/src/components/pr-review/discussion/reaction-picker-sheet.tsx - latch renamed, comments updated
  • apps/mobile/src/components/pr-review/discussion/reactions-row.tsx - import/comment updated
  • apps/mobile/src/components/pr-review/discussion/reactions-row.test.tsx - mock and test description updated
  • apps/mobile/src/components/pr-review/pr-review-pending-comment-row.tsx - import/call updated

Previous review (commit 792d360)

Status: No Issues Found | Recommendation: Merge

Executive Summary

I reviewed the 25 changed files in the mobile app: a repository-wide rename of moveA11yFocus to requestA11yFocus with corrected contract documentation, plus a new mounted render suite for useStatusAnnouncement. No leftover references to the old name exist, every import and test mock matches the new export, the hook's platform gate and cross-render dedupe are genuinely exercised, and the change alters no runtime behavior.

Assumptions: [INFERENCE] I could not execute pnpm test/pnpm typecheck in this read-only sandbox, so correctness is based on static verification: a repo-wide search found zero remaining moveA11yFocus references, all 18 call sites/mocks import the new name, and the new *.mounted.test.tsx file matches the mobile-mounted project glob while escaping the mobile-pure globs. Several otherwise-unrelated suites partially mock @/lib/a11y/announce without the focus export, but I traced them and none renders a component that calls requestA11yFocus.

Files Reviewed (25 files)
  • apps/mobile/src/lib/a11y/announce.ts - renamed helper; contract doc now states true is dispatch-only
  • apps/mobile/src/lib/a11y/announce.test.ts - test names/assertions match new contract
  • apps/mobile/src/lib/a11y/status-announcement.mounted.test.tsx - new iOS/Android render coverage
  • apps/mobile/src/app/_layout.tsx - import/comment updated
  • apps/mobile/src/components/agents/attachment-preview-strip.tsx - import updated
  • apps/mobile/src/components/agents/attachment-preview-strip.mounted.test.tsx - mock/assertions updated
  • apps/mobile/src/components/agents/blocking-card-state.ts - dependency doc updated
  • apps/mobile/src/components/agents/permission-card.tsx - import updated
  • apps/mobile/src/components/agents/permission-card.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/question-card.tsx - import updated
  • apps/mobile/src/components/agents/question-card.mounted.test.tsx - mock/assertions updated
  • apps/mobile/src/components/agents/question-card-actions.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-detail-content.tsx - import/comment updated
  • apps/mobile/src/components/agents/session-detail-content.test.ts - mock updated
  • apps/mobile/src/components/agents/session-detail-queue.test.ts - mock updated
  • apps/mobile/src/components/agents/session-history-screen.pull-failure.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-list-content.tsx - import/comment/param type updated
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx - mock updated
  • apps/mobile/src/components/agents/session-preview-overlay.tsx - import updated
  • apps/mobile/src/components/agents/session-preview-overlay.test-helpers.tsx - mock renamed
  • apps/mobile/src/components/agents/session-preview-overlay.mounted.test.tsx - assertions updated
  • apps/mobile/src/components/pr-review/discussion/reaction-picker-sheet.tsx - import/ref/docs updated
  • apps/mobile/src/components/pr-review/discussion/reactions-row.tsx - import/comment updated
  • apps/mobile/src/components/pr-review/discussion/reactions-row.test.tsx - mock updated
  • apps/mobile/src/components/pr-review/pr-review-pending-comment-row.tsx - import updated

Reviewed by deepseek-flash-latest · Input: 59.7K · Output: 9.3K · Cached: 505.5K

Review guidance: REVIEW.md from base branch main

@iscekic iscekic added merge:blocked:insufficient-e2e Verification does not cover the change merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. labels Oct 22, 2026
@iscekic iscekic removed merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. merge:blocked:insufficient-e2e Verification does not cover the change labels Oct 22, 2026
@iscekic

iscekic commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Merged main and resolved four conflicts: announce.ts keeps HostInstance/sendAccessibilityEvent with the request-only contract; announce.test.ts tests that API; blocking-card-state.ts keeps native host refs; reaction-picker-sheet.tsx keeps TextInstance and the dispatch latch. Removed obsolete announcement mocks in a follow-up commit. Verified 365 targeted tests, mobile typecheck/lint/format/unused checks, and Android native events: each of two picker openings emitted TYPE_VIEW_FOCUSED for the Reactions title. Refreshing the question card emitted its native announcement. This proves dispatch, not TalkBack speech or confirmed focus. CI and the head review are green; the PR body now contains the current evidence and limitations.

@iscekic iscekic added merge:blocked:insufficient-e2e Verification does not cover the change merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. labels Oct 22, 2026
The retry case exercised the second attempt but never asserted that a
dispatched request seals the presentation. Extend it so a later layout
signal cannot ask the platform again.
@iscekic iscekic removed merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. merge:blocked:insufficient-e2e Verification does not cover the change labels Oct 23, 2026
…hrows

A native accessibility outage is swallowed by the announce helper, so the
hook's cross-render dedupe must keep working after one failed call. Pin it
in the mounted suite.
@iscekic

iscekic commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

bot: The automated review of this head did not complete. Its check run ended with Review failed: Environment preparation timed out, so this commit carries no review result.

State of this head: every other check passed, the pull request is approved by a human reviewer, it has no merge conflict, and no review thread is open. The same failure happened on the previous head, while reviews of other pull requests were completing, so it looks environmental rather than related to this diff.

A work session cannot re-request that check run: the API refuses a re-request from anything but the app that created it. Re-running the review from the app side is what this pull request needs next.

…s request

The helper that runs after a confirmed removal had no direct coverage: the
gate for a stale id and the announce-before-focus order were untested.
@iscekic iscekic added the merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. label Oct 23, 2026
@iscekic
iscekic added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 47f35a5 Oct 23, 2026
104 checks passed
@iscekic
iscekic deleted the drain/a11y-focus-request-contract branch October 23, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain janitor:maintainability Maintainability finding from the janitor merge:bot Bot-owned review; merge when ready once readiness passes and a review approves.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants