use std::path::Path;
use super::*;
use crate::session_manager::worktree_git_fixture::{GitWorktreeFixture, deny_all};
fn not_a_working_tree_stderr(path: &Path) -> String {
format!("fatal: '{}' is not a working tree\n", path.display())
}
fn real_removal_stderr(repo: &Path, path: &Path) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(["worktree", "remove", "--force"])
.arg(path)
.output()
.expect("spawn git worktree remove");
assert!(
!out.status.success(),
"precondition: git must have DECLINED to remove {}",
path.display()
);
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn a_locked_worktree_is_protected() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("locked-4732");
fx.lock_worktree(&wt);
let stderr = real_removal_stderr(&fx.repo, &wt);
assert!(
stderr.contains("locked working tree"),
"precondition: git must have refused for the LOCK reason: {stderr}"
);
let verdict = protection_after_failed_removal(&wt, &fx.repo, &stderr);
assert!(
matches!(verdict, GitProtection::Protected(_)),
"a locked worktree must never be deletable: {verdict:?}"
);
assert!(wt.exists(), "and the directory must still be there");
}
#[test]
fn a_broken_git_file_is_protected() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("broken-git-file-4732");
std::fs::write(wt.join("precious.txt"), "work\n").expect("write precious file");
std::fs::write(wt.join(".git"), "gitdir: /nonexistent/xyz\n").expect("corrupt .git");
let stderr = real_removal_stderr(&fx.repo, &wt);
let verdict = protection_after_failed_removal(&wt, &fx.repo, &stderr);
assert!(
matches!(verdict, GitProtection::Protected(_)),
"git validated and declined; the near-miss message must not read as \
'nothing here': {stderr} -> {verdict:?}"
);
}
#[test]
fn an_unrecognized_git_message_is_protected() {
let fx = GitWorktreeFixture::new();
let plain = fx.repo.join(".worktrees").join("unrecognized-4732");
std::fs::create_dir_all(&plain).expect("mkdir");
assert_eq!(
registry_verdict(&fx.repo, &plain),
GitProtection::Unclaimed,
"precondition: every OTHER gate permits this path"
);
let verdict =
protection_after_failed_removal(&plain, &fx.repo, "fatal: something entirely new\n");
assert!(
matches!(verdict, GitProtection::Protected(_)),
"an unrecognized refusal must fail closed: {verdict:?}"
);
}
#[test]
fn a_stale_worktree_pointer_is_protected() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("stale-pointer-4732");
std::fs::write(wt.join("precious.txt"), "uncommitted work\n").expect("write precious file");
std::fs::remove_dir_all(fx.repo.join(".git").join("worktrees")).expect("drop admin dir");
assert!(
super::registry_root_for(&wt).is_none(),
"precondition: git must be unable to name the owning checkout"
);
let verdict = protection_without_registry_root(&wt);
assert!(
matches!(verdict, GitProtection::Protected(_)),
"a stale worktree pointer is a broken worktree, not an unmanaged \
directory: {verdict:?}"
);
}
#[test]
fn an_unreadable_git_entry_is_protected() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("unreadable-git-4732");
let _restore = deny_all(&wt.join(".git"));
let verdict = protection_without_registry_root(&wt);
assert!(
matches!(verdict, GitProtection::Protected(_)),
"an unreadable .git is a worktree git could not read, not an absent \
one: {verdict:?}"
);
}
#[test]
fn a_dot_git_entry_overrides_gits_not_a_working_tree_message() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("witness-beats-message-4732");
std::fs::remove_dir_all(fx.repo.join(".git").join("worktrees")).expect("drop admin dir");
assert_eq!(
registry_verdict(&fx.repo, &wt),
GitProtection::Unclaimed,
"precondition: git's registry no longer names this path"
);
let verdict = protection_after_failed_removal(&wt, &fx.repo, ¬_a_working_tree_stderr(&wt));
assert!(
matches!(verdict, GitProtection::Protected(_)),
"the filesystem witness must win over the message: {verdict:?}"
);
}
#[test]
fn a_registered_worktree_is_protected_even_when_git_says_otherwise() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("registered-4732");
std::fs::remove_file(wt.join(".git")).expect("remove .git pointer");
let verdict = protection_after_failed_removal(&wt, &fx.repo, ¬_a_working_tree_stderr(&wt));
assert!(
matches!(verdict, GitProtection::Protected(_)),
"git's registry outranks git's message: {verdict:?}"
);
}
#[test]
fn the_registry_reports_a_lock_as_a_lock() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("registry-lock-4732");
fx.lock_worktree(&wt);
let verdict = registry_verdict(&fx.repo, &wt);
match verdict {
GitProtection::Protected(reason) => assert!(
reason.contains("git-locked"),
"the reason must name the lock: {reason}"
),
other => panic!("a locked worktree must be Protected: {other:?}"),
}
}
#[test]
fn an_unreadable_registry_is_undetermined() {
let tmp = tempfile::tempdir().expect("tempdir");
let outside = tmp.path().join("not-a-repo");
std::fs::create_dir_all(&outside).expect("mkdir");
let verdict = registry_verdict(&outside, &outside);
assert!(
matches!(verdict, GitProtection::Undetermined(_)),
"a registry that could not be read must refuse, not permit: {verdict:?}"
);
}
#[test]
fn an_uncanonicalisable_path_is_undetermined() {
let fx = GitWorktreeFixture::new();
let absent = fx.repo.join(".worktrees").join("never-existed-4732");
let verdict = registry_verdict(&fx.repo, &absent);
assert!(
matches!(verdict, GitProtection::Undetermined(_)),
"an uncanonicalisable path must refuse: {verdict:?}"
);
}
#[test]
fn an_unresolvable_ancestor_repo_is_undetermined() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake_repo = tmp.path().join("fake-repo");
let leftover = fake_repo.join("leftover");
std::fs::create_dir_all(&leftover).expect("mkdir");
std::fs::write(fake_repo.join(".git"), "not a gitfile at all\n").expect("write bogus .git");
let verdict = protection_without_registry_root(&leftover);
assert!(
matches!(verdict, GitProtection::Undetermined(_)),
"an ancestor .git git will not resolve must refuse: {verdict:?}"
);
}
#[test]
fn a_directory_no_repository_claims_is_unclaimed() {
let tmp = tempfile::tempdir().expect("tempdir");
let leftover = tmp.path().join("leftover-4732");
std::fs::create_dir_all(&leftover).expect("mkdir");
assert_eq!(
protection_without_registry_root(&leftover),
GitProtection::Unclaimed,
"a plain directory outside every repository holds no git state"
);
}
#[test]
fn an_unregistered_leftover_under_a_repo_is_unclaimed() {
let fx = GitWorktreeFixture::new();
let leftover = fx.repo.join(".worktrees").join("leftover-4732");
std::fs::create_dir_all(&leftover).expect("mkdir");
assert_eq!(
protection_without_registry_root(&leftover),
GitProtection::Unclaimed,
"a directory the owning repository does not register holds no git state"
);
}
#[test]
fn an_unregistered_directory_inside_a_repo_is_unclaimed() {
let fx = GitWorktreeFixture::new();
let leftover = fx.repo.join(".worktrees").join("unregistered-4732");
std::fs::create_dir_all(&leftover).expect("mkdir");
let stderr = real_removal_stderr(&fx.repo, &leftover);
assert!(
stderr.contains(NOT_A_WORKING_TREE),
"precondition: git's real message for a plain directory: {stderr}"
);
assert_eq!(
protection_after_failed_removal(&leftover, &fx.repo, &stderr),
GitProtection::Unclaimed
);
}
#[test]
fn refusal_covers_both_non_unclaimed_states() {
assert!(
GitProtection::Protected("held".into()).refusal().is_some(),
"Protected must refuse"
);
assert!(
GitProtection::Undetermined("unknown".into())
.refusal()
.is_some(),
"Undetermined must refuse — an unanswerable probe is never a verdict"
);
assert!(
GitProtection::Unclaimed.refusal().is_none(),
"Unclaimed is the only state that permits removal"
);
}