Files
galaxy/app/src/tab_configs/session_config_tests.rs
T

934 lines
30 KiB
Rust

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, &param_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, &param_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, &param_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("/")));
}