934 lines
30 KiB
Rust
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, ¶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("/")));
|
|
}
|