Skip to content

Manual verification: macOS input injection (key, click, scroll) on a real session #141

Description

@jonocodes

What to verify

The macOS injection rows in docs/PLATFORM-PARITY.md are implemented, typechecked, and unit-tested — but nobody has watched an injected event land in an application. The 2026-08-06 hardware pass (macOS 15.6.1, Apple Silicon) machine-verified focus, enumeration, raise_window, and pointer motion by driving a live daemon over its own WebSocket API, and stopped there:

capability evidence today gap
pointer / drag machine-verified (cursor position read back after pad frames) —
scroll (jogstrip) jog frames sent, no assertion did anything actually scroll?
click (left / right) never fired at a real target does a tap select / does two-finger tap open a context menu?
key injection (printable, combos, special) MacKeySink._check_accessibility's osascript probe exits 0 a passing probe is not a delivered keystroke

The probe sends keystroke "" — an empty string. It proves the System Events grant exists; it proves nothing about _build_keystroke_script output arriving in the focused app, which is the part with a hand-written HID-code map (_MAC_KEY_CODE) and a modifier-clause builder.

This is a manual verification pass — no code changes expected unless a real bug surfaces, in which case file a follow-up ticket.

Repro environment

Any Mac running the daemon from a terminal that holds the Accessibility grant (System Settings → Privacy & Security → Accessibility). Without it, Quartz events are silently dropped and the whole exercise measures nothing — MacKeySink now warns at startup when AXIsProcessTrusted() is false, so check the daemon log first. Note the grant follows the responsible process: the terminal or launchd agent that started the daemon, not python.

just setup && just build-client
just dev-daemon-lan        # then open the client on a phone or a second browser

Acceptance criteria

Record pass / fail plus a one-line observation for each. A single failing scenario is a follow-up ticket, not a blocker on closing this one.

  • Scroll lands. Focus a long web page or document; drag the chrome jogstrip. The content scrolls in the drag direction, and release-with-velocity produces momentum decay rather than an abrupt stop.
  • Left click lands. Move the cursor onto a button/link with the trackpad widget, quick-tap. The control activates (not just a focus ring).
  • Right click lands. Two-finger tap on the trackpad widget over a page. A context menu opens.
  • Drag lock lands. Tap-and-a-half, then drag: a text selection or window move actually follows the finger, and releases on lift (this is the LeftMouseDragged path that motivated Quartz over cliclick).
  • Printable key lands. A key: "a" button types a into a focused text field.
  • Combo lands. A key: "super+t" button opens a new tab in the focused browser (Command, not Control — _MOD_CLAUSE maps super/meta → command).
  • Bracket keys land. super+[ / super+] navigate back / forward in the browser — these go through key code rather than keystroke precisely because the character form is layout-dependent.
  • Special keys land. Esc, Tab, arrows, Enter, and an F-key each do the expected thing in a focused app (the _MAC_KEY_CODE map is documented as partial — record which ones are missing rather than only pass/fail).
  • Manual control mode round-trips. In manual control, typing on the phone's IME inserts the literal characters into the focused desktop app, and the strip keys (Esc/Tab/arrows) work.

Follow-up

Update the evidence table in docs/PLATFORM-PARITY.md ("Verification status") with the results and the date — that table is the thing this ticket exists to make honest. Related: #132 would automate this class of check via an echo-receiver app; this ticket is the cheap human version in the meantime.

Activity

  1. added
    input-injectionSynthetic input via uinput or platform equivalent
    human-verification-requiredCode is complete; a human must verify on real hardware / a live session before closing
    on Aug 7, 2026
  2. jonocodes commented on Aug 7, 2026

    @jonocodes
    OwnerAuthor

    Verification run — 2026-08-06, macOS 15.6.1, Apple Silicon

    Driven through the running daemon over its WebSocket (pad / pad_tap / pad_drag / jog / key / type frames), with results read back out of a local HTML page in Chrome that records mousemove / mousedown / click / contextmenu / keydown / scrollTop. That makes most of this checklist machine-checked rather than eyeballed — the cheap macOS analogue of the #132 echo receiver.

    Two notes on method:

    • Injection had to come from the daemon's process tree, not a fresh shell. TCC attributes Accessibility to the responsible process, so events posted from anywhere else are silently dropped (that's what AXIsProcessTrusted() now warns about at startup).
    • The driver refuses to fire unless /diag reports the target app focused, and raises it through raise_window first — a mis-aimed click can't land in someone else's window.

    Results

    criterion result evidence
    Scroll lands ✅ scrollTop on the element under the cursor moved; page itself didn't scroll, so targeting is right
    Scroll — momentum ❌ daemon decay runs (active_momenta 1 → 0 over ~0.4s) but nothing reaches the app → #143
    Left click lands ✅ click at client (755, 138) — exactly the injected screen point minus the window offset
    Right click lands ✅ contextmenu at client (725, 230), same coordinate match
    Drag lock lands ✅ 1 × mousedown, 5 × mousemove with buttons === 1 stepping exactly +50/+20, 1 × mouseup
    Printable key lands ✅ type: "hello" → textarea value hello, five keydowns
    Combo lands ✅ super+t opened a tab (visible in the CDP page list), super+w closed it; shift+a appended A
    Bracket keys land ✅ super+[ went back (URL lost ?page=second), super+] went forward (regained it)
    Special keys land ✅ Escape, ArrowUp/Down/Left/Right, Enter, F9, Tab all delivered with correct code
    Manual control round-trip ⬜ not covered — the wire-level equivalents (type + key) pass above, but typing on the phone's own IME still needs a human with the phone

    Bug found: scroll accumulator direction asymmetry (fixed)

    MacScrollSink accumulated hi-res units and used floor division to convert to detents. -4 // 120 == -1, so the first downward tick of any gesture fired a whole detent and left the remainder at +116 — which then swallowed the next 120 units. Upward scrolling needed a full 120 units before anything happened. Symmetric input, asymmetric output:

    10 x -4  (40 units down)  ->  1 detent emitted, remainder +80
    10 x +4  (40 units up)    ->  0 detents emitted, remainder +40
    

    Fixed by truncating toward zero instead (int(remainder / DETENT)), with tests pinning symmetry and remainder carry-over. Re-verified live afterwards: 40 units in either direction now moves nothing, 120 units moves exactly one detent, both ways.

    That fix does not make momentum visible — the travel budget is the separate problem in #143.

    Doc updated

    docs/PLATFORM-PARITY.md's verification table now records scroll / click / key as machine-verified with this date, and momentum as a known gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    human-verification-requiredCode is complete; a human must verify on real hardware / a live session before closinginput-injectionSynthetic input via uinput or platform equivalent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions