fix(runner): refuse an iOS coordinate fill whose point names no text input

Merged
TH
15h ago
+285-24

Summary

On the iOS degraded-XCTest-channel fill route — which ref, selector, and replay fills all reach as a point — the synthesized first-responder replacement has no element: with no text input under the point it still dispatched its focus tap and ran clear+type through the app-level first responder, so a fill aimed at empty space rewrote whichever field an earlier focus left active (#1721). The runner now refuses before the focus tap: NO_TEXT_INPUT_AT_POINT when the point identified no input, TEXT_INPUT_PROBE_UNAVAILABLE when the pre-tap lookup could not answer (a blind point is not an empty one). Each publishes its own error.details.reason with dispatched: no; nothing runs and no field changes. Eleven files, one command family, three commits (fix/test/fix); no declared guarantee in interaction-guarantees.ts changes.

agent-device fill 300 780 "text"   # point with no input → NO_TEXT_INPUT_AT_POINT, dispatched: no
agent-device fill 201 496 "text"   # point inside an input → unchanged behavior

Validation

Base 5007540a9, head 28c6cf1a6 (rebased; the red was captured pre-rebase at the same content). Same XCTest on an iPhone 17 Pro simulator:

  • base: testPenalizedCoordinateFillAtAPointWithNoTextInputRefusesBeforeItsTapAndKeepsTheFocusedField red — TEXT_INPUT_CLEAR_NOT_OBSERVED instead of the refusal, and the focused field's seeded "stale" was destroyed.
  • head: that test plus the failed-lookup (TEXT_INPUT_PROBE_UNAVAILABLE) and the 55-test fill/coordinate/synthesized-entry family green. The regression asserts the fixture's tap witness stays at presses=0; moving the guard after the focus tap turns it red with presses=1, so the pre-tap claim is pinned.

Live on a self-created simulator (production daemon + runner; the channel penalty forced by a local-only scratch build, reverted before commit; captured at 81601c1c8, identical route code at head): the empty-space fill refused with NO_TEXT_INPUT_AT_POINT / no_text_input_at_point / dispatched: no, the field held "stale", the witness held presses=0, and fill 201 496 on the real input still landed.

pnpm check:affected --run --head 28c6cf1a6: passed (exactCommit=true; format, lint, typecheck, layering, fallow, build, vitest-related, command-docs, docs-links, docs-prose, xctest-selection, packaged-runner-swift). CI obligations: ios.yml targeted runner lane selects the new tests at head. Resolved risks: the non-penalized route keeps its pre-existing untyped refusal (out of scope); the .xctestFallback unbound window predates this PR (see review reply); the live penalty was forced because a healthy simulator cannot degrade the XCTest channel on demand.

Fixes #1721

🤖 Generated with Claude Code