test: 1a --- A1-A4 witnesses and the PanelPointer frozen-byte pin
**A1 is an exhaustive loop over all 35 function keys, not spot checks.** The defect it closes was `_ => return None` swallowing F13 upward, and a test covering F1-F12 would have passed against exactly that. Each row also asserts `should_forward_key`, because translating without forwarding leaves a key mapped and inert --- which reads as a daemon keymap gap rather than a frontend one. **A2 asserts both halves**: `BackTab`, and `Shift` still set. A `BackTab` that lost its modifier is indistinguishable from one the user did not shift. **A3** likewise pairs the mapping with forwarding. **A4 establishes idle rather than asserting it.** A fresh `State` starts with `dispatch_idle` false --- the daemon has not spoken yet --- so the first version of the row asserted the precondition and failed. Had it been written the other way round it would have tested the INTERCEPTING case under an idle name, which is the state where Escape never quit anyway: the row would have passed while proving nothing about the behaviour A4 changes. It now sets idle, confirms nothing intercepts, and asserts both halves: the Escape reaches the daemon AND no exit occurs. **The frozen-byte pin sits on `PanelPointer`, not on `TextInput`, and the placement is the point.** `TextInput` is appended, so its own round-trip is byte-identical whether or not a variant was inserted beneath it; only the PREVIOUS final variant's bytes move. Every v6-v23 daemon decodes the variants below `PanelPointer` on every session, so an insertion anywhere earlier is a silent wire break for all of them. MY FIRST MUTATION OF THAT PIN WAS WRONG AND THE PIN WAS RIGHT. I inserted the wedge variant before `TextInput` --- which is to say AFTER `PanelPointer`, exactly where an append belongs --- and the pin passed, correctly, because nothing shifted. Re-run with the wedge BEFORE `PanelPointer`, it fails with the discriminant visibly moving 15 -> 16. Worth recording because a mutation that targets the wrong side of the boundary reports the pin as vacuous when it is sound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
42a0a91f58
commit
76beb79a4d
|
|
@ -4476,6 +4476,48 @@ mod input_routing_tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// A4 — an IDLE Escape reaches the daemon and does not exit.
|
||||
///
|
||||
/// "Idle" is the case that used to quit: with nothing intercepting,
|
||||
/// Escape destroyed the window instead of cancelling. Both halves
|
||||
/// are asserted, because either alone would pass a wrong
|
||||
/// implementation — sending Escape while also exiting, or not
|
||||
/// exiting while also sending nothing.
|
||||
#[test]
|
||||
fn a4_an_idle_escape_reaches_the_daemon_and_does_not_exit() {
|
||||
let mut h = EffectHarness::new();
|
||||
// Establish idle explicitly. A fresh `State` starts with
|
||||
// `dispatch_idle` false — the daemon has not said otherwise yet
|
||||
// — so ASSERTING the precondition rather than setting it would
|
||||
// have tested the intercepting case under an idle name. It
|
||||
// failed exactly that way first.
|
||||
h.app.state.as_mut().expect("harness state").dispatch_idle = true;
|
||||
assert!(
|
||||
!h.app
|
||||
.state
|
||||
.as_ref()
|
||||
.expect("harness state")
|
||||
.daemon_intercepts_keys(),
|
||||
"precondition: nothing intercepts, which is the case that quit"
|
||||
);
|
||||
|
||||
let step = h.feed_keyboard(&Key::Named(NamedKey::Escape), None);
|
||||
|
||||
assert!(
|
||||
step.outbound.iter().any(|e| matches!(
|
||||
e,
|
||||
pmacs_protocol::FrontendEvent::Key(k) if k.key == ProtocolKey::Escape
|
||||
)),
|
||||
"an idle Escape must reach the daemon: {:?}",
|
||||
step.outbound
|
||||
);
|
||||
assert!(
|
||||
!step.local.contains(&LocalEffect::Exit),
|
||||
"and must NOT exit: {:?}",
|
||||
step.local
|
||||
);
|
||||
}
|
||||
|
||||
/// The complement, so the row above cannot pass by sending
|
||||
/// `TextInput` for everything: a SINGLE scalar while intercepting
|
||||
/// still travels as `Key`, which is §5 rule 4 and preserves mode
|
||||
|
|
@ -13709,6 +13751,105 @@ mod tests {
|
|||
assert_eq!(source_line_range(text, 99), (7, 10));
|
||||
}
|
||||
|
||||
/// A1 — every function key winit names maps to the protocol's
|
||||
/// 1-based `F(n)`, and forwards.
|
||||
///
|
||||
/// Written as an exhaustive loop over all 35 rather than spot
|
||||
/// checks: the old code stopped at `_ => return None`, so F13+
|
||||
/// silently did nothing, and a test covering only F1–F12 would have
|
||||
/// passed against exactly that.
|
||||
#[test]
|
||||
fn a1_function_keys_f1_to_f35_map_and_forward() {
|
||||
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey};
|
||||
|
||||
let named = [
|
||||
NamedKey::F1,
|
||||
NamedKey::F2,
|
||||
NamedKey::F3,
|
||||
NamedKey::F4,
|
||||
NamedKey::F5,
|
||||
NamedKey::F6,
|
||||
NamedKey::F7,
|
||||
NamedKey::F8,
|
||||
NamedKey::F9,
|
||||
NamedKey::F10,
|
||||
NamedKey::F11,
|
||||
NamedKey::F12,
|
||||
NamedKey::F13,
|
||||
NamedKey::F14,
|
||||
NamedKey::F15,
|
||||
NamedKey::F16,
|
||||
NamedKey::F17,
|
||||
NamedKey::F18,
|
||||
NamedKey::F19,
|
||||
NamedKey::F20,
|
||||
NamedKey::F21,
|
||||
NamedKey::F22,
|
||||
NamedKey::F23,
|
||||
NamedKey::F24,
|
||||
NamedKey::F25,
|
||||
NamedKey::F26,
|
||||
NamedKey::F27,
|
||||
NamedKey::F28,
|
||||
NamedKey::F29,
|
||||
NamedKey::F30,
|
||||
NamedKey::F31,
|
||||
NamedKey::F32,
|
||||
NamedKey::F33,
|
||||
NamedKey::F34,
|
||||
NamedKey::F35,
|
||||
];
|
||||
for (index, key) in named.into_iter().enumerate() {
|
||||
let n = u8::try_from(index + 1).expect("1..=35 fits");
|
||||
let (k, m) = translate_key(&WKey::Named(key), ModifiersState::empty())
|
||||
.unwrap_or_else(|| panic!("F{n} must map"));
|
||||
assert_eq!(k, ProtocolKey::F(n), "F{n} maps to F({n})");
|
||||
assert!(
|
||||
should_forward_key(k, m),
|
||||
"F{n} must FORWARD; translating without forwarding leaves \
|
||||
the key mapped and inert"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A2 — Shift+Tab is `BackTab`, and the `Shift` stays set.
|
||||
///
|
||||
/// Both halves matter: the daemon binds `Tab` and `BackTab`
|
||||
/// differently, and a `BackTab` that lost its modifier would be
|
||||
/// indistinguishable from one the user did not shift.
|
||||
#[test]
|
||||
fn a2_shift_tab_is_backtab_with_shift_retained() {
|
||||
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey};
|
||||
|
||||
let (plain, plain_mods) =
|
||||
translate_key(&WKey::Named(NamedKey::Tab), ModifiersState::empty()).expect("Tab maps");
|
||||
assert_eq!(plain, ProtocolKey::Tab);
|
||||
assert!(plain_mods.is_empty());
|
||||
|
||||
let (shifted, shifted_mods) =
|
||||
translate_key(&WKey::Named(NamedKey::Tab), ModifiersState::SHIFT)
|
||||
.expect("Shift+Tab maps");
|
||||
assert_eq!(shifted, ProtocolKey::BackTab);
|
||||
assert!(
|
||||
shifted_mods.contains(Modifiers::SHIFT),
|
||||
"Shift is retained on BackTab"
|
||||
);
|
||||
assert!(should_forward_key(shifted, shifted_mods));
|
||||
}
|
||||
|
||||
/// A3 — the menu key reaches the daemon. `ProtocolKey::Menu` and the
|
||||
/// TUI's mapping both already existed; only the GPU translation was
|
||||
/// missing, so the key did nothing in the GUI.
|
||||
#[test]
|
||||
fn a3_context_menu_maps_to_menu_and_forwards() {
|
||||
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey};
|
||||
|
||||
let (k, m) = translate_key(&WKey::Named(NamedKey::ContextMenu), ModifiersState::empty())
|
||||
.expect("ContextMenu maps");
|
||||
assert_eq!(k, ProtocolKey::Menu);
|
||||
assert!(should_forward_key(k, m), "and forwards, or it is inert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_key_maps_motion_named_keys_and_chars() {
|
||||
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey, SmolStr};
|
||||
|
|
|
|||
|
|
@ -1971,6 +1971,38 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_pointer_encoding_is_unchanged_by_the_v24_build() {
|
||||
// GUI arc Stage 1a — the placement pin for `TextInput`, and it
|
||||
// sits on `PanelPointer` rather than on `TextInput` itself for
|
||||
// a structural reason: `TextInput` is APPENDED, so its own
|
||||
// round-trip is identical whether or not a variant was inserted
|
||||
// beneath it. Only the PREVIOUS final variant's bytes move, so
|
||||
// only they can witness the shift.
|
||||
//
|
||||
// `PanelPointer` was the last `FrontendEvent` variant at v23.
|
||||
// Every v6–v23 daemon decodes the variants below it on every
|
||||
// session, so a variant inserted anywhere earlier is a silent
|
||||
// wire break for all of them.
|
||||
let ev = FrontendEvent::PanelPointer {
|
||||
frontend_id: FrontendId(2),
|
||||
geometry_epoch: 1,
|
||||
panel_epoch: 1,
|
||||
buffer_id: pmacs_protocol::BufferId::from_raw(4),
|
||||
coord: CellCoord { row: 0, col: 0 },
|
||||
kind: pmacs_protocol::MouseKind::Move,
|
||||
mods: Modifiers::NONE,
|
||||
};
|
||||
let bytes = postcard::to_allocvec(&ev).expect("encode");
|
||||
assert_eq!(
|
||||
bytes,
|
||||
[15, 2, 1, 1, 4, 0, 0, 3, 0],
|
||||
"PanelPointer's v23 wire bytes changed — a variant was \
|
||||
inserted before it; append new FrontendEvent variants at \
|
||||
the end"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_family_round_trips_and_pins_its_discriminants() {
|
||||
let bid = pmacs_protocol::BufferId::from_raw(9);
|
||||
|
|
|
|||
Loading…
Reference in New Issue