aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-08 17:24:33 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-08 17:24:33 +0300
commit3689515f6a11d0f7635067de2f964b63288c645b (patch)
tree640baa1e16c1c5c88550df5d5a0a219c688de947 /src
parent46f9651dfa78044c7ccc7bc2b9884194ffa22dc8 (diff)
test: strengthen session snapshot capture contract
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs95
-rw-r--r--src/persist.rs193
2 files changed, 288 insertions, 0 deletions
diff --git a/src/app/input.rs b/src/app/input.rs
index 56dc97c9..b9ddc890 100644
--- a/src/app/input.rs
+++ b/src/app/input.rs
@@ -3038,6 +3038,24 @@ mod tests {
}
}
+ fn capture_snapshot(state: &AppState) -> crate::persist::SessionSnapshot {
+ crate::persist::capture(
+ &state.workspaces,
+ state.active,
+ state.selected,
+ state.agent_panel_scope,
+ state.sidebar_width,
+ state.sidebar_section_split,
+ )
+ }
+
+ fn root_layout_ratio(snapshot: &crate::persist::SessionSnapshot) -> Option<f32> {
+ match &snapshot.workspaces.first()?.tabs.first()?.layout {
+ crate::persist::LayoutSnapshot::Split { ratio, .. } => Some(*ratio),
+ crate::persist::LayoutSnapshot::Pane(_) => None,
+ }
+ }
+
#[test]
fn custom_rename_key_enters_rename_mode() {
let mut state = state_with_workspaces(&["test"]);
@@ -3270,6 +3288,11 @@ mod tests {
);
assert_eq!(state.mode, Mode::Terminal);
assert_eq!(state.workspaces[0].display_name(), "renamed");
+ let snapshot = capture_snapshot(&state);
+ assert_eq!(
+ snapshot.workspaces[0].custom_name.as_deref(),
+ Some("renamed")
+ );
state.view.sidebar_rect = Rect::new(0, 0, 26, 20);
state.view.terminal_area = Rect::new(26, 0, 80, 20);
@@ -3282,6 +3305,24 @@ mod tests {
}
#[test]
+ fn tab_rename_updates_captured_snapshot() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.mode = Mode::RenameTab;
+ state.name_input = "logs".into();
+
+ handle_rename_key(
+ &mut state,
+ KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
+ );
+
+ let snapshot = capture_snapshot(&state);
+ assert_eq!(
+ snapshot.workspaces[0].tabs[0].custom_name.as_deref(),
+ Some("logs")
+ );
+ }
+
+ #[test]
fn rename_cancel_returns_to_terminal_when_workspace_is_active() {
let mut state = state_with_workspaces(&["test"]);
state.mode = Mode::RenameTab;
@@ -3691,6 +3732,12 @@ mod tests {
second_pane
);
assert_eq!(app.state.mode, Mode::Terminal);
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(snapshot.workspaces[0].active_tab, second_tab);
+ assert_eq!(
+ snapshot.workspaces[0].tabs[second_tab].focused,
+ Some(second_pane.raw())
+ );
}
#[test]
@@ -3715,6 +3762,8 @@ mod tests {
assert_eq!(app.state.agent_panel_scope, AgentPanelScope::AllWorkspaces);
assert_eq!(app.state.agent_panel_scroll, 0);
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(snapshot.agent_panel_scope, AgentPanelScope::AllWorkspaces);
}
#[test]
@@ -3936,6 +3985,9 @@ mod tests {
assert_eq!(app.state.active, Some(1));
assert_eq!(app.state.selected, 1);
assert!(app.state.workspace_press.is_none());
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(snapshot.active, Some(1));
+ assert_eq!(snapshot.selected, 1);
}
#[test]
@@ -3989,6 +4041,13 @@ mod tests {
assert_eq!(app.state.selected, 2);
assert_eq!(app.state.workspaces[0].id, active_id);
assert_eq!(app.state.workspaces[2].id, selected_id);
+ let snapshot = capture_snapshot(&app.state);
+ let captured_names: Vec<_> = snapshot
+ .workspaces
+ .iter()
+ .map(|ws| ws.custom_name.clone().unwrap())
+ .collect();
+ assert_eq!(captured_names, vec!["b", "a", "c"]);
}
#[test]
@@ -4034,6 +4093,8 @@ mod tests {
app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), 30, 5));
assert_eq!(app.state.sidebar_width, 31);
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(snapshot.sidebar_width, Some(31));
}
#[test]
@@ -4056,6 +4117,38 @@ mod tests {
));
assert!(app.state.sidebar_section_split > 0.5);
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(
+ snapshot.sidebar_section_split,
+ Some(app.state.sidebar_section_split)
+ );
+ }
+
+ #[test]
+ fn dragging_pane_split_updates_captured_layout_ratio() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("test")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.workspaces[0].test_split(Direction::Horizontal);
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+ let border = app.state.view.split_borders[0].clone();
+ let before = capture_snapshot(&app.state);
+ let drag_row = border.area.y.saturating_add(1);
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ border.pos,
+ drag_row,
+ ));
+ app.handle_mouse(mouse(
+ MouseEventKind::Drag(MouseButton::Left),
+ border.pos.saturating_add(6),
+ drag_row,
+ ));
+
+ let after = capture_snapshot(&app.state);
+ assert_ne!(root_layout_ratio(&before), root_layout_ratio(&after));
}
#[test]
@@ -4070,6 +4163,8 @@ mod tests {
assert_eq!(app.state.sidebar_width, 26);
assert!(app.state.drag.is_none());
+ let snapshot = capture_snapshot(&app.state);
+ assert_eq!(snapshot.sidebar_width, Some(26));
}
#[test]
diff --git a/src/persist.rs b/src/persist.rs
index 06c5af61..ccb44ace 100644
--- a/src/persist.rs
+++ b/src/persist.rs
@@ -564,6 +564,11 @@ pub fn load() -> Option<SessionSnapshot> {
#[cfg(test)]
mod tests {
use super::*;
+ use ratatui::layout::Rect;
+
+ use crate::app::{state::AgentPanelScope, AppState, Mode};
+ use crate::layout::NavDirection;
+ use crate::workspace::Workspace;
fn session_fixture(name: &str) -> &'static str {
match name {
@@ -785,6 +790,194 @@ mod tests {
assert_eq!(unique.len(), 3);
}
+ fn state_with_workspaces(names: &[&str]) -> AppState {
+ let mut state = AppState::test_new();
+ state.workspaces = names.iter().map(|name| Workspace::test_new(name)).collect();
+ if !state.workspaces.is_empty() {
+ state.active = Some(0);
+ state.selected = 0;
+ state.mode = Mode::Terminal;
+ }
+ state
+ }
+
+ fn capture_from_state(state: &AppState) -> SessionSnapshot {
+ capture(
+ &state.workspaces,
+ state.active,
+ state.selected,
+ state.agent_panel_scope,
+ state.sidebar_width,
+ state.sidebar_section_split,
+ )
+ }
+
+ fn root_split_ratio(tab: &TabSnapshot) -> Option<f32> {
+ match &tab.layout {
+ LayoutSnapshot::Split { ratio, .. } => Some(*ratio),
+ LayoutSnapshot::Pane(_) => None,
+ }
+ }
+
+ #[test]
+ fn capture_contract_tracks_workspace_order_active_and_selected() {
+ let mut state = state_with_workspaces(&["a", "b", "c"]);
+ state.active = Some(1);
+ state.selected = 2;
+
+ state.move_workspace(1, 0);
+
+ let snapshot = capture_from_state(&state);
+ let ids: Vec<_> = state.workspaces.iter().map(|ws| ws.id.clone()).collect();
+ let captured_ids: Vec<_> = snapshot
+ .workspaces
+ .iter()
+ .map(|ws| ws.id.clone().unwrap())
+ .collect();
+ assert_eq!(captured_ids, ids);
+ assert_eq!(snapshot.active, state.active);
+ assert_eq!(snapshot.selected, state.selected);
+ }
+
+ #[test]
+ fn capture_contract_tracks_workspace_and_tab_names_and_active_tab() {
+ let mut state = state_with_workspaces(&["one"]);
+ state.workspaces[0].set_custom_name("renamed-workspace".into());
+ let second_tab = state.workspaces[0].test_add_tab(Some("logs"));
+ state.workspaces[0].switch_tab(second_tab);
+ state.workspaces[0].tabs[0].set_custom_name("main".into());
+
+ let snapshot = capture_from_state(&state);
+ let workspace = &snapshot.workspaces[0];
+ assert_eq!(workspace.custom_name.as_deref(), Some("renamed-workspace"));
+ assert_eq!(workspace.active_tab, second_tab);
+ assert_eq!(workspace.tabs[0].custom_name.as_deref(), Some("main"));
+ assert_eq!(workspace.tabs[1].custom_name.as_deref(), Some("logs"));
+ }
+
+ #[test]
+ fn capture_contract_tracks_workspace_closure() {
+ let mut state = state_with_workspaces(&["one", "two"]);
+ state.selected = 1;
+ state.active = Some(1);
+
+ state.close_selected_workspace();
+
+ let snapshot = capture_from_state(&state);
+ assert_eq!(snapshot.workspaces.len(), 1);
+ assert_eq!(snapshot.workspaces[0].custom_name.as_deref(), Some("one"));
+ assert_eq!(snapshot.active, Some(0));
+ assert_eq!(snapshot.selected, 0);
+ }
+
+ #[test]
+ fn capture_contract_tracks_sidebar_state() {
+ let mut state = state_with_workspaces(&["one"]);
+ state.sidebar_width = 31;
+ state.sidebar_section_split = 0.4;
+ state.agent_panel_scope = AgentPanelScope::AllWorkspaces;
+
+ let snapshot = capture_from_state(&state);
+ assert_eq!(snapshot.sidebar_width, Some(31));
+ assert_eq!(snapshot.sidebar_section_split, Some(0.4));
+ assert_eq!(snapshot.agent_panel_scope, AgentPanelScope::AllWorkspaces);
+ }
+
+ #[test]
+ fn capture_contract_tracks_layout_focus_zoom_and_root_pane() {
+ let mut state = state_with_workspaces(&["one"]);
+ let root = state.workspaces[0].tabs[0].root_pane;
+ let second = state.workspaces[0].test_split(Direction::Horizontal);
+ state.workspaces[0].tabs[0].layout.focus_pane(second);
+ state.toggle_fullscreen();
+
+ let snapshot = capture_from_state(&state);
+ let tab = &snapshot.workspaces[0].tabs[0];
+ assert!(matches!(tab.layout, LayoutSnapshot::Split { .. }));
+ assert_eq!(tab.focused, Some(second.raw()));
+ assert_eq!(tab.root_pane, Some(root.raw()));
+ assert!(tab.zoomed);
+ assert_eq!(tab.panes.len(), 2);
+ }
+
+ #[test]
+ fn capture_contract_tracks_focus_navigation() {
+ let mut state = state_with_workspaces(&["one"]);
+ let root = state.workspaces[0].tabs[0].root_pane;
+ let second = state.workspaces[0].test_split(Direction::Horizontal);
+ crate::ui::compute_view(&mut state, Rect::new(0, 0, 106, 20));
+
+ state.navigate_pane(NavDirection::Right);
+
+ let snapshot = capture_from_state(&state);
+ assert_eq!(snapshot.workspaces[0].tabs[0].focused, Some(second.raw()));
+ assert_ne!(snapshot.workspaces[0].tabs[0].focused, Some(root.raw()));
+ }
+
+ #[test]
+ fn capture_contract_tracks_resize_ratio_changes() {
+ let mut state = state_with_workspaces(&["one"]);
+ state.workspaces[0].test_split(Direction::Horizontal);
+ crate::ui::compute_view(&mut state, Rect::new(0, 0, 106, 20));
+ let before = capture_from_state(&state);
+
+ state.resize_pane(NavDirection::Right);
+
+ let after = capture_from_state(&state);
+ let before_ratio = root_split_ratio(&before.workspaces[0].tabs[0]).unwrap();
+ let after_ratio = root_split_ratio(&after.workspaces[0].tabs[0]).unwrap();
+ assert_ne!(before_ratio, after_ratio);
+ }
+
+ #[test]
+ fn capture_contract_tracks_tab_closure() {
+ let mut state = state_with_workspaces(&["one"]);
+ let second_tab = state.workspaces[0].test_add_tab(Some("logs"));
+ state.switch_tab(second_tab);
+
+ state.close_tab();
+
+ let snapshot = capture_from_state(&state);
+ let workspace = &snapshot.workspaces[0];
+ assert_eq!(workspace.tabs.len(), 1);
+ assert_eq!(workspace.active_tab, 0);
+ assert!(workspace.tabs[0].custom_name.is_none());
+ }
+
+ #[test]
+ fn capture_contract_tracks_pane_closure() {
+ let mut state = state_with_workspaces(&["one"]);
+ state.workspaces[0].test_split(Direction::Horizontal);
+
+ state.close_pane();
+
+ let snapshot = capture_from_state(&state);
+ let tab = &snapshot.workspaces[0].tabs[0];
+ assert_eq!(tab.panes.len(), 1);
+ assert!(matches!(tab.layout, LayoutSnapshot::Pane(_)));
+ assert!(!tab.zoomed);
+ }
+
+ #[test]
+ fn capture_contract_tracks_workspace_identity_and_pane_cwds() {
+ let mut state = state_with_workspaces(&["one"]);
+ let root = state.workspaces[0].tabs[0].root_pane;
+ state.workspaces[0].tabs[0]
+ .pane_cwds
+ .insert(root, PathBuf::from("/tmp/pion"));
+ let second = state.workspaces[0].test_split(Direction::Horizontal);
+ state.workspaces[0].tabs[0]
+ .pane_cwds
+ .insert(second, PathBuf::from("/tmp/herdr"));
+
+ let snapshot = capture_from_state(&state);
+ let workspace = &snapshot.workspaces[0];
+ let tab = &workspace.tabs[0];
+ assert_eq!(workspace.identity_cwd, PathBuf::from("/tmp/pion"));
+ assert_eq!(tab.panes[&root.raw()].cwd, PathBuf::from("/tmp/pion"));
+ assert_eq!(tab.panes[&second.raw()].cwd, PathBuf::from("/tmp/herdr"));
+ }
+
#[test]
fn old_unversioned_snapshot_loads_as_version_0() {
// Simulate a snapshot from before versioning was added