test(contracts): prove the keyboard band forwarding the #2622 guard prefers
Summary
Follow-up to #3727 (merged), closing the evidence gaps an independent re-review of maint/2622-find-focus-keyboard-guard found on the merged seam. No production behavior changes; every change here is a test, plus one registry sentence that was false.
The one that mattered: every test of the seam scripted a tree only, so match.keyboard was always undefined there and the forwarding spread the guard prefers (...(match.keyboard ? { keyboard: match.keyboard } : {}) in find.ts, and its twin in preresolvedTarget for find click/fill) was proven nowhere — deleting it kept all 37 tests green even though it is the exact path the live Android refusal runs on (measured band, zero key nodes). The live transcript proves it works on device; no test proved it stays working.
runnerSnapshotEntrynow carries extra runner-result fields, so a scenario can scriptkeyboard: { kind: 'visible', frame }onto a capture and it travels the production read path (readAppleSnapshotResult→ acquisition →snapshotResult.keyboard→ the seam).- New
keyboardOcclusionscenario (third, manifest-registered): the producer band over thekeyboardLiftedtree, where the tree-derived rule refuses to measure — the refusal can only come from the forwarded fact. Red when the seam's spread is deleted (verified: 1 failed / 6 passed). - New promotion scenario (plain title, deliberately outside the coverage manifest, since the gate forbids entries on the waived
nonHittablecell): the deleted hittable-ancestor transcript is back, discriminating on the row center (200,200) vs the label center (140,195). Red whenfindAct.promotionis retargeted (verified: 1 failed / 7 passed). - New
find clickforwarding scenario: pinspreresolvedTarget's "#2660: the leaf's keyboard guard measures the band this capture measured" comment on the same lifted tree. Red when that spread is deleted (verified: 1 failed / 8 passed).
Cleanups from the same review:
responseConstructionpinned the payload's key set (['message','x','y']) instead ofrefsGeneration === undefined, which could not fail for the reason its comment claimed after the?? { ref: match.ref }fallback was deleted.errorTaxonomynow exercises the ambiguous refusal produced bybuildAmbiguousMatchError— the builder its matrix cell names — alongside the no-match arm, instead of asserting only the no-match message built elsewhere.- The row's description called
focus <x> <y>"the coordinate route"; no row of the matrix classifiesfocus(it is a generic-route command that shares onlyexecuteFocusPointand never enters the interaction leaf). The sentence now says that. - The two coordinate-disclosure unit cases in
keyboard-occlusion.test.tsduplicate the interaction leaf's door, which is the strongest production boundary fordescribeKeyboardOccludedPointWarning(one caller) and already asserts the same point/reason/band string; they're gone, with the interpolated-point assertion folded into the leaf so nothing was lost. The leaf's locallifted()helper now defers to the shared fixture'skeyboardLiftedvariant, leaving the-560hoist one owner.
Validation
- Focused at the head:
interaction-contractproject 81/81 (find-focus-dispatch file 9/9),keyboard-occlusion/interaction-action/leaf/repo-guards/coverage gates 36/36.pnpm typecheckandcheck:layering: OK. - Fail-before verified by mutation for all three new scenarios (each mutation restored afterward).
- Gate:
check:affected --run --head 930e36b0e: all runnable checks passed,exactCommit=true(receipt run-2zbCNo).
🤖 Generated with Claude Code