fix(runner): refuse an iOS coordinate fill whose point names no text input
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:
testPenalizedCoordinateFillAtAPointWithNoTextInputRefusesBeforeItsTapAndKeepsTheFocusedFieldred —TEXT_INPUT_CLEAR_NOT_OBSERVEDinstead 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 atpresses=0; moving the guard after the focus tap turns it red withpresses=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