From 019cbc1f72da7847cf4fbbdcb4ab57534f97a671 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Mon, 21 Sep 2026 10:19:20 -0700 Subject: [PATCH] Check the LayoutGeometry case list for completeness (improvise-26dm) assert_eq!(cases.len(), 13) catches a case being DELETED from the list and never one being ABSENT from it, and the two are written identically as an assert_eq over a number. The adversarial review of PR #78 added a fourteenth pub struct declaring LayoutGeometry, pointed at by nothing, and the suite stayed green. doctrine(a_list_checked_for_length_reads_like_a_list_checked_ for_completeness). improvise-5ubs's mechanism cannot reach this one -- an effect is a struct implementing a trait, so serde has no derived enumeration to diff against. The WIRE vocabulary does have one, and model_effects maps each ModelCommand variant back to the effect it came from (gotcha 38), so the list is now diffed against the LayoutGeometry effects reachable through ModelCommand. The two sets are equal today, which is the evidence the substitution lost nothing. Injection: drop TogglePruneEmpty -- the old count said 12 != 13, this names the effect. reduce.rs's test module, every_model_command and effect_type_name become pub(crate) so the enumeration and the name reader have ONE home rather than a second copy in effect.rs. Residual, stated in the evidence rather than closed: total over the wire, not over the effects. An effect no ModelCommand names is still invisible -- exactly PR #78's probe. A second [...; 13] array survives one function away in every_layout_geometry_effect_is_visible_to_the_narrow_fingerprint; filed, not smuggled in. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH --- crates/improvise-command/src/ui/effect.rs | 45 +++++++++++++++++++---- crates/improvise-command/src/ui/reduce.rs | 6 +-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/crates/improvise-command/src/ui/effect.rs b/crates/improvise-command/src/ui/effect.rs index d79469a9..1739ace8 100644 --- a/crates/improvise-command/src/ui/effect.rs +++ b/crates/improvise-command/src/ui/effect.rs @@ -1410,11 +1410,36 @@ mod tests { } } - /// Effects declaring `LayoutGeometry` must move `document_config` — the - /// production hash `model_state.dirty` is decided by (improvise-1xf4). + /// The case list below is COMPLETE over the wire vocabulary, which the + /// compiler maintains — improvise-26dm, `context/code/command.md`. #[test] - fn layout_geometry_effects_actually_change_document_config() { - let cases: Vec> = vec![ + fn the_layout_geometry_cases_are_every_wire_effect_of_that_kind() { + let app = grouped_app(); + let mut reachable: Vec = crate::ui::reduce::tests::every_model_command() + .iter() + .flat_map(|cmd| crate::ui::reduce::model_effects(&app, cmd)) + .filter(|e| e.kind() == EffectKind::Model(ModelChange::LayoutGeometry)) + .map(|e| crate::ui::reduce::tests::effect_type_name(e.as_ref())) + .collect(); + let mut listed: Vec = layout_geometry_cases() + .iter() + .map(|e| crate::ui::reduce::tests::effect_type_name(e.as_ref())) + .collect(); + reachable.sort_unstable(); + reachable.dedup(); + listed.sort_unstable(); + listed.dedup(); + assert_eq!( + listed, reachable, + "the LayoutGeometry case list and the wire vocabulary disagree; a name \ + only on the right is an effect nothing here exercises" + ); + } + + /// The `LayoutGeometry` effects, named once so two tests can ask the same + /// list the same questions — improvise-26dm, `context/code/command.md`. + fn layout_geometry_cases() -> Vec> { + vec![ Box::new(AddCategory("Region".into())), Box::new(RemoveCategory("Month".into())), Box::new(CreateView("Second".into())), @@ -1443,10 +1468,14 @@ mod tests { Box::new(CycleAxis("Type".into())), Box::new(SetNumberFormat(",.3f".into())), Box::new(TogglePruneEmpty), - ]; - // A COUNT, not a totality check: nothing forces a fourteenth here, and a - // probe variant proved it — improvise-5ubs, `context/code/command.md`. - assert_eq!(cases.len(), 13, "the LayoutGeometry effects were thirteen"); + ] + } + + /// Effects declaring `LayoutGeometry` must move `document_config` — the + /// production hash `model_state.dirty` is decided by (improvise-1xf4). + #[test] + fn layout_geometry_effects_actually_change_document_config() { + let cases = layout_geometry_cases(); for effect in cases { assert_eq!( effect.kind(), diff --git a/crates/improvise-command/src/ui/reduce.rs b/crates/improvise-command/src/ui/reduce.rs index f3716cc6..13386735 100644 --- a/crates/improvise-command/src/ui/reduce.rs +++ b/crates/improvise-command/src/ui/reduce.rs @@ -273,7 +273,7 @@ pub fn reduce_key(app: &mut App, key: KeyEvent) -> Vec { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::model::cell::CellValue; use crate::ui::app::AppMode; @@ -494,7 +494,7 @@ mod tests { /// One value per `ModelCommand` variant. [`variant_name`] matches /// exhaustively, so a new variant fails the build until it is listed here. - fn every_model_command() -> Vec { + pub(crate) fn every_model_command() -> Vec { vec![ ModelCommand::SetCell { cell: record_cell(&a_stored_cell()), @@ -658,7 +658,7 @@ mod tests { /// An effect's type name, off the leading identifier of its derived `Debug`. /// `Effect` is a trait object, so `std::any::type_name` cannot reach it. - fn effect_type_name(effect: &dyn Effect) -> String { + pub(crate) fn effect_type_name(effect: &dyn Effect) -> String { format!("{effect:?}") .split(|c: char| !c.is_alphanumeric() && c != '_') .next()