use super::*;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
#[test]
fn parse_worktree_list_reads_porcelain_records() {
let stdout = "\
worktree /repos/owner/repo
HEAD abc123
branch refs/heads/main
worktree /repos/owner/repo/.worktrees/session-a
HEAD def456
branch refs/heads/session/session-a
worktree /somewhere/detached
HEAD 0f0f0f
detached
worktree /repos/owner/repo/.base
bare
worktree /gone
HEAD 111111
prunable gitdir file points to non-existent location
";
let got = parse_worktree_list(stdout);
assert_eq!(got.len(), 5, "one record per stanza; got {got:?}");
assert_eq!(got[0].path, std::path::PathBuf::from("/repos/owner/repo"));
assert_eq!(got[0].branch.as_deref(), Some("main"));
assert!(got[0].is_main, "git lists the main worktree first");
assert_eq!(got[1].branch.as_deref(), Some("session/session-a"));
assert!(!got[1].is_main);
assert_eq!(got[2].branch, None, "a detached worktree has no branch");
assert!(got[3].bare, "the `bare` attribute line must be read");
assert!(
got[4].prunable,
"`prunable <reason>` must set the flag, not just the bare word"
);
}
#[test]
fn parse_worktree_list_marks_only_the_first_record_main() {
let stdout = "worktree /a\n\nworktree /b\n\nworktree /c\n";
let got = parse_worktree_list(stdout);
let mains: Vec<_> = got.iter().filter(|w| w.is_main).collect();
assert_eq!(mains.len(), 1, "exactly one main record; got {got:?}");
assert_eq!(mains[0].path, std::path::PathBuf::from("/a"));
}
#[test]
fn parse_worktree_list_empty_input_is_empty() {
assert!(parse_worktree_list("").is_empty());
}
#[test]
fn parse_worktree_list_reads_locked_with_and_without_reason() {
let stdout = "\
worktree /repos/owner/repo
HEAD abc123
branch refs/heads/main
worktree /repos/owner/repo/.worktrees/bare-lock
HEAD def456
locked
worktree /repos/owner/repo/.worktrees/lock-with-reason
HEAD 789abc
locked keeping this for the postmortem
worktree /repos/owner/repo/.worktrees/unlocked
HEAD 000111
";
let got = parse_worktree_list(stdout);
assert_eq!(got.len(), 4, "one record per stanza; got {got:?}");
assert!(got[1].locked, "the bare `locked` line must set the flag");
assert!(
got[2].locked,
"`locked <reason>` must set the flag too, not just the bare word"
);
assert!(
!got[3].locked,
"control: a worktree with no `locked` line must not be flagged"
);
}
#[test]
fn registry_root_for_linked_worktree_is_the_owning_checkout() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("session-root-test");
let root = registry_root_for(&wt).expect("a real worktree must resolve a registry root");
assert_eq!(
std::fs::canonicalize(&root).unwrap_or(root),
std::fs::canonicalize(&fixture.repo).unwrap_or_else(|_| fixture.repo.clone()),
);
}
#[test]
fn registry_root_for_ignores_where_the_worktree_is_parked() {
let fixture = GitWorktreeFixture::new();
let parked = fixture.add_worktree_at(
&fixture.repo.join("deeply").join("nested").join("elsewhere"),
"parked",
);
let grandparent = parked
.parent()
.and_then(|p| p.parent())
.expect("candidate has a grandparent");
assert_ne!(
grandparent,
fixture.repo.as_path(),
"test invariant: the grandparent must NOT be the owning checkout, or \
this test cannot distinguish the two rules"
);
let root = registry_root_for(&parked).expect("registry root");
assert_eq!(
std::fs::canonicalize(&root).unwrap_or(root),
std::fs::canonicalize(&fixture.repo).unwrap_or_else(|_| fixture.repo.clone()),
"the owning checkout must be derived from git, not from the path shape"
);
}
#[test]
fn registry_root_for_non_repo_is_none() {
let tmp = tempfile::tempdir().expect("tempdir");
assert!(
registry_root_for(tmp.path()).is_none(),
"a directory outside any repository must resolve to None, never a guess"
);
}
#[test]
fn list_registered_worktrees_reports_a_real_worktree() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("listed");
let listed = list_registered_worktrees(&wt).expect("list");
let canonical_wt = std::fs::canonicalize(&wt).unwrap_or_else(|_| wt.clone());
assert!(
listed.iter().any(
|w| std::fs::canonicalize(&w.path).unwrap_or_else(|_| w.path.clone()) == canonical_wt
),
"the worktree must appear in its own repository's registry; got {listed:?}"
);
}
#[test]
fn list_registered_worktrees_none_outside_a_repo() {
let tmp = tempfile::tempdir().expect("tempdir");
assert!(
list_registered_worktrees(tmp.path()).is_none(),
"an unanswerable probe must be None, never an empty list that reads as \
`no worktrees exist`"
);
}
#[test]
fn enumerate_finds_worktree_at_an_unwalked_location() {
let fixture = GitWorktreeFixture::new();
let parked = fixture.add_worktree_at(&fixture.repo.join("agents").join("scratch"), "wt-1");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical = std::fs::canonicalize(&parked).unwrap_or_else(|_| parked.clone());
assert!(
found.contains(&canonical),
"a registered worktree must be found wherever it lives; got {found:?}"
);
}
#[test]
fn enumerate_finds_worktree_registered_to_base_clone() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_base_clone_worktree("session-in-base");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical = std::fs::canonicalize(&wt).unwrap_or_else(|_| wt.clone());
assert!(
found.contains(&canonical),
"a worktree registered to the .base clone must be enumerated; got {found:?}"
);
}
#[test]
fn enumerate_ignores_plain_directory_that_is_not_a_worktree() {
let fixture = GitWorktreeFixture::new();
let fake = fixture.repo.join(".worktrees").join("just-a-mkdir");
std::fs::create_dir_all(&fake).expect("mkdir");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical = std::fs::canonicalize(&fake).unwrap_or_else(|_| fake.clone());
assert!(
!found.contains(&canonical),
"a plain directory is not a registered worktree; got {found:?}"
);
}
#[test]
fn enumerate_excludes_the_main_checkout() {
let fixture = GitWorktreeFixture::new();
fixture.add_worktree("some-session");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical_repo =
std::fs::canonicalize(&fixture.repo).unwrap_or_else(|_| fixture.repo.clone());
assert!(
!found.contains(&canonical_repo),
"the main checkout must never be enumerated for reclaim; got {found:?}"
);
}
#[test]
fn enumerate_excludes_a_sibling_checkout_of_the_same_repo() {
let fixture = GitWorktreeFixture::new();
let owner_dir = fixture.repo.parent().expect("<repos_root>/<owner>");
let sibling = fixture.add_worktree_at(owner_dir, "operator-checkout");
let control = fixture.add_worktree("control-session");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical_control = std::fs::canonicalize(&control).unwrap_or_else(|_| control.clone());
assert!(
found.contains(&canonical_control),
"control: an in-project worktree MUST still be enumerated, or this test \
proves nothing; got {found:?}"
);
let canonical_sibling = std::fs::canonicalize(&sibling).unwrap_or_else(|_| sibling.clone());
assert!(
!found.contains(&canonical_sibling),
"a checkout parked beside the managed project must never be a reclaim \
candidate; got {found:?}"
);
assert!(sibling.exists(), "the sibling checkout must be untouched");
}
#[test]
fn enumerate_excludes_a_worktree_parked_beside_the_project() {
let fixture = GitWorktreeFixture::new();
let owner_dir = fixture.repo.parent().expect("<repos_root>/<owner>");
let parking_lot = owner_dir.join("repo-worktrees");
let parked = fixture.add_worktree_at(&parking_lot, "wt-1");
let control = fixture.add_worktree("control-session");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical_control = std::fs::canonicalize(&control).unwrap_or_else(|_| control.clone());
assert!(
found.contains(&canonical_control),
"control: an in-project worktree MUST still be enumerated; got {found:?}"
);
let canonical_parked = std::fs::canonicalize(&parked).unwrap_or_else(|_| parked.clone());
assert!(
!found.contains(&canonical_parked),
"`<owner>/repo-worktrees/wt-1` merely shares a string prefix with the \
project `<owner>/repo` and must NOT be enumerated; got {found:?}"
);
assert!(parked.exists(), "the parked worktree must be untouched");
}
#[test]
fn scan_from_anchor_never_admits_the_containment_boundary_itself() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("session-a");
let canonical_wt = std::fs::canonicalize(&wt).unwrap_or_else(|_| wt.clone());
let canonical_root = std::fs::canonicalize(&fixture.repos_root).expect("canonical root");
let canonical_repo = std::fs::canonicalize(&fixture.repo).expect("canonical repo");
let admission_under_boundary = |boundary: &std::path::Path| -> Admission {
let mut collected = std::collections::BTreeSet::new();
scan_from_anchor(&fixture.repo, &canonical_root, boundary, &mut collected);
collected
.into_iter()
.find(|(path, _, _)| path == &canonical_wt)
.map(|(_, _, key)| key.admission)
.expect("the worktree must be SCANNED under either boundary")
};
assert_eq!(
admission_under_boundary(&canonical_repo),
Admission::Admitted,
"control: bounded by the project, a real in-project worktree must be admitted"
);
assert_eq!(
admission_under_boundary(&canonical_wt),
Admission::OutsideProject,
"a path equal to its containment boundary must never be a candidate — \
this is what keeps the operator's project checkout unselectable"
);
}
#[test]
fn enumerate_excludes_a_locked_worktree() {
let fixture = GitWorktreeFixture::new();
let locked = fixture.add_worktree("locked-by-operator");
fixture.lock_worktree(&locked);
let control = fixture.add_worktree("not-locked");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical_control = std::fs::canonicalize(&control).unwrap_or_else(|_| control.clone());
assert!(
found.contains(&canonical_control),
"control: an identically-placed UNLOCKED worktree must be enumerated, so \
the exclusion below is attributable to the lock and nothing else; got {found:?}"
);
let canonical_locked = std::fs::canonicalize(&locked).unwrap_or_else(|_| locked.clone());
assert!(
!found.contains(&canonical_locked),
"a git-locked worktree must never be enumerated for reclaim; got {found:?}"
);
}
#[test]
fn enumerate_excludes_worktrees_outside_the_repos_root() {
let fixture = GitWorktreeFixture::new();
let outside_parent = tempfile::tempdir().expect("tempdir");
let outside = fixture.add_worktree_at(outside_parent.path(), "far-away");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical = std::fs::canonicalize(&outside).unwrap_or_else(|_| outside.clone());
assert!(
!found.contains(&canonical),
"a worktree outside the managed repos root must not be enumerated; got {found:?}"
);
}
#[test]
fn enumerate_ignores_a_worktree_whose_directory_is_gone() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("vanished");
let canonical = std::fs::canonicalize(&wt).unwrap_or_else(|_| wt.clone());
std::fs::remove_dir_all(&wt).expect("remove the worktree directory");
let found = enumerate_registered_worktrees(&fixture.repos_root);
assert!(
!found.contains(&canonical),
"a worktree with no directory left is not a deletion candidate; got {found:?}"
);
}
#[test]
fn enumerate_missing_repos_root_is_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
assert!(enumerate_registered_worktrees(&tmp.path().join("nope")).is_empty());
}
#[test]
fn scan_reports_the_excluded_set_with_reasons() {
let fixture = GitWorktreeFixture::new();
let admitted = fixture.add_worktree("admitted-one");
let locked = fixture.add_worktree("locked-one");
fixture.lock_worktree(&locked);
let outside = fixture.add_worktree_at(&fixture.repos_root.join("owner"), "beside-the-project");
let scan = scan_registered_worktrees(&fixture.repos_root);
let verdict = |p: &std::path::Path| -> Admission {
let c = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
scan.iter()
.find(|s| s.path == c)
.unwrap_or_else(|| panic!("scan must report {}; got {scan:?}", c.display()))
.admission
};
assert_eq!(verdict(&admitted), Admission::Admitted);
assert_eq!(verdict(&locked), Admission::Locked);
assert_eq!(verdict(&outside), Admission::OutsideProject);
assert_eq!(verdict(&fixture.repo), Admission::MainCheckout);
assert_eq!(
Admission::Locked.reason(),
"excluded: git-locked by the operator"
);
let enumerated = enumerate_registered_worktrees(&fixture.repos_root);
let admitted_from_scan: Vec<_> = scan
.iter()
.filter(|s| s.admission == Admission::Admitted)
.map(|s| s.path.clone())
.collect();
assert_eq!(admitted_from_scan.len(), 1, "got {admitted_from_scan:?}");
assert_eq!(enumerated.len(), 1, "got {enumerated:?}");
assert_eq!(enumerated[0], admitted_from_scan[0]);
}
#[test]
fn scan_reports_both_registries_for_the_same_path_prefix() {
let fixture = GitWorktreeFixture::new();
let base_owned = fixture.add_base_clone_worktree("in-base-registry");
let base_worktrees_dir = fixture.repo.join(".base").join(".worktrees");
let repo_owned = fixture.add_worktree_at(&base_worktrees_dir, "in-repo-registry");
let scan = scan_registered_worktrees(&fixture.repos_root);
let registry_of = |p: &std::path::Path| -> std::path::PathBuf {
let c = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
scan.iter()
.find(|s| s.path == c)
.unwrap_or_else(|| panic!("scan must report {}", c.display()))
.registry_root
.clone()
};
let base_root = std::fs::canonicalize(fixture.repo.join(".base")).expect("canonical .base");
let repo_root = std::fs::canonicalize(&fixture.repo).expect("canonical repo");
assert_eq!(registry_of(&base_owned), base_root);
assert_eq!(registry_of(&repo_owned), repo_root);
assert_ne!(
base_root, repo_root,
"the two registries must genuinely differ, or this test proves nothing"
);
}
#[test]
fn enumerate_orders_nested_worktree_before_its_parent() {
let fixture = GitWorktreeFixture::new();
let parent = fixture.add_worktree("outer");
let child = fixture.add_nested_worktree(&parent, ".claude/worktrees", "inner");
let found = enumerate_registered_worktrees(&fixture.repos_root);
let canonical = |p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.into());
let idx = |p: &std::path::Path| {
found
.iter()
.position(|f| f == &canonical(p))
.unwrap_or_else(|| panic!("{} must be enumerated; got {found:?}", p.display()))
};
assert!(
idx(&child) < idx(&parent),
"the nested worktree must precede its container; got {found:?}"
);
}