aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 19:02:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 19:03:16 +0300
commit7f702a7ad026f1503d33c442a50fb6aca84b0dc8 (patch)
tree42457fa227d5e7cc0d450d14b2f277b48f7c696a /src/app
parentde0f43a061162a223c5946a4b50aa9f9ac7ab8d7 (diff)
test: harden pane identity regression coverage
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs41
-rw-r--r--src/app/state.rs224
2 files changed, 244 insertions, 21 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 99bd0ff6..1538f12b 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -3531,27 +3531,6 @@ mod tests {
assert_eq!(state.workspaces[0].worktree_space().cloned(), membership);
}
- #[test]
- fn update_ready_sets_explicit_upgrade_toast() {
- let mut state = AppState::test_new();
- state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
-
- let updates = state.handle_app_event(crate::events::AppEvent::UpdateReady {
- version: "0.5.0".into(),
- install_command: "herdr update".into(),
- });
-
- assert!(updates.is_empty());
- assert_eq!(state.update_available.as_deref(), Some("0.5.0"));
- assert!(state.latest_release_notes_available);
- let toast = state.toast.as_ref().expect("update toast");
- assert_eq!(toast.title, "v0.5.0 available");
- assert_eq!(
- toast.context,
- "detach, run `herdr update`, then follow its restart guidance"
- );
- }
-
fn mark_agent(state: &mut AppState, ws_idx: usize, tab_idx: usize, pane_id: PaneId) {
state.ensure_test_terminals();
let terminal_id = state.workspaces[ws_idx].tabs[tab_idx]
@@ -3596,6 +3575,7 @@ mod tests {
state.previous_agent();
assert_eq!(state.active, Some(0));
assert_eq!(state.workspaces[0].focused_pane_id(), Some(first_second));
+ state.assert_invariants_for_test();
}
#[test]
@@ -3621,6 +3601,7 @@ mod tests {
assert_eq!(state.active, Some(1));
assert_eq!(state.workspaces[1].focused_pane_id(), Some(second_root));
+ state.assert_invariants_for_test();
}
#[test]
@@ -3633,6 +3614,7 @@ mod tests {
assert!(state.focus_agent_entry(0));
assert_eq!(state.active, Some(0));
assert_eq!(state.workspaces[0].focused_pane_id(), Some(root));
+ state.assert_invariants_for_test();
}
#[test]
@@ -3659,6 +3641,7 @@ mod tests {
assert_eq!(state.active, Some(0));
assert_eq!(state.workspaces[0].focused_pane_id(), Some(first_root));
+ state.assert_invariants_for_test();
}
#[test]
@@ -3688,6 +3671,7 @@ mod tests {
let last_idx = state.workspaces[0].tabs.len() - 1;
assert_eq!(state.workspaces[0].active_tab, last_idx);
assert!(state.agent_panel_scroll > 0);
+ state.assert_invariants_for_test();
}
#[test]
@@ -3958,6 +3942,7 @@ mod tests {
assert_eq!(state.workspaces.len(), 1);
assert_eq!(state.workspaces[0].custom_name.as_deref(), Some("b"));
+ state.assert_invariants_for_test();
}
#[test]
@@ -3970,17 +3955,20 @@ mod tests {
assert!(state.workspaces.is_empty());
assert_eq!(state.mode, Mode::Navigate);
+ state.assert_invariants_for_test();
}
#[test]
fn pane_died_multi_pane_keeps_workspace() {
let mut state = app_with_workspaces(&["test"]);
let second_id = state.workspaces[0].test_split(Direction::Horizontal);
+ state.ensure_test_terminals();
state.handle_pane_died(second_id);
assert_eq!(state.workspaces.len(), 1);
assert_eq!(state.workspaces[0].panes.len(), 1);
+ state.assert_invariants_for_test();
}
#[test]
@@ -3991,6 +3979,7 @@ mod tests {
state.handle_pane_died(fake_id);
assert_eq!(state.workspaces.len(), 1);
+ state.assert_invariants_for_test();
}
#[test]
@@ -4013,6 +4002,7 @@ mod tests {
assert!(state.selection.is_some());
assert!(state.selection_autoscroll.is_some());
+ state.assert_invariants_for_test();
}
#[test]
@@ -4020,6 +4010,7 @@ mod tests {
let mut state = app_with_workspaces(&["test"]);
let first_id = state.workspaces[0].tabs[0].root_pane;
let second_id = state.workspaces[0].test_split(Direction::Horizontal);
+ state.ensure_test_terminals();
state.selection = Some(crate::selection::Selection::anchor(second_id, 0, 0, None));
state.selection_autoscroll = Some(crate::app::state::SelectionAutoscroll {
@@ -4036,6 +4027,7 @@ mod tests {
assert!(state.selection_autoscroll.is_none());
assert_eq!(state.workspaces[0].panes.len(), 1);
assert_eq!(state.workspaces[0].panes.keys().next().unwrap(), &first_id);
+ state.assert_invariants_for_test();
}
#[test]
@@ -4928,6 +4920,7 @@ mod tests {
fn close_pane_removes_from_workspace() {
let mut state = app_with_workspaces(&["test"]);
let closed = state.workspaces[0].test_split(Direction::Horizontal);
+ state.ensure_test_terminals();
assert_eq!(state.workspaces[0].panes.len(), 2);
state.plugin_panes.insert(
closed,
@@ -4940,6 +4933,7 @@ mod tests {
state.close_pane();
assert_eq!(state.workspaces[0].panes.len(), 1);
assert!(!state.plugin_panes.contains_key(&closed));
+ state.assert_invariants_for_test();
}
#[test]
@@ -4985,6 +4979,7 @@ mod tests {
state.close_pane();
assert!(!state.terminals.contains_key(&terminal_id));
+ state.assert_invariants_for_test();
}
#[test]
@@ -5007,6 +5002,7 @@ mod tests {
assert!(!state.terminals.contains_key(&terminal_id));
assert!(!state.plugin_panes.contains_key(&pane_id));
+ state.assert_invariants_for_test();
}
#[test]
@@ -5026,6 +5022,7 @@ mod tests {
assert!(!state.terminals.contains_key(&terminal_id));
assert!(!state.plugin_panes.contains_key(&pane_id));
+ state.assert_invariants_for_test();
}
#[test]
@@ -5042,6 +5039,7 @@ mod tests {
assert_eq!(state.workspaces.len(), 1);
assert_eq!(state.workspaces[0].display_name(), "selected");
assert!(!state.terminals.contains_key(&active_terminal_id));
+ state.assert_invariants_for_test();
}
#[test]
@@ -5058,6 +5056,7 @@ mod tests {
assert_eq!(state.workspaces.len(), 1);
assert_eq!(state.workspaces[0].display_name(), "selected");
assert!(!state.terminals.contains_key(&active_terminal_id));
+ state.assert_invariants_for_test();
}
#[test]
diff --git a/src/app/state.rs b/src/app/state.rs
index 2482a06d..089834d8 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1782,6 +1782,68 @@ impl AppState {
self.selected, 0,
"empty app state should keep selected workspace at 0"
);
+ assert!(
+ self.pane_id_aliases.is_empty(),
+ "empty app state must not keep raw pane aliases"
+ );
+ assert!(
+ self.public_pane_id_aliases.is_empty(),
+ "empty app state must not keep public pane aliases"
+ );
+ assert!(
+ self.previous_pane_focus.is_none(),
+ "empty app state must not keep previous pane focus"
+ );
+ assert!(
+ self.plugin_panes.is_empty(),
+ "empty app state must not keep plugin pane records"
+ );
+ assert!(
+ self.pending_agent_notifications.is_empty(),
+ "empty app state must not keep pending agent notifications"
+ );
+ assert!(
+ self.copy_mode.is_none(),
+ "empty app state must not keep copy mode"
+ );
+ assert!(
+ self.rename_pane_target.is_none(),
+ "empty app state must not keep rename pane target"
+ );
+ assert!(
+ self.selection.is_none(),
+ "empty app state must not keep text selection"
+ );
+ assert!(
+ self.selection_autoscroll.is_none(),
+ "empty app state must not keep selection autoscroll"
+ );
+ if let Some(toast) = &self.toast {
+ assert!(
+ toast.target.is_none(),
+ "empty app state must not keep pane-targeted toast"
+ );
+ }
+ assert!(
+ self.right_click_passthrough.is_none(),
+ "empty app state must not keep right-click passthrough gesture"
+ );
+ assert!(
+ self.drag.is_none(),
+ "empty app state must not keep drag state"
+ );
+ assert!(
+ self.workspace_press.is_none(),
+ "empty app state must not keep workspace press state"
+ );
+ assert!(
+ self.tab_press.is_none(),
+ "empty app state must not keep tab press state"
+ );
+ assert!(
+ self.context_menu.is_none(),
+ "empty app state must not keep context menu"
+ );
return;
}
@@ -1802,6 +1864,7 @@ impl AppState {
);
let mut workspace_ids = std::collections::HashSet::new();
+ let mut workspace_id_to_idx = std::collections::HashMap::new();
let mut pane_ids = std::collections::HashSet::new();
let mut attached_terminal_ids = std::collections::HashSet::new();
for (ws_idx, ws) in self.workspaces.iter().enumerate() {
@@ -1811,6 +1874,7 @@ impl AppState {
ws.id,
ws_idx
);
+ workspace_id_to_idx.insert(ws.id.clone(), ws_idx);
ws.assert_invariants_for_test();
for tab in &ws.tabs {
@@ -1834,6 +1898,166 @@ impl AppState {
}
}
}
+
+ let assert_live_pane = |pane_id: PaneId, context: &str| {
+ assert!(
+ pane_ids.contains(&pane_id),
+ "{context} references missing pane {:?}",
+ pane_id
+ );
+ };
+ let assert_workspace_pane = |workspace_id: &str, pane_id: PaneId, context: &str| {
+ let ws_idx = workspace_id_to_idx
+ .get(workspace_id)
+ .copied()
+ .unwrap_or_else(|| panic!("{context} references missing workspace {workspace_id}"));
+ assert!(
+ self.workspaces[ws_idx].pane_state(pane_id).is_some(),
+ "{context} references pane {:?} outside workspace {}",
+ pane_id,
+ workspace_id
+ );
+ };
+ let assert_workspace_index = |ws_idx: usize, context: &str| {
+ assert!(
+ ws_idx < self.workspaces.len(),
+ "{context} references workspace index {} out of bounds for {} workspaces",
+ ws_idx,
+ self.workspaces.len()
+ );
+ };
+ let assert_tab_index = |ws_idx: usize, tab_idx: usize, context: &str| {
+ assert_workspace_index(ws_idx, context);
+ assert!(
+ tab_idx < self.workspaces[ws_idx].tabs.len(),
+ "{context} references tab index {} out of bounds for workspace {} with {} tabs",
+ tab_idx,
+ ws_idx,
+ self.workspaces[ws_idx].tabs.len()
+ );
+ };
+
+ for (&raw, &pane_id) in &self.pane_id_aliases {
+ assert_live_pane(pane_id, &format!("raw pane alias {raw}"));
+ }
+ for (public_id, &pane_id) in &self.public_pane_id_aliases {
+ assert_live_pane(pane_id, &format!("public pane alias {public_id}"));
+ }
+ if let Some(focus) = &self.previous_pane_focus {
+ assert_workspace_pane(&focus.workspace_id, focus.pane_id, "previous pane focus");
+ }
+ if let Some(toast) = &self.toast {
+ if let Some(target) = &toast.target {
+ assert_workspace_pane(&target.workspace_id, target.pane_id, "toast target");
+ }
+ }
+ for (&pane_id, notification) in &self.pending_agent_notifications {
+ assert_eq!(
+ pane_id, notification.pane_id,
+ "pending agent notification map key must match payload pane id"
+ );
+ assert_workspace_pane(
+ &notification.workspace_id,
+ notification.pane_id,
+ "pending agent notification",
+ );
+ }
+ for &pane_id in self.plugin_panes.keys() {
+ assert_live_pane(pane_id, "plugin pane record");
+ }
+ if let Some(copy_mode) = &self.copy_mode {
+ assert_live_pane(copy_mode.pane_id, "copy mode");
+ }
+ if let Some(pane_id) = self.rename_pane_target {
+ assert_live_pane(pane_id, "rename pane target");
+ }
+ if let Some(selection) = &self.selection {
+ assert_live_pane(selection.pane_id, "text selection");
+ } else {
+ assert!(
+ self.selection_autoscroll.is_none(),
+ "selection autoscroll must not remain without an active text selection"
+ );
+ }
+ if let Some(gesture) = &self.right_click_passthrough {
+ assert_live_pane(gesture.pane_info.id, "right-click passthrough gesture");
+ }
+ if let Some(drag) = &self.drag {
+ match &drag.target {
+ DragTarget::WorkspaceReorder {
+ source_ws_idx,
+ insert_idx,
+ } => {
+ assert_workspace_index(*source_ws_idx, "workspace drag source");
+ if let Some(insert_idx) = insert_idx {
+ assert!(
+ *insert_idx <= self.workspaces.len(),
+ "workspace drag insert index {} out of bounds for {} workspaces",
+ insert_idx,
+ self.workspaces.len()
+ );
+ }
+ }
+ DragTarget::TabReorder {
+ ws_idx,
+ source_tab_idx,
+ insert_idx,
+ } => {
+ assert_tab_index(*ws_idx, *source_tab_idx, "tab drag source");
+ if let Some(insert_idx) = insert_idx {
+ assert!(
+ *insert_idx <= self.workspaces[*ws_idx].tabs.len(),
+ "tab drag insert index {} out of bounds for workspace {} with {} tabs",
+ insert_idx,
+ ws_idx,
+ self.workspaces[*ws_idx].tabs.len()
+ );
+ }
+ }
+ DragTarget::PaneScrollbar { pane_id, .. } => {
+ assert_live_pane(*pane_id, "pane scrollbar drag")
+ }
+ _ => {}
+ }
+ }
+ if let Some(press) = &self.workspace_press {
+ assert_workspace_index(press.ws_idx, "workspace press");
+ }
+ if let Some(press) = &self.tab_press {
+ assert_tab_index(press.ws_idx, press.tab_idx, "tab press");
+ }
+ if let Some(menu) = &self.context_menu {
+ match menu.kind {
+ ContextMenuKind::Workspace { ws_idx }
+ | ContextMenuKind::GitWorkspace { ws_idx, .. } => {
+ assert_workspace_index(ws_idx, "context menu workspace")
+ }
+ ContextMenuKind::Tab { ws_idx, tab_idx } => {
+ assert_tab_index(ws_idx, tab_idx, "context menu tab")
+ }
+ ContextMenuKind::Pane {
+ ws_idx,
+ tab_idx,
+ pane_id,
+ source_pane_id,
+ ..
+ } => {
+ assert_tab_index(ws_idx, tab_idx, "context menu pane tab");
+ assert!(
+ self.workspaces[ws_idx].tabs[tab_idx]
+ .panes
+ .contains_key(&pane_id),
+ "context menu pane references pane {:?} outside workspace {} tab {}",
+ pane_id,
+ ws_idx,
+ tab_idx
+ );
+ if let Some(source_pane_id) = source_pane_id {
+ assert_live_pane(source_pane_id, "context menu source pane");
+ }
+ }
+ }
+ }
}
pub fn insert_test_runtime(