use crate::common::{
TestRepo, configure_directive_file, directive_file, repo, repo_with_feature_worktree,
repo_with_remote, repo_with_remote_and_feature, setup_snapshot_settings, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs as unix_fs;
use std::path::Path;
#[rstest]
fn test_switch_directive_file(#[from(repo_with_remote)] mut repo: TestRepo) {
let _feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"cd '[^']+'", "cd '[PATH]'");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("switch")
.arg("feature")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
directives.contains("cd '"),
"Directive file should contain cd command, got: {}",
directives
);
});
}
#[rstest]
fn test_merge_directive_file(mut repo_with_remote_and_feature: TestRepo) {
let repo = &mut repo_with_remote_and_feature;
let feature_wt = &repo.worktrees["feature"];
let (directive_path, _guard) = directive_file();
let mut settings = setup_snapshot_settings(repo);
settings.add_filter(r"cd '[^']+'", "cd '[PATH]'");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("merge").arg("main").current_dir(feature_wt);
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
directives.contains("cd '"),
"Directive file should contain cd command, got: {}",
directives
);
});
}
#[rstest]
fn test_remove_directive_file(#[from(repo_with_remote)] mut repo: TestRepo) {
let feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"cd '[^']+'", "cd '[PATH]'");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("remove").current_dir(&feature_wt);
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
directives.contains("cd '"),
"Directive file should contain cd command, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_preserves_subdir(#[from(repo_with_remote)] mut repo: TestRepo) {
let feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let subdir = "apps/gateway";
fs::create_dir_all(repo.root_path().join(subdir)).unwrap();
fs::create_dir_all(feature_wt.join(subdir)).unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("switch")
.arg("feature")
.current_dir(repo.root_path().join(subdir));
let output = cmd.output().unwrap();
assert!(output.status.success(), "wt switch failed: {:?}", output);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let expected_subdir = feature_wt.join(Path::new("apps").join("gateway"));
let expected_str = expected_subdir.to_string_lossy();
assert!(
directives.contains(&*expected_str),
"Directive should cd to subdirectory {}, got: {}",
expected_str,
directives
);
}
#[rstest]
fn test_switch_falls_back_to_root_when_subdir_missing(
#[from(repo_with_remote)] mut repo: TestRepo,
) {
let feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let subdir = "apps/gateway";
fs::create_dir_all(repo.root_path().join(subdir)).unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("switch")
.arg("feature")
.current_dir(repo.root_path().join(subdir));
let output = cmd.output().unwrap();
assert!(output.status.success(), "wt switch failed: {:?}", output);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let feature_str = feature_wt.to_string_lossy();
assert!(
directives.contains(&*feature_str),
"Directive should cd to worktree root {}, got: {}",
feature_str,
directives
);
let subdir_path = feature_wt.join(Path::new("apps").join("gateway"));
let subdir_str = subdir_path.to_string_lossy();
assert!(
!directives.contains(&*subdir_str),
"Directive should NOT cd to missing subdirectory {}, got: {}",
subdir_str,
directives
);
}
#[rstest]
fn test_switch_create_preserves_subdir(#[from(repo_with_remote)] repo: TestRepo) {
let (directive_path, _guard) = directive_file();
let subdir = "apps/gateway";
fs::create_dir_all(repo.root_path().join(subdir)).unwrap();
fs::write(repo.root_path().join(subdir).join(".gitkeep"), "").unwrap();
repo.commit("Add apps/gateway");
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args(["switch", "--create", "new-feature"])
.current_dir(repo.root_path().join(subdir));
let output = cmd.output().unwrap();
assert!(output.status.success(), "wt switch failed: {:?}", output);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let subdir_suffix = Path::new("apps").join("gateway");
let subdir_str = subdir_suffix.to_string_lossy();
assert!(
directives.contains(&*subdir_str),
"New worktree should cd to preserved subdirectory, got: {}",
directives
);
}
#[rstest]
fn test_switch_no_cd_suppresses_directive(#[from(repo_with_remote)] mut repo: TestRepo) {
let _feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args(["switch", "feature", "--no-cd"])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
!directives.contains("cd '"),
"Directive file should NOT contain cd command with --no-cd, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_no_cd_create_suppresses_directive(#[from(repo_with_remote)] repo: TestRepo) {
let (directive_path, _guard) = directive_file();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args(["switch", "--create", "new-feature", "--no-cd"])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
!directives.contains("cd '"),
"Directive file should NOT contain cd command with --no-cd, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_no_cd_hooks_show_path_annotation(#[from(repo_with_remote)] repo: TestRepo) {
let (directive_path, _guard) = directive_file();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
"post-switch = \"echo switched\"\n",
)
.unwrap();
repo.commit("Add config");
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args(["switch", "--create", "hook-test", "--no-cd", "--yes"])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
!directives.contains("cd '"),
"Directive file should NOT contain cd command with --no-cd, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_no_cd_execute_runs_in_target_worktree(#[from(repo_with_remote)] repo: TestRepo) {
let (directive_path, _guard) = directive_file();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args([
"switch",
"--create",
"exec-test",
"--no-cd",
"--execute",
"pwd",
])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
!directives.contains("cd '"),
"Directive file should NOT contain cd command with --no-cd, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_no_cd_config_suppresses_directive(#[from(repo_with_remote)] mut repo: TestRepo) {
let _feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
repo.write_test_config(
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[switch]
cd = false
"#,
);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.args(["switch", "feature"])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
!directives.contains("cd '"),
"Directive file should NOT contain cd command with no-cd config, got: {}",
directives
);
});
}
#[rstest]
fn test_switch_without_directive_file(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("switch")
.arg("my-feature")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_remove_without_directive_file(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("remove").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_merge_directive_no_remove(mut repo_with_feature_worktree: TestRepo) {
let repo = &mut repo_with_feature_worktree;
let feature_wt = &repo.worktrees["feature"];
let (directive_path, _guard) = directive_file();
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("merge")
.arg("main")
.arg("--no-remove")
.current_dir(feature_wt);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_merge_directive_remove(mut repo_with_feature_worktree: TestRepo) {
let repo = &mut repo_with_feature_worktree;
let feature_wt = &repo.worktrees["feature"];
let (directive_path, _guard) = directive_file();
let mut settings = setup_snapshot_settings(repo);
settings.add_filter(r"cd '[^']+'", "cd '[PATH]'");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.arg("merge").arg("main").current_dir(feature_wt);
assert_cmd_snapshot!(cmd);
let directives = std::fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
directives.contains("cd '"),
"Directive file should contain cd command, got: {}",
directives
);
});
}
#[cfg(unix)]
#[rstest]
fn test_switch_preserves_symlink_path(#[from(repo_with_remote)] mut repo: TestRepo) {
let _feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let real_parent = repo.root_path().parent().unwrap();
let symlink_dir = tempfile::tempdir().unwrap();
let symlink_path = symlink_dir.path().join("link");
unix_fs::symlink(real_parent, &symlink_path).unwrap();
let repo_dir_name = repo.root_path().file_name().unwrap();
let logical_cwd = symlink_path.join(repo_dir_name);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.env("PWD", &logical_cwd);
cmd.arg("switch").arg("feature").current_dir(&logical_cwd);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt switch failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let symlink_prefix = symlink_path.to_string_lossy();
assert!(
directives.contains(&*symlink_prefix),
"Directive should use symlink path (containing {}), got: {}",
symlink_prefix,
directives
);
let real_prefix = real_parent.to_string_lossy();
assert!(
!directives.contains(&*real_prefix),
"Directive should NOT contain canonical path {}, got: {}",
real_prefix,
directives
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&*symlink_prefix),
"Display message should contain logical path {}, got: {}",
symlink_prefix,
stderr
);
assert!(
!stderr.contains(&*real_prefix),
"Display message should NOT contain canonical path {}, got: {}",
real_prefix,
stderr
);
}
#[cfg(unix)]
#[rstest]
fn test_switch_create_preserves_symlink_path(#[from(repo_with_remote)] repo: TestRepo) {
let (directive_path, _guard) = directive_file();
let real_parent = repo.root_path().parent().unwrap();
let symlink_dir = tempfile::tempdir().unwrap();
let symlink_path = symlink_dir.path().join("link");
unix_fs::symlink(real_parent, &symlink_path).unwrap();
let repo_dir_name = repo.root_path().file_name().unwrap();
let logical_cwd = symlink_path.join(repo_dir_name);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.env("PWD", &logical_cwd);
cmd.args(["switch", "--create", "new-feature"])
.current_dir(&logical_cwd);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt switch --create failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let symlink_prefix = symlink_path.to_string_lossy();
assert!(
directives.contains(&*symlink_prefix),
"Directive should use symlink path (containing {}), got: {}",
symlink_prefix,
directives
);
let real_prefix = real_parent.to_string_lossy();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&*symlink_prefix),
"Display message should contain logical path {}, got: {}",
symlink_prefix,
stderr
);
assert!(
!stderr.contains(&*real_prefix),
"Display message should NOT contain canonical path {}, got: {}",
real_prefix,
stderr
);
}
#[cfg(unix)]
#[rstest]
fn test_switch_preserves_symlink_path_from_subdirectory(
#[from(repo_with_remote)] mut repo: TestRepo,
) {
let feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let subdir = "apps/gateway";
fs::create_dir_all(repo.root_path().join(subdir)).unwrap();
fs::create_dir_all(feature_wt.join(subdir)).unwrap();
let real_parent = repo.root_path().parent().unwrap();
let symlink_dir = tempfile::tempdir().unwrap();
let symlink_path = symlink_dir.path().join("link");
unix_fs::symlink(real_parent, &symlink_path).unwrap();
let repo_dir_name = repo.root_path().file_name().unwrap();
let logical_cwd = symlink_path.join(repo_dir_name).join(subdir);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.env("PWD", &logical_cwd);
cmd.arg("switch").arg("feature").current_dir(&logical_cwd);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt switch failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
let symlink_prefix = symlink_path.to_string_lossy();
assert!(
directives.contains(&*symlink_prefix),
"Directive should use symlink path (containing {}), got: {}",
symlink_prefix,
directives
);
let subdir_suffix = Path::new("apps").join("gateway");
let subdir_str = subdir_suffix.to_string_lossy();
assert!(
directives.contains(&*subdir_str),
"Directive should preserve subdirectory {}, got: {}",
subdir_str,
directives
);
}
#[cfg(unix)]
#[rstest]
fn test_switch_no_symlink_uses_canonical(#[from(repo_with_remote)] mut repo: TestRepo) {
let _feature_wt = repo.add_worktree("feature");
let (directive_path, _guard) = directive_file();
let canonical_cwd = dunce::canonicalize(repo.root_path()).unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
configure_directive_file(&mut cmd, &directive_path);
cmd.env("PWD", &canonical_cwd);
cmd.arg("switch").arg("feature").current_dir(&canonical_cwd);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt switch failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let directives = fs::read_to_string(&directive_path).unwrap_or_default();
assert!(
directives.contains("cd '"),
"Directive file should contain cd command, got: {}",
directives
);
}