use std::path::Path; use super::*; use crate::terminal::cli_agent::CLIAgent; fn generated_worktree_path_string(repo: &str, worktree_name: &str) -> String { super::super::tab_config::generated_worktree_path(Path::new(repo), worktree_name) .display() .to_string() } // ── SessionType helpers ── #[test] fn terminal_command_prefix_is_none() { assert_eq!(SessionType::Terminal.command_prefix(), None); } #[test] fn oz_command_prefix_is_none() { assert_eq!(SessionType::Oz.command_prefix(), None); } #[test] fn cli_agent_command_prefix_delegates() { assert_eq!( SessionType::CliAgent(CLIAgent::Claude).command_prefix(), Some("claude") ); assert_eq!( SessionType::CliAgent(CLIAgent::Codex).command_prefix(), Some("codex") ); assert_eq!( SessionType::CliAgent(CLIAgent::Gemini).command_prefix(), Some("gemini") ); } // ── build_tab_config ── #[test] fn terminal_no_worktree() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); assert_eq!(config.name, "New tab: project"); assert!(config.title.is_none()); assert_eq!(config.panes.len(), 1); assert_eq!( config.panes[0].directory.as_deref(), Some("/home/user/project") ); assert!(config.panes[0].commands.is_none()); assert!(config.params.is_empty()); } #[test] fn cli_agent_no_worktree() { let config = build_tab_config( &SessionType::CliAgent(CLIAgent::Claude), Path::new("/home/user/project"), false, true, ); assert_eq!(config.panes[0].commands.as_deref().unwrap(), &["claude"]); assert!(config.params.is_empty()); assert!(config.title.is_none()); } #[test] fn terminal_with_worktree() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/repo"), true, false, ); assert_eq!(config.title.as_deref(), Some("{{worktree_branch_name}}")); let expected_worktree_path = generated_worktree_path_string("/home/user/repo", "{{worktree_branch_name}}"); assert_eq!( config.panes[0].commands.as_deref().unwrap(), [ format!("git worktree add -b {{{{worktree_branch_name}}}} {expected_worktree_path}"), format!("cd {expected_worktree_path}"), ] .as_ref() ); assert!(!config.uses_autogenerated_branch_name()); assert!(config.params.contains_key("worktree_branch_name")); let param = &config.params["worktree_branch_name"]; assert_eq!(param.param_type, TabConfigParamType::Text); assert_eq!(param.default.as_deref(), Some("my-feature-branch")); } #[test] fn cli_agent_with_worktree() { let config = build_tab_config( &SessionType::CliAgent(CLIAgent::Gemini), Path::new("/home/user/repo"), true, false, ); // Worktree commands come first, then agent command. let expected_worktree_path = generated_worktree_path_string("/home/user/repo", "{{worktree_branch_name}}"); assert_eq!( config.panes[0].commands.as_deref().unwrap(), [ format!("git worktree add -b {{{{worktree_branch_name}}}} {expected_worktree_path}"), format!("cd {expected_worktree_path}"), "gemini".to_string(), ] .as_ref() ); assert!(config.params.contains_key("worktree_branch_name")); assert_eq!(config.title.as_deref(), Some("{{worktree_branch_name}}")); } #[test] fn oz_no_worktree_same_as_terminal() { let oz = build_tab_config( &SessionType::Oz, Path::new("/home/user/project"), false, true, ); let terminal = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); assert_eq!(oz.panes[0].directory, terminal.panes[0].directory); assert_eq!(oz.panes[0].commands, terminal.panes[0].commands); assert_eq!(oz.params.len(), terminal.params.len()); } #[test] fn oz_with_worktree_has_worktree_commands_but_no_agent_command() { let config = build_tab_config(&SessionType::Oz, Path::new("/home/user/repo"), true, false); let expected_worktree_path = generated_worktree_path_string("/home/user/repo", "{{worktree_branch_name}}"); assert_eq!( config.panes[0].commands.as_deref().unwrap(), [ format!("git worktree add -b {{{{worktree_branch_name}}}} {expected_worktree_path}"), format!("cd {expected_worktree_path}"), ] .as_ref() ); assert!(config.params.contains_key("worktree_branch_name")); } #[test] fn directory_path_is_absolute_in_directory() { let config = build_tab_config( &SessionType::Terminal, Path::new("/absolute/path/here"), false, true, ); let directory = config.panes[0].directory.as_deref().unwrap(); assert!( directory.starts_with('/'), "directory should be absolute, got: {directory}" ); } // ── TOML round-trip ── #[test] fn round_trip_terminal_no_worktree() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); let toml_str = toml::to_string_pretty(&config).expect("Should serialize"); let parsed: TabConfig = toml::from_str(&toml_str).expect("Should deserialize"); assert_eq!(parsed.name, config.name); assert_eq!(parsed.panes[0].directory, config.panes[0].directory); assert_eq!(parsed.panes[0].commands, config.panes[0].commands); } #[test] fn round_trip_cli_agent_with_worktree() { let config = build_tab_config( &SessionType::CliAgent(CLIAgent::Claude), Path::new("/home/user/repo"), true, false, ); let toml_str = toml::to_string_pretty(&config).expect("Should serialize"); let parsed: TabConfig = toml::from_str(&toml_str).expect("Should deserialize"); assert_eq!(parsed.name, config.name); assert_eq!(parsed.title, config.title); assert_eq!(parsed.panes[0].commands, config.panes[0].commands); assert!(parsed.params.contains_key("worktree_branch_name")); assert_eq!( parsed.params["worktree_branch_name"].default.as_deref(), Some("my-feature-branch") ); } // ── render_tab_config integration ── #[test] fn render_terminal_produces_correct_pane_template() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); let param_values = config.default_param_values(); let (title, pane_template) = super::super::render_tab_config(&config, ¶m_values, None); assert!(title.is_none()); if let crate::launch_configs::launch_config::PaneTemplateType::PaneTemplate { cwd, commands, .. } = pane_template { assert_eq!(cwd, std::path::PathBuf::from("/home/user/project")); assert!(commands.is_empty()); } else { panic!("Expected PaneTemplate variant"); } } #[test] fn render_cli_agent_produces_correct_commands() { let config = build_tab_config( &SessionType::CliAgent(CLIAgent::Claude), Path::new("/home/user/project"), false, true, ); let param_values = config.default_param_values(); let (_, pane_template) = super::super::render_tab_config(&config, ¶m_values, None); if let crate::launch_configs::launch_config::PaneTemplateType::PaneTemplate { commands, .. } = pane_template { assert_eq!(commands.len(), 1); assert_eq!(commands[0].exec, "claude"); } else { panic!("Expected PaneTemplate variant"); } } #[test] fn render_worktree_substitutes_default_branch_name() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/repo"), true, false, ); let param_values = config.default_param_values(); let (title, pane_template) = super::super::render_tab_config(&config, ¶m_values, None); assert_eq!(title.as_deref(), Some("my-feature-branch")); if let crate::launch_configs::launch_config::PaneTemplateType::PaneTemplate { commands, .. } = pane_template { assert!(commands[0].exec.contains("my-feature-branch")); assert!(commands[1].exec.contains("my-feature-branch")); } else { panic!("Expected PaneTemplate variant"); } } // ── write_tab_config (file naming) ── #[test] fn write_tab_config_creates_file_with_correct_naming() { let dir = tempfile::tempdir().expect("Should create temp dir"); let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); let path1 = write_tab_config(&config, dir.path(), "startup_config") .expect("First write should succeed"); assert_eq!(path1.file_name().unwrap(), "startup_config.toml"); let path2 = write_tab_config(&config, dir.path(), "startup_config") .expect("Second write should succeed"); assert_eq!(path2.file_name().unwrap(), "startup_config_1.toml"); let path3 = write_tab_config(&config, dir.path(), "startup_config") .expect("Third write should succeed"); assert_eq!(path3.file_name().unwrap(), "startup_config_2.toml"); } #[test] fn write_tab_config_content_is_valid_toml() { let dir = tempfile::tempdir().expect("Should create temp dir"); let config = build_tab_config( &SessionType::CliAgent(CLIAgent::Claude), Path::new("/home/user/repo"), true, false, ); let path = write_tab_config(&config, dir.path(), "startup_config").expect("Write should succeed"); let contents = std::fs::read_to_string(&path).expect("Should read file"); let parsed: TabConfig = toml::from_str(&contents).expect("Should parse as TabConfig"); assert_eq!(parsed.name, "Worktree: repo"); assert_eq!(parsed.panes[0].commands.as_ref().unwrap().len(), 3); assert!(parsed.params.contains_key("worktree_branch_name")); } #[test] fn write_tab_config_creates_directory_if_missing() { let dir = tempfile::tempdir().expect("Should create temp dir"); let nested = dir.path().join("nested").join("tab_configs"); let config = build_tab_config(&SessionType::Terminal, Path::new("/tmp"), false, true); let path = write_tab_config(&config, &nested, "startup_config").expect("Write should succeed"); assert!(path.exists()); } #[test] fn write_tab_config_custom_base_name() { let dir = tempfile::tempdir().expect("Should create temp dir"); let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/project"), false, true, ); let path = write_tab_config(&config, dir.path(), "my_tab_config").expect("Write should succeed"); assert_eq!(path.file_name().unwrap(), "my_tab_config.toml"); } #[test] fn terminal_with_autogenerated_worktree() { let config = build_tab_config( &SessionType::Terminal, Path::new("/home/user/repo"), true, true, ); assert!(config.title.is_none()); let expected_worktree_path = generated_worktree_path_string("/home/user/repo", "{{autogenerated_branch_name}}"); assert_eq!( config.panes[0].commands.as_deref().unwrap(), [ format!( "git worktree add -b {{{{autogenerated_branch_name}}}} {expected_worktree_path}" ), format!("cd {expected_worktree_path}"), ] .as_ref() ); assert!(config.uses_autogenerated_branch_name()); assert!(config.params.is_empty()); } // ── tab_config_from_pane_snapshot ── use crate::app_state::{ BranchSnapshot, LeafContents, LeafSnapshot, PaneNodeSnapshot, TerminalPaneSnapshot, }; use crate::tab_configs::tab_config::TabConfigPaneType; fn make_terminal_leaf(cwd: Option<&str>, is_focused: bool) -> PaneNodeSnapshot { PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused, custom_vertical_tabs_title: None, contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![], cwd: cwd.map(|s| s.to_string()), shell_launch_data: None, is_active: false, is_read_only: false, input_config: None, llm_model_override: None, active_profile_id: None, conversation_ids_to_restore: vec![], active_conversation_id: None, }), }) } #[test] fn snapshot_single_terminal_pane() { let snapshot = make_terminal_leaf(Some("/home/user/project"), true); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.name, "My Tab Config"); assert!(config.title.is_none()); assert!(config.color.is_none()); assert_eq!(config.panes.len(), 1); assert_eq!(config.panes[0].id, "p1"); assert_eq!(config.panes[0].pane_type, Some(TabConfigPaneType::Terminal)); assert_eq!( config.panes[0].directory.as_deref(), Some("/home/user/project") ); assert_eq!(config.panes[0].is_focused, Some(true)); assert!(config.panes[0].split.is_none()); assert!(config.panes[0].children.is_none()); assert!(config.params.is_empty()); } #[test] fn snapshot_two_pane_horizontal_split() { let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/home/user/a"), true), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/home/user/b"), false), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.panes.len(), 3); // Root should be the split node. assert_eq!(config.panes[0].id, "p1"); assert_eq!( config.panes[0].split, Some(crate::launch_configs::launch_config::SplitDirection::Horizontal) ); assert_eq!( config.panes[0].children, Some(vec!["p2".to_string(), "p3".to_string()]) ); // Children. assert_eq!(config.panes[1].directory.as_deref(), Some("/home/user/a")); assert_eq!(config.panes[1].is_focused, Some(true)); assert_eq!(config.panes[2].directory.as_deref(), Some("/home/user/b")); assert!(config.panes[2].is_focused.is_none()); } #[test] fn snapshot_2x2_grid() { let left = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Vertical, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/a"), true), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/b"), false), ), ], }); let right = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Vertical, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/c"), false), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/d"), false), ), ], }); let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ (crate::app_state::PaneFlex(0.5), left), (crate::app_state::PaneFlex(0.5), right), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); // 3 split nodes + 4 leaf nodes = 7 panes. assert_eq!(config.panes.len(), 7); // Root is p1 (horizontal split). assert_eq!(config.panes[0].id, "p1"); assert!(config.panes[0].split.is_some()); } #[test] fn snapshot_non_terminal_leaf_replaced_with_terminal() { use crate::app_state::NotebookPaneSnapshot; use crate::drive::OpenGalaxyDriveObjectSettings; let notebook_leaf = PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused: false, custom_vertical_tabs_title: None, contents: LeafContents::Notebook(NotebookPaneSnapshot::CloudNotebook { notebook_id: None, settings: OpenGalaxyDriveObjectSettings::default(), }), }); let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/home/user"), true), ), (crate::app_state::PaneFlex(0.5), notebook_leaf), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.panes.len(), 3); // The notebook pane should be replaced with a terminal (no cwd). assert_eq!(config.panes[2].pane_type, Some(TabConfigPaneType::Terminal)); assert!(config.panes[2].directory.is_none()); } #[test] fn snapshot_preserves_custom_title_and_color() { let snapshot = make_terminal_leaf(Some("/home/user"), true); let config = tab_config_from_pane_snapshot( &snapshot, Some("My Project".to_string()), Some(crate::themes::theme::AnsiColorIdentifier::Blue), ); assert_eq!(config.title.as_deref(), Some("My Project")); assert_eq!( config.color, Some(crate::themes::theme::AnsiColorIdentifier::Blue) ); } #[test] fn snapshot_round_trip_toml() { let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/home/user/a"), true), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/home/user/b"), false), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, Some("Test".to_string()), None); let toml_str = toml::to_string_pretty(&config).expect("Should serialize"); let parsed: TabConfig = toml::from_str(&toml_str).expect("Should deserialize"); assert_eq!(parsed.name, config.name); assert_eq!(parsed.title, config.title); assert_eq!(parsed.panes.len(), config.panes.len()); assert_eq!(parsed.panes[0].id, config.panes[0].id); assert_eq!(parsed.panes[1].directory, config.panes[1].directory); assert_eq!(parsed.panes[2].directory, config.panes[2].directory); } // ── snapshot pane_type derivation ── use crate::ai::agent::conversation::AIConversationId; use crate::app_state::AmbientAgentPaneSnapshot; fn make_agent_leaf(cwd: Option<&str>, is_focused: bool) -> PaneNodeSnapshot { PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused, custom_vertical_tabs_title: None, contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![], cwd: cwd.map(|s| s.to_string()), shell_launch_data: None, is_active: false, is_read_only: false, input_config: None, llm_model_override: None, active_profile_id: None, conversation_ids_to_restore: vec![], active_conversation_id: Some(AIConversationId::new()), }), }) } fn make_cloud_leaf(is_focused: bool) -> PaneNodeSnapshot { PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused, custom_vertical_tabs_title: None, contents: LeafContents::AmbientAgent(AmbientAgentPaneSnapshot { uuid: vec![], task_id: None, }), }) } #[test] fn snapshot_agent_pane_gets_agent_type() { let snapshot = make_agent_leaf(Some("/home/user/project"), true); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.panes.len(), 1); assert_eq!(config.panes[0].pane_type, Some(TabConfigPaneType::Agent)); assert_eq!( config.panes[0].directory.as_deref(), Some("/home/user/project") ); assert_eq!(config.panes[0].is_focused, Some(true)); } #[test] fn snapshot_cloud_pane_gets_cloud_type() { let snapshot = make_cloud_leaf(true); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.panes.len(), 1); assert_eq!(config.panes[0].pane_type, Some(TabConfigPaneType::Cloud)); assert!(config.panes[0].directory.is_none()); } #[test] fn snapshot_mixed_terminal_agent_cloud_split() { let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.33), make_terminal_leaf(Some("/home/user/a"), true), ), ( crate::app_state::PaneFlex(0.33), make_agent_leaf(Some("/home/user/b"), false), ), (crate::app_state::PaneFlex(0.33), make_cloud_leaf(false)), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); // 1 split + 3 leaves = 4 panes. assert_eq!(config.panes.len(), 4); assert_eq!(config.panes[1].pane_type, Some(TabConfigPaneType::Terminal)); assert_eq!(config.panes[2].pane_type, Some(TabConfigPaneType::Agent)); assert_eq!(config.panes[3].pane_type, Some(TabConfigPaneType::Cloud)); } // ── nested / multi-level pane layout edge cases ── #[test] fn snapshot_3_deep_nesting() { // Root(H) -> left(V) -> inner(H) -> [leaf_c, leaf_d] // -> leaf_b // -> right_leaf let inner = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/c"), false), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/d"), false), ), ], }); let left = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Vertical, children: vec![ (crate::app_state::PaneFlex(0.5), inner), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/b"), false), ), ], }); let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ (crate::app_state::PaneFlex(0.5), left), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/a"), true), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); // 3 splits + 4 leaves = 7 panes. assert_eq!(config.panes.len(), 7); // Root-first ordering: each split appears before its subtree. assert_eq!(config.panes[0].id, "p1"); // root H-split assert!(config.panes[0].split.is_some()); assert_eq!( config.panes[0].children, Some(vec!["p2".to_string(), "p7".to_string()]) ); assert_eq!(config.panes[1].id, "p2"); // left V-split assert!(config.panes[1].split.is_some()); assert_eq!( config.panes[1].children, Some(vec!["p3".to_string(), "p6".to_string()]) ); assert_eq!(config.panes[2].id, "p3"); // inner H-split assert!(config.panes[2].split.is_some()); assert_eq!( config.panes[2].children, Some(vec!["p4".to_string(), "p5".to_string()]) ); // Leaves assert_eq!(config.panes[3].directory.as_deref(), Some("/c")); assert_eq!(config.panes[4].directory.as_deref(), Some("/d")); assert_eq!(config.panes[5].directory.as_deref(), Some("/b")); assert_eq!(config.panes[6].directory.as_deref(), Some("/a")); assert_eq!(config.panes[6].is_focused, Some(true)); } #[test] fn snapshot_asymmetric_tree() { // Root(H) -> deep_left(V) -> [leaf_a, leaf_b] // -> right_leaf // Left subtree has 3 panes; right has 1. let deep_left = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Vertical, children: vec![ ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/a"), true), ), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/b"), false), ), ], }); let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ (crate::app_state::PaneFlex(0.7), deep_left), ( crate::app_state::PaneFlex(0.3), make_agent_leaf(Some("/c"), false), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); // 2 splits + 3 leaves = 5 panes. assert_eq!(config.panes.len(), 5); assert_eq!(config.panes[0].id, "p1"); // root assert_eq!( config.panes[0].children, Some(vec!["p2".to_string(), "p5".to_string()]) ); assert_eq!(config.panes[1].id, "p2"); // deep_left assert_eq!(config.panes[4].pane_type, Some(TabConfigPaneType::Agent)); } #[test] fn snapshot_3_way_split() { let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.33), make_terminal_leaf(Some("/a"), true), ), ( crate::app_state::PaneFlex(0.33), make_terminal_leaf(Some("/b"), false), ), ( crate::app_state::PaneFlex(0.33), make_terminal_leaf(Some("/c"), false), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); assert_eq!(config.panes.len(), 4); assert_eq!(config.panes[0].id, "p1"); assert_eq!( config.panes[0].children, Some(vec!["p2".to_string(), "p3".to_string(), "p4".to_string()]) ); assert_eq!(config.panes[1].directory.as_deref(), Some("/a")); assert_eq!(config.panes[2].directory.as_deref(), Some("/b")); assert_eq!(config.panes[3].directory.as_deref(), Some("/c")); } #[test] fn snapshot_round_trip_agent_and_cloud_pane_types() { let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ ( crate::app_state::PaneFlex(0.33), make_terminal_leaf(Some("/term"), true), ), ( crate::app_state::PaneFlex(0.33), make_agent_leaf(Some("/agent"), false), ), (crate::app_state::PaneFlex(0.33), make_cloud_leaf(false)), ], }); let config = tab_config_from_pane_snapshot(&snapshot, Some("Mixed".to_string()), None); let toml_str = toml::to_string_pretty(&config).expect("Should serialize"); let parsed: TabConfig = toml::from_str(&toml_str).expect("Should deserialize"); assert_eq!(parsed.panes.len(), 4); assert_eq!(parsed.panes[1].pane_type, Some(TabConfigPaneType::Terminal)); assert_eq!(parsed.panes[1].directory.as_deref(), Some("/term")); assert_eq!(parsed.panes[2].pane_type, Some(TabConfigPaneType::Agent)); assert_eq!(parsed.panes[2].directory.as_deref(), Some("/agent")); assert_eq!(parsed.panes[3].pane_type, Some(TabConfigPaneType::Cloud)); assert!(parsed.panes[3].directory.is_none()); } #[test] fn snapshot_round_trip_3_deep_nesting() { let inner = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Vertical, children: vec![ ( crate::app_state::PaneFlex(0.5), make_agent_leaf(Some("/x"), false), ), (crate::app_state::PaneFlex(0.5), make_cloud_leaf(true)), ], }); let snapshot = PaneNodeSnapshot::Branch(BranchSnapshot { direction: crate::app_state::SplitDirection::Horizontal, children: vec![ (crate::app_state::PaneFlex(0.5), inner), ( crate::app_state::PaneFlex(0.5), make_terminal_leaf(Some("/y"), false), ), ], }); let config = tab_config_from_pane_snapshot(&snapshot, None, None); let toml_str = toml::to_string_pretty(&config).expect("Should serialize"); let parsed: TabConfig = toml::from_str(&toml_str).expect("Should deserialize"); // Verify structural integrity after round-trip. assert_eq!(parsed.panes.len(), 5); // Root split references inner split and leaf. assert_eq!( parsed.panes[0].children, Some(vec!["p2".to_string(), "p5".to_string()]) ); // Inner split references its two leaves. assert_eq!( parsed.panes[1].children, Some(vec!["p3".to_string(), "p4".to_string()]) ); assert_eq!(parsed.panes[2].pane_type, Some(TabConfigPaneType::Agent)); assert_eq!(parsed.panes[3].pane_type, Some(TabConfigPaneType::Cloud)); assert_eq!(parsed.panes[4].pane_type, Some(TabConfigPaneType::Terminal)); } // ── is_git_repo ── #[test] fn detects_git_repo_with_dot_git_dir() { let dir = tempfile::tempdir().expect("Should create temp dir"); std::fs::create_dir(dir.path().join(".git")).expect("Should create .git dir"); assert!(is_git_repo(dir.path())); } #[test] fn detects_non_git_directory() { let dir = tempfile::tempdir().expect("Should create temp dir"); assert!(!is_git_repo(dir.path())); } #[test] fn detects_git_repo_in_parent() { let dir = tempfile::tempdir().expect("Should create temp dir"); std::fs::create_dir(dir.path().join(".git")).expect("Should create .git dir"); let subdir = dir.path().join("src"); std::fs::create_dir(&subdir).expect("Should create subdir"); assert!(is_git_repo(&subdir)); } #[test] fn root_directory_does_not_loop() { // Ensures the walk terminates at the filesystem root without panicking. assert!(!is_git_repo(Path::new("/"))); }