diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 19:02:52 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 19:03:16 +0300 |
| commit | 7f702a7ad026f1503d33c442a50fb6aca84b0dc8 (patch) | |
| tree | 42457fa227d5e7cc0d450d14b2f277b48f7c696a /src/app | |
| parent | de0f43a061162a223c5946a4b50aa9f9ac7ab8d7 (diff) | |
test: harden pane identity regression coverage
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 41 | ||||
| -rw-r--r-- | src/app/state.rs | 224 |
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( + ¬ification.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( |
