Test both arms of the records-mode settle (improvise-4dpj)

settle_normal_mode matches `Normal | RecordsNormal`, and replacing that with a
one-way `Normal` left the ENTIRE workspace suite passing: the existing crossing
test drives the layout both ways, but both branches end in `enter-mode normal`,
so the settle is handed `AppMode::Normal` either way. It discriminates "settles
at all" and "settles against the ending layout", never "settles from either
mode".

Gap 1, a_batch_that_leaves_records_settles_out_of_records_normal: the
production repro -- SetAxis { "_Index", Axis::None } from a records grid.
Injection is the one-way guard; it is the only test in the workspace that fails.

Gap 2, switching_views_across_records_ness_settles_against_the_view_switched_to
(raised by ChatGPT Codex on PR #91, verified correct on main first): the batch
SwitchViewAtCursor emits, both ways. A genuinely different path -- records-ness
is auto-detected from the axes, so nothing in the batch says "records".
Injection is moving the settle back above rebuild_layout; both fail by name.
Scope note in the evidence: this drives the effects, not the view-panel key
path, which needs a CmdContext in another wave's ground this round.

Also carries the command.md half of improvise-hsoe's three duplicate headings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 10:19:10 -07:00
parent 383e33a1a0
commit 78ca25b5e0
3 changed files with 214 additions and 10 deletions
+83
View File
@@ -2604,6 +2604,89 @@ mod tests {
}
}
/// The settle's `RecordsNormal` arm, driven from the mode it names. The
/// `:` crossing above enters the settle in `Normal` both ways — the evidence.
#[test]
fn a_batch_that_leaves_records_settles_out_of_records_normal() {
use crate::view::Axis;
let mut wb = Workbook::new("T");
wb.add_category("Region").unwrap();
wb.add_item("Region", "East").unwrap();
let mut app = App::new(wb, None);
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
assert!(app.layout.is_records_mode(), "setup: the grid is records");
assert_eq!(
app.view_state.mode,
AppMode::RecordsNormal,
"setup: the mode handed to the settle must be RecordsNormal"
);
app.apply_effects(vec![Box::new(super::super::effect::SetAxis {
category: "_Index".into(),
axis: Axis::None,
})]);
assert!(
!app.layout.is_records_mode(),
"setup: the batch has to leave records, or nothing crossed"
);
assert_eq!(
app.view_state.mode,
AppMode::Normal,
"a batch that left records stayed in RecordsNormal (improvise-4dpj)"
);
}
/// The batch `SwitchViewAtCursor` emits, both ways across records-ness:
/// records-ness is auto-detected from the axes, so a view switch moves it.
#[test]
fn switching_views_across_records_ness_settles_against_the_view_switched_to() {
for to_records in [false, true] {
let mut wb = Workbook::new("T");
wb.add_category("Region").unwrap();
wb.add_item("Region", "East").unwrap();
let mut app = App::new(wb, None);
// `R` creates and enters `_Records`; the pivot view it left is the
// other end of every crossing below.
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
let records_view = app.view_state.active_view.clone();
let (from, to) = match to_records {
true => ("Default", records_view.as_str()),
false => (records_view.as_str(), "Default"),
};
app.apply_effects(vec![Box::new(super::super::effect::SwitchView(
from.into(),
))]);
assert_eq!(
app.layout.is_records_mode(),
!to_records,
"setup: the switch has to CROSS, so it must start on the other side"
);
app.apply_effects(vec![
Box::new(super::super::effect::SwitchView(to.into())),
super::super::effect::change_mode(AppMode::Normal),
]);
assert_eq!(
app.layout.is_records_mode(),
to_records,
"setup: the switch has to land, or nothing crossed"
);
let expected = match to_records {
true => AppMode::RecordsNormal,
false => AppMode::Normal,
};
assert_eq!(
app.view_state.mode, expected,
"the batch settled against the view it LEFT, not the one it \
switched to (improvise-4dpj)"
);
}
}
/// A `:` line that really runs settles through `ReturnToNormalUnlessMoved`,
/// which an empty line never reaches. See the evidence.
#[test]