test(contracts): prove the keyboard band forwarding the #2622 guard prefers

Merged
TH
3d ago
+168-79

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.

  • runnerSnapshotEntry now carries extra runner-result fields, so a scenario can script keyboard: { kind: 'visible', frame } onto a capture and it travels the production read path (readAppleSnapshotResult → acquisition → snapshotResult.keyboard → the seam).
  • New keyboardOcclusion scenario (third, manifest-registered): the producer band over the keyboardLifted tree, 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 nonHittable cell): the deleted hittable-ancestor transcript is back, discriminating on the row center (200,200) vs the label center (140,195). Red when findAct.promotion is retargeted (verified: 1 failed / 7 passed).
  • New find click forwarding scenario: pins preresolvedTarget'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:

  • responseConstruction pinned the payload's key set (['message','x','y']) instead of refsGeneration === undefined, which could not fail for the reason its comment claimed after the ?? { ref: match.ref } fallback was deleted.
  • errorTaxonomy now exercises the ambiguous refusal produced by buildAmbiguousMatchError — 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 classifies focus (it is a generic-route command that shares only executeFocusPoint and never enters the interaction leaf). The sentence now says that.
  • The two coordinate-disclosure unit cases in keyboard-occlusion.test.ts duplicate the interaction leaf's door, which is the strongest production boundary for describeKeyboardOccludedPointWarning (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 local lifted() helper now defers to the shared fixture's keyboardLifted variant, leaving the -560 hoist one owner.

Validation

  • Focused at the head: interaction-contract project 81/81 (find-focus-dispatch file 9/9), keyboard-occlusion/interaction-action/leaf/repo-guards/coverage gates 36/36. pnpm typecheck and check: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