The 194-line arm moves to `App::apply_keyboard`, verified byte-identical
against `HEAD~1` modulo exactly two named conversions: the press guard
becomes the router's decision, and `event_loop.exit()` becomes a
returned `EventOutcome::Exit`.
THE OUTCOME RETURN IS THE LOAD-BEARING PART. The keyboard arm was the
second caller of `event_loop.exit()` --- an idle Escape is a local quit
--- so a body that owned the exit would have needed an
`&ActiveEventLoop`, and `ActiveEventLoop` is exactly what cannot exist
in a test. Returning the decision keeps `event_loop.exit()` in one
place, `window_event`, and leaves every body reachable in principle.
Both call sites are now inside `window_event` and nowhere else, which
is checkable by grep. Stage 1a's A4 deletes the Escape branch, at which
point `EventOutcome` has one variant and should go; the branch carries
a comment saying so.
A SECOND ACCEPTED STRUCTURAL EXCEPTION, ALONGSIDE P3, and it is winit's
rather than this seam's: `KeyEvent` carries a `pub(crate)
platform_specific` field, so NO `WindowEvent::KeyboardInput` CAN BE
CONSTRUCTED OUTSIDE WINIT and no headless test can feed one. Checked in
winit-0.30.13/src/event.rs, not assumed.
The response is to shrink what the exception covers rather than to
accept it whole. The family's only real decision --- press acted on,
release claimed and discarded --- is factored into
`route_key_action(ElementState) -> KeyAction`, which takes a
constructible argument and is tested directly. What stays unwitnessed
is one pattern arm containing a match and a call, with no logic in it.
The exception does NOT extend to the pointer families: winit provides
`DeviceId::dummy()` for exactly this purpose ("useful for unit
testing") and `CursorMoved`/`MouseInput`/`MouseWheel` are constructible.
Checked before writing the exception down, so its scope is measured.
`Release` is a route and not a `None`. The family CLAIMS a key-up and
drops it, which is a different fact from no family claiming the event;
collapsing them would hide the drop the moment a slice wants key-up
semantics. `window_event` merges the two arms because both are today
nothing to do, and says so.
Evidence --- 8 rows, 3 further mutations, each failing exactly one row:
M9 a release treated as a press -> the key-action row
M10 a press treated as a release -> the key-action row
M11 harness records outbound only -> the transcript row
`route_one` deliberately calls `route_event` and not the harness, so
the transcript row stays P2's sole owner and M11 stays surgical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai