Skip to content

feat: golden tap-point parity table for TS and the iOS runner (ADR 0011)#1086

Merged
thymikee merged 1 commit into
mainfrom
feat/adr0011-tap-point-parity
Jul 4, 2026
Merged

feat: golden tap-point parity table for TS and the iOS runner (ADR 0011)#1086
thymikee merged 1 commit into
mainfrom
feat/adr0011-tap-point-parity

Conversation

@thymikee

@thymikee thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member

Implements the golden-fixture-table step of ADR 0011 Layer 2 for the offscreen guarantee: the tap-point rule now has exactly one implementation per side of the wire, and a shared JSON table proves they agree.

The rule (contracted, both sides)

The tap point is the element frame's center; a tap is allowed iff that center lies inside the window frame, edges inclusive. An empty/invalid window frame fails open (allowed) — resolving the best available frame is the impure caller's job (onScreenWindowFrame / app.frame), the policy must not turn a missing frame into a refusal.

Scope note: the table proves the geometric rule only. The TS isNodeVisibleOnScreen layers scrollable-ancestor (effective viewport) logic on top; that part is TS-only and out of scope for the table.

What changed

  • contracts/fixtures/tap-point-policy.json — 12 cases, including this week's real bug shapes: the Bluesky closed drawer fully left of the viewport (x=-321.6, w=321.33 → tapped (-161, 265) reported as success, fix: honor wait budgets, preserve the daemon on polling timeouts, refuse off-screen selector taps #1075) and the edge-grazing container (w=321.67, 0.07pt overlap, center off-screen → still refused), plus edge-inclusive boundary pins, empty-frame fail-open, non-zero-origin window frames.
  • Swift — new pure TapPointPolicy.isAllowed(elementFrame:windowFrame:) (RunnerTapPointPolicy.swift, no XCUIElement). The ELEMENT_OFFSCREEN guard in RunnerTests+CommandExecution.swift now calls it; onScreenWindowFrame stays as the frame getter. A gated XCTest (AGENT_DEVICE_RUNNER_UNIT_TESTS) loads the JSON via #filePath and asserts every case.
  • TSisTapPointInsideViewport(rect, viewport) extracted from isNodeVisibleOnScreen (src/snapshot/mobile-snapshot-semantics.ts), used by both the production rule and the new parity test src/snapshot/__tests__/tap-point-policy-parity.test.ts. No duplicated math.
  • RegistryparityTable wired on direct-ios-selector/offscreen (via now points at the policy) and on maestro-non-hittable-fallback/offscreen.

hasTappableFrame: unified

hasTappableFrame used the same geometric decision (center-in-frame, empty app frame → allowed), so it now delegates to TapPointPolicy too. Its two path-specific choices stay outside the policy and are documented at the call site: empty element frames are refused (can't coordinate-tap a frameless element), and app.frame is deliberately the frame source (Maestro taps resolved bounds against the union frame).

Deliberate micro-semantics (called out for review)

  • Max edges are now inclusive in Swift (closed interval), matching the TS containsPoint rule; previously CGRect.contains was max-edge-exclusive. Only differs when a center sits exactly on maxX/maxY — pinned by a table case so it can't drift silently.
  • TS now uses the exact (unrounded) center in the guard instead of centerOfRect's Math.round, matching Swift's midX/midY exactly. Only differs within 0.5pt of an edge. (The tap dispatch still rounds; the guard evaluates geometry.)
  • Empty window frame fails open in the policy on both sides. TS previously also treated a zero-size resolved viewport as an effective refusal wall; that now fails open, consistent with the contract.

Running the Swift test

CI's "Swift Runner Unit Compile" job compiles the unit-test surface (AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1 pnpm build:xcuitest:macos) but does not execute tests — unchanged. To run locally (iOS simulator; the macOS UITest runner needs a UI session and hangs headless):

AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1 pnpm build:xcuitest:ios
xcodebuild test-without-building \
  -xctestrun <derived>/Build/Products/AgentDeviceRunner_AgentDeviceRunnerUITests_iphonesimulator*.xctestrun \
  -destination 'platform=iOS Simulator,name=<any iPhone>' \
  -only-testing:AgentDeviceRunnerUITests/RunnerTests/testTapPointPolicyMatchesGoldenParityTable

Verification

  • Swift: CI compile command green for macOS and iOS with unit tests enabled; the XCTest executed on an iOS simulator — green in 0.02s, and red-checked (flipping a table case fails it with the case name).
  • TS: format:check, typecheck, lint, fallow audit --base origin/main all green; vitest run src/snapshot src/contracts (18 tests), semantics/daemon suites (980 tests), vitest run test/integration green except the known flaky-under-contention doctor timeout, which passes in isolation.
  • Rebased on origin/main after feat: preflight native-ref fast path with shared interaction guards (ADR 0011) #1082 (native-ref preflight) landed; gates re-run green post-rebase.

ADR 0011 Layer 2: the offscreen tap-point rule now has exactly one home per
side of the wire, proven equivalent by a shared golden fixture table.

- contracts/fixtures/tap-point-policy.json: 12 cases including this week's
  real bug shapes (closed drawer fully left, 0.07pt edge-grazing container)
  plus edge-inclusive and empty-frame fail-open semantics.
- Swift: pure TapPointPolicy.isAllowed(elementFrame:windowFrame:) extracted;
  the ELEMENT_OFFSCREEN guard (onScreenWindowFrame stays the frame getter)
  and hasTappableFrame (Maestro fallback) both delegate the decision to it.
  Gated XCTest asserts every table case (runs in the existing
  AGENT_DEVICE_RUNNER_UNIT_TESTS surface CI already compiles).
- TS: isTapPointInsideViewport extracted from isNodeVisibleOnScreen (no
  duplicated math); vitest parity test asserts the same table.
- Registry: parityTable wired on direct-ios-selector/offscreen and
  maestro-non-hittable-fallback/offscreen.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.5 MB 1.5 MB -9 B
JS gzip 485.9 kB 485.9 kB -18 B
npm tarball 589.5 kB 590.7 kB +1.2 kB
npm unpacked 2.1 MB 2.1 MB +3.4 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.9 ms 29.3 ms +0.4 ms
CLI --help 51.1 ms 51.0 ms -0.1 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/893.js -9 B -18 B

@thymikee

thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Reviewed by running. This is the parity mechanism working exactly as the ADR intended:

  • The 12-case table reads as executable documentation — both fix: honor wait budgets, preserve the daemon on polling timeouts, refuse off-screen selector taps #1075 bug shapes are pinned by name, the closed-interval edge semantics are pinned on both max edges, and the non-zero-origin window pair guards against the classic "viewport starts at 0,0" assumption.
  • Rule placement is right: the policy is pure geometry with the frame-resolution responsibility explicitly left to impure callers, and fail-open on missing frames is stated (and tested) rather than implied on both sides. The "change the rule only via the table" comment on the TS twin is the correct social contract.
  • The two deliberate micro-semantics shifts (Swift max edges become inclusive vs CGRect.contains half-open; TS exact-center vs rounded center) are sub-pixel, flagged in the PR, and each is pinned by a dedicated table case — this is how behavior changes should ride along.
  • hasTappableFrame unification: right call, and keeping refuse-empty-element-frame at the call site preserves the maestro path's stricter input handling.
  • Verified here: TS parity + contracts suites green (18/18); the report's on-simulator Swift run with a red-check validation (flipped case fails with its name) covers the side I can't run in this environment.

LGTM. Merge-order note: registry/pin conflicts with #1083 are expected whichever lands second — combined-removal resolution as usual.

@thymikee thymikee merged commit 415f599 into main Jul 4, 2026
21 checks passed
@thymikee thymikee deleted the feat/adr0011-tap-point-parity branch July 4, 2026 13:11
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-04 13:12 UTC

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