The advertised-key guard judges by what the fold moved, off every edge (6562)

heard() compared command types, so a clamped move counted as acting. It
now folds the outcome and compares the view slice (status and disarm
aside); a Model command counts as sent. The fixture is a 3x3 grid with
the cursor at (1,1), three tiles with the cursor at 1, help page 1.
Direction is out of reach for any did-it-move check, so every grid key
is also pressed from the centre and must land where its binding says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-22 21:40:17 -07:00
parent c5e72e889f
commit e6d7d13e90
+101 -15
View File
@@ -2039,6 +2039,35 @@ mod tests {
// ── what is bound and what is not ───────────────────────────────
/// A three-by-three grid with `id` at (1,1) and the cursor on it: no move
/// from there is clamped by an edge (improvise-6562).
fn centred_on(id: CellId) -> ClientState {
let mut centre = CellDisplay::number("1200");
centre.id = Some(id);
let mut cells: Vec<_> = (0..3)
.flat_map(|r| (0..3).map(move |c| ((r, c), CellDisplay::number("7"))))
.collect();
cells[4].1 = centre;
let mut client = ClientState::default();
client.bootstrap(Snapshot {
view: ViewState::default(),
seed: vec![
Projection::Grid(GridUpdate::RowFrame(AxisFrame::flat([
"North", "South", "East",
]))),
Projection::Grid(GridUpdate::ColFrame(AxisFrame::flat([
"Revenue", "Cost", "Profit",
]))),
Projection::Grid(GridUpdate::Cells(cells)),
Projection::Grid(GridUpdate::Extent(GridExtent { rows: 3, cols: 3 })),
Projection::Basis(published()),
],
});
client.window = (3, 3);
client.view.cursor_mut().selected = (1, 1);
client
}
/// A client whose every widget has something in it, so an `Upstream` names
/// an **unhandled command** and never a state that said no (improvise-rmvx).
fn furnished(mode: AppMode) -> ClientState {
@@ -2048,7 +2077,7 @@ mod tests {
/// `furnished`, with `cell` under the cursor. A history in both directions
/// and a yanked value, so no `Cmd` is refused by an empty stack (3t5x).
fn furnished_on(mode: AppMode, cell: CellId) -> ClientState {
let mut state = client_on(cell);
let mut state = centred_on(cell);
let frame = |name: &str| crate::ViewFrame {
view_name: name.into(),
mode: AppMode::Normal,
@@ -2062,8 +2091,8 @@ mod tests {
state.view.mode = mode;
state.grid.page_coords = vec![crate::PageAxis {
cat: "Quarter".into(),
selected: "Q1".into(),
items: Some(vec!["Q1".into(), "Q2".into()]),
selected: "Q2".into(),
items: Some(vec!["Q1".into(), "Q2".into(), "Q3".into()]),
}];
// THREE of everything, every panel cursor seated in the MIDDLE: an edge
// makes `move-panel-cursor` answer `Ignored` for the wrong reason.
@@ -2095,10 +2124,15 @@ mod tests {
axes: Vec::new(),
})
.collect();
state.tiles.tiles = vec![crate::TileEntry {
category: "Region".into(),
axis: Axis::Row,
}];
state.tiles.tiles = ["Region", "Product", "Quarter"]
.into_iter()
.map(|category| crate::TileEntry {
category: category.into(),
axis: Axis::Row,
})
.collect();
state.view.tile_cat_idx = 1;
state.view.help_page = 1;
state.view.cat_panel_cursor = 1;
state.view.formula_cursor = 1;
state.view.view_panel_cursor = 1;
@@ -2124,19 +2158,24 @@ mod tests {
Silent,
}
/// Judged by what moved, never by the variant: a chord's second key is
/// `Local` even when all it does is disarm the prefix.
fn heard(outcome: KeyOutcome) -> Heard {
/// Judged by what the fold MOVED, never by a command's type: a clamped move
/// or a lone disarm changes nothing (improvise-6562). `Model` is sent.
fn heard(state: &mut ClientState, outcome: KeyOutcome) -> Heard {
let commands = match outcome {
KeyOutcome::Upstream(_) | KeyOutcome::Ignored => return Heard::Silent,
KeyOutcome::Request(_) => return Heard::Acts,
KeyOutcome::Local(commands) => commands,
};
let quiet = |view: &ViewState| ViewState {
status_msg: String::new(),
transient_prefix: None,
..view.clone()
};
let before = quiet(&state.view);
commands.iter().for_each(|c| state.fold(c));
let sent = commands.iter().any(|c| matches!(c, Command::Model { .. }));
let said = commands.iter().any(|c| matches!(c, Command::SetStatus(_)));
let did = commands
.iter()
.any(|c| !matches!(c, Command::SetStatus(_) | Command::SetTransientPrefix(None)));
match (did, said) {
match (sent || quiet(&state.view) != before, said) {
(true, _) => Heard::Acts,
(false, true) => Heard::Declines,
(false, false) => Heard::Silent,
@@ -2196,7 +2235,8 @@ mod tests {
commands.iter().for_each(|c| state.fold(c));
}
}
heard(resolve(&state, *last))
let outcome = resolve(&state, *last);
heard(&mut state, outcome)
}
/// improvise-rmvx, wk18, 3t5x. Every advertised key does what it says, or
@@ -2312,6 +2352,52 @@ mod tests {
);
}
/// improvise-6562: "it moved" cannot see a move the WRONG way, so every grid
/// key is pressed from the centre and must land where it says. Evidence file.
#[test]
fn every_grid_move_lands_where_its_binding_says() {
let keymap = Keymaps::shared().get(&ModeKey::Normal).expect("Normal");
let mut moves = Vec::new();
bound_commands(keymap, &mut moves);
// A move's destination is its own args; a jump's is its name.
let mut expected: Vec<(String, Vec<String>, (usize, usize))> = moves
.into_iter()
.filter(|(name, _)| name == "move-selection")
.map(|(name, args)| {
let d = |i: usize| args[i].parse::<isize>().expect("a delta");
let to = ((1 + d(0)) as usize, (1 + d(1)) as usize);
(name, args, to)
})
.collect();
for (name, to) in [
("jump-first-row", (0, 1)),
("jump-last-row", (2, 1)),
("jump-first-col", (1, 0)),
("jump-last-col", (1, 2)),
] {
expected.push((name.to_string(), Vec::new(), to));
}
let mut pressed = 0;
for (name, args, to) in expected {
let wanted = (!args.is_empty()).then_some(args.as_slice());
for sequence in sequences_running(keymap, &name, wanted) {
let mut state = furnished(AppMode::Normal);
for key in &sequence {
if let KeyOutcome::Local(commands) = resolve(&state, *key) {
commands.iter().for_each(|c| state.fold(c));
}
}
assert_eq!(
state.view.cursor().selected,
to,
"{sequence:?} runs `{name} {args:?}` from (1,1)"
);
pressed += 1;
}
}
assert!(pressed >= 14, "only {pressed} keys pressed; the walk went blind");
}
/// improvise-rmvx. `step` checks `declined` first, so a name answered
/// there makes an arm in `composed` UNREACHABLE. Evidence file.
#[test]