use super::{
aid_worktree_path, aid_worktree_root, create_worktree,
is_aid_managed_worktree_path, is_safe_worktree_path, remove_worktree,
};
use super::path::WorktreeHomeGuard;
use crate::test_env::CargoTargetDirGuard;
use crate::test_subprocess;
use std::path::Path;
use std::process::Command;
fn expected_project_id(repo_dir: &Path) -> String {
let canonical = repo_dir.canonicalize().unwrap();
let basename = canonical.file_name().unwrap().to_string_lossy();
let hash = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
canonical.to_string_lossy().hash(&mut hasher);
format!("{:x}", hasher.finish())
};
format!("{basename}-{}", hash.chars().take(8).collect::<String>())
}
fn git(repo_dir: &Path, args: &[&str]) {
assert!(Command::new("git")
.args(["-C", &repo_dir.to_string_lossy()])
.args(args)
.status()
.unwrap()
.success());
}
fn init_repo(repo_dir: &Path) {
git(repo_dir, &["init", "-b", "main"]);
git(repo_dir, &["config", "user.email", "test@example.com"]);
git(repo_dir, &["config", "user.name", "Test User"]);
std::fs::write(repo_dir.join("file.txt"), "hello\n").unwrap();
git(repo_dir, &["add", "file.txt"]);
git(repo_dir, &["commit", "-m", "init"]);
}
fn unique_branch(prefix: &str) -> String {
format!(
"{prefix}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
)
}
#[test]
fn remove_worktree_cleans_up_properly() {
let _permit = test_subprocess::acquire();
let repo = tempfile::tempdir().unwrap();
init_repo(repo.path());
let branch = unique_branch("cleanup");
let wt_path = format!("/tmp/aid-wt-test-{branch}");
git(repo.path(), &["worktree", "add", &wt_path, "-b", &branch]);
remove_worktree(&repo.path().to_string_lossy(), &wt_path).unwrap();
assert!(!Path::new(&wt_path).exists());
let out = Command::new("git")
.args(["-C", &repo.path().to_string_lossy(), "worktree", "list"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(!stdout.contains(&branch));
}
#[test]
fn remove_worktree_reaps_branch_target_and_keeps_base() {
let _permit = test_subprocess::acquire();
let aid_home = tempfile::tempdir().unwrap();
let _aid_guard = crate::paths::AidHomeGuard::set(aid_home.path());
let repo = tempfile::tempdir().unwrap();
init_repo(repo.path());
let branch = unique_branch("feat/reap-target");
let wt_path = format!("/tmp/aid-wt-test-{}", branch.replace('/', "-"));
let target_root = aid_home.path().join("cargo-target");
let _target_guard = CargoTargetDirGuard::set(&target_root);
let base = target_root.join("_base");
let branch_target = target_root.join(branch.replace('/', "-"));
std::fs::create_dir_all(&base).unwrap();
std::fs::create_dir_all(&branch_target).unwrap();
git(repo.path(), &["worktree", "add", &wt_path, "-b", &branch]);
remove_worktree(&repo.path().to_string_lossy(), &wt_path).unwrap();
assert!(base.exists());
assert!(!branch_target.exists());
}
#[test]
fn sandbox_allows_aid_worktree_paths() {
let _permit = test_subprocess::acquire();
let home = tempfile::tempdir().unwrap();
let _home_guard = WorktreeHomeGuard::set(home.path());
let home_path = aid_worktree_root().join("demo").join("feat/foo");
assert!(is_safe_worktree_path(&home_path.to_string_lossy()));
assert!(is_safe_worktree_path("/tmp/aid-wt-feat-foo"));
assert!(is_safe_worktree_path("/tmp/aid-wt-fix/bar"));
assert!(is_safe_worktree_path("/private/tmp/aid-wt-test"));
}
#[test]
fn sandbox_blocks_non_worktree_paths() {
let _permit = test_subprocess::acquire();
assert!(!is_safe_worktree_path("/home/user/project"));
assert!(!is_safe_worktree_path("/Users/someone/Develop/myrepo"));
assert!(!is_safe_worktree_path("/tmp/other-dir"));
assert!(!is_safe_worktree_path("/tmp/aid-wt")); assert!(!is_safe_worktree_path("/tmp"));
assert!(!is_safe_worktree_path(""));
assert!(!is_safe_worktree_path("/"));
}
#[test]
fn remove_worktree_refuses_unsafe_path() {
let _permit = test_subprocess::acquire();
let repo = tempfile::tempdir().unwrap();
init_repo(repo.path());
let unsafe_path = repo.path().join("subdir");
std::fs::create_dir_all(&unsafe_path).unwrap();
let result = remove_worktree(
&repo.path().to_string_lossy(),
&unsafe_path.to_string_lossy(),
);
assert!(result.is_err());
assert!(
unsafe_path.exists(),
"Sandbox guard failed: unsafe path was deleted!"
);
}
#[test]
fn create_worktree_uses_aid_home_project_branch_path() {
let permit = test_subprocess::acquire();
let home = tempfile::tempdir().unwrap();
let home_guard = WorktreeHomeGuard::set(home.path());
let repo = tempfile::tempdir().unwrap();
init_repo(repo.path());
let branch = "feat/home-root";
let info = create_worktree(repo.path(), branch, None).unwrap();
let expected = home
.path()
.join(".aid")
.join("worktrees")
.join(expected_project_id(repo.path()))
.join(branch);
assert_eq!(aid_worktree_root(), home.path().join(".aid").join("worktrees"));
assert_eq!(aid_worktree_path(repo.path(), branch), expected);
assert_eq!(info.path, expected);
assert!(info.path.exists());
assert!(info.path.parent().unwrap().exists());
assert!(is_aid_managed_worktree_path(&info.path));
git(
repo.path(),
&[
"worktree",
"remove",
"--force",
&info.path.to_string_lossy(),
],
);
drop(home_guard);
drop(permit);
}
#[test]
fn managed_path_rejects_relative_and_traversal_paths() {
let home = tempfile::tempdir().unwrap();
let home_guard = WorktreeHomeGuard::set(home.path());
let traversal = aid_worktree_root()
.join("project")
.join("..")
.join("..")
.join("outside");
assert!(!is_aid_managed_worktree_path(Path::new("relative/path")));
assert!(!is_aid_managed_worktree_path(&traversal));
drop(home_guard);
}
#[test]
fn managed_path_accepts_nonexistent_child_under_aid_root() {
let home = tempfile::tempdir().unwrap();
let home_guard = WorktreeHomeGuard::set(home.path());
let path = aid_worktree_root()
.join("project")
.join("feat")
.join("nonexistent");
assert!(!path.exists());
assert!(is_aid_managed_worktree_path(&path));
drop(home_guard);
}
#[test]
fn path_from_linked_worktree_uses_main_repo_project_id() {
let permit = test_subprocess::acquire();
let home = tempfile::tempdir().unwrap();
let home_guard = WorktreeHomeGuard::set(home.path());
let repo = tempfile::tempdir().unwrap();
let linked_parent = tempfile::tempdir().unwrap();
let linked = linked_parent.path().join("linked-worktree");
init_repo(repo.path());
git(
repo.path(),
&["worktree", "add", &linked.to_string_lossy(), "-b", "feat/linked"],
);
let branch = "feat/from-linked";
let from_main = aid_worktree_path(repo.path(), branch);
let from_linked = aid_worktree_path(&linked, branch);
let linked_name = linked.file_name().unwrap().to_string_lossy();
assert_eq!(from_linked, from_main);
assert_eq!(
from_linked,
home.path()
.join(".aid")
.join("worktrees")
.join(expected_project_id(repo.path()))
.join(branch)
);
assert!(!from_linked.to_string_lossy().contains(linked_name.as_ref()));
git(
repo.path(),
&["worktree", "remove", "--force", &linked.to_string_lossy()],
);
drop(home_guard);
drop(permit);
}