use super::*;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
fn enumerate_all(repos_root: &std::path::Path, adopted: &[PathBuf]) -> Vec<PathBuf> {
enumerate_registered_worktrees_in(repos_root, adopted, &WorktreeScope::all())
.into_iter()
.map(|row| row.path)
.collect()
}
#[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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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_all(&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:?}"
);
}
#[test]
fn parse_worktree_list_keeps_the_harness_agent_lock_reason() {
let stdout = "\
worktree /repos/owner/repo
HEAD abc123
branch refs/heads/main
worktree /repos/owner/repo/.claude/worktrees/agent-a1c
HEAD def456
branch refs/heads/worktree-agent-a1c
locked claude agent agent-a1c (pid 11002 start Mon Aug 31 20:33:51 2026)
worktree /repos/owner/repo/.worktrees/kept
HEAD 999999
branch refs/heads/session/kept
locked
";
let got = parse_worktree_list(stdout);
assert_eq!(got.len(), 3, "one record per stanza; got {got:?}");
assert!(got[1].locked);
assert_eq!(
got[1].lock_reason.as_deref(),
Some("claude agent agent-a1c (pid 11002 start Mon Aug 31 20:33:51 2026)"),
"the reason must survive the parse"
);
assert!(got[2].locked, "a bare `locked` line still locks");
assert_eq!(
got[2].lock_reason, None,
"and carries no reason to misread as the harness's"
);
}
#[test]
fn harness_agent_lock_reason_matches_the_harness_shape() {
assert!(is_harness_agent_lock_reason(
"claude agent agent-a1c (pid 11002 start Mon Aug 31 20:33:51 2026)"
));
assert!(
is_harness_agent_lock_reason("Claude Agent agent-a1c (pid 1)"),
"the shape is what matters, not the casing"
);
}
#[test]
fn harness_agent_lock_reason_rejects_an_operator_lock() {
assert!(!is_harness_agent_lock_reason(""));
assert!(!is_harness_agent_lock_reason(
"do not remove - salvaging work"
));
assert!(
!is_harness_agent_lock_reason("held for review by claude agent later"),
"a mention further along the string is not the harness's own reason"
);
}
#[test]
fn harness_lock_pid_reads_the_measured_reason_shape() {
assert_eq!(
harness_lock_pid_in_reason(
"claude agent agent-a92efd8de8a9e6960 (pid 17167 start Mon Sep 14 15:30:46 2026)"
),
Some(17167)
);
}
#[test]
fn harness_lock_pid_rejects_an_operator_lock() {
assert_eq!(
harness_lock_pid_in_reason("do not remove (pid 17167)"),
None
);
}
#[test]
fn harness_lock_pid_is_none_when_the_reason_names_no_pid() {
assert_eq!(harness_lock_pid_in_reason("claude agent agent-a1c"), None);
assert_eq!(
harness_lock_pid_in_reason("claude agent agent-a1c (pid notanumber)"),
None
);
}
#[test]
fn harness_lock_pid_of_a_non_worktree_is_none() {
let tmp = tempfile::tempdir().expect("tempdir");
assert_eq!(harness_agent_lock_pid(tmp.path()), None);
}
#[test]
fn pid_liveness_separates_running_gone_and_undeterminable() {
assert_eq!(
pid_liveness(std::process::id()),
Some(true),
"this process is running"
);
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn a process that exits immediately");
let dead = child.id();
child.wait().expect("reap the child");
assert_eq!(
pid_liveness(dead),
Some(false),
"a reaped child's pid is gone"
);
assert_eq!(pid_liveness(0), None, "0 is a process group, not a process");
assert_eq!(
pid_liveness(u32::MAX),
None,
"a pid outside positive pid_t is not a process"
);
}
#[test]
fn classify_kill_probe_reads_eperm_as_running() {
assert_eq!(
classify_kill_probe(Err(nix::errno::Errno::EPERM)),
Some(true),
"a permission refusal is an answer about a process that exists"
);
}
#[test]
fn classify_kill_probe_reads_an_unrecognised_errno_as_undeterminable() {
assert_eq!(
classify_kill_probe(Err(nix::errno::Errno::EINVAL)),
None,
"an unrecognised errno must not be coerced into death"
);
}
#[test]
fn classify_kill_probe_agrees_with_the_live_probe_on_ok_and_esrch() {
assert_eq!(
classify_kill_probe(Ok(())),
pid_liveness(std::process::id()),
"the Ok arm must agree with a live probe of this process"
);
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn a process that exits immediately");
let dead = child.id();
child.wait().expect("reap the child");
assert_eq!(
classify_kill_probe(Err(nix::errno::Errno::ESRCH)),
pid_liveness(dead),
"the ESRCH arm must agree with a live probe of a reaped pid"
);
}
#[test]
fn scan_separates_a_harness_agent_lock_from_an_operator_lock() {
let fixture = GitWorktreeFixture::new();
let store = fixture.repo.join(".claude").join("worktrees");
let agent = fixture.add_worktree_at(&store, "agent-6561");
let session = fixture.add_worktree("operator-held");
fixture.harness_lock_worktree(&agent, "agent-6561");
fixture.lock_worktree(&session);
let scanned = scan_registered_worktrees(&fixture.repos_root, &[]);
let canonical = |p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.into());
let verdict = |p: &std::path::Path| {
scanned
.iter()
.find(|s| s.path == canonical(p))
.unwrap_or_else(|| panic!("{} must be scanned; got {scanned:?}", p.display()))
.admission
};
assert_eq!(verdict(&agent), Admission::HarnessAgentLock);
assert_eq!(verdict(&session), Admission::Locked);
}
#[test]
fn harness_lock_state_reports_a_harness_locked_tree_held() {
let fixture = GitWorktreeFixture::new();
let store = fixture.repo.join(".claude").join("worktrees");
let agent = fixture.add_worktree_at(&store, "agent-held");
fixture.harness_lock_worktree(&agent, "agent-held");
assert_eq!(harness_lock_state(&agent), HarnessLockState::Held);
}
#[test]
fn harness_lock_state_reports_an_unlocked_tree_released() {
let fixture = GitWorktreeFixture::new();
let store = fixture.repo.join(".claude").join("worktrees");
let agent = fixture.add_worktree_at(&store, "agent-released");
assert_eq!(harness_lock_state(&agent), HarnessLockState::Released);
}
#[test]
fn harness_lock_state_of_an_operator_locked_tree_is_undeterminable() {
let fixture = GitWorktreeFixture::new();
let store = fixture.repo.join(".claude").join("worktrees");
let agent = fixture.add_worktree_at(&store, "agent-operator-held");
fixture.lock_worktree(&agent);
assert_eq!(harness_lock_state(&agent), HarnessLockState::Undeterminable);
}
#[test]
fn harness_lock_state_of_a_non_worktree_is_undeterminable() {
let tmp = tempfile::tempdir().expect("tempdir");
assert_eq!(
harness_lock_state(tmp.path()),
HarnessLockState::Undeterminable,
"git cannot be asked here, and silence is never `Released`"
);
}
#[test]
fn scan_admits_a_worktree_under_an_adopted_checkout() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("adopted-one");
let canonical = std::fs::canonicalize(&wt).expect("canonical worktree");
let elsewhere = tempfile::tempdir().expect("tempdir");
let without = scan_registered_worktrees(elsewhere.path(), &[]);
assert!(
without.is_empty(),
"without an adopted anchor the walk sees nothing; got {without:?}"
);
let with = scan_registered_worktrees(elsewhere.path(), std::slice::from_ref(&fixture.repo));
let row = with
.iter()
.find(|s| s.path == canonical)
.unwrap_or_else(|| panic!("adopted worktree must be scanned; got {with:?}"));
assert_eq!(
row.admission,
Admission::Admitted,
"an adopted project bounds its own candidates"
);
}
#[test]
fn enumerate_admits_a_worktree_under_an_adopted_checkout() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("adopted-two");
let canonical = std::fs::canonicalize(&wt).expect("canonical worktree");
let elsewhere = tempfile::tempdir().expect("tempdir");
let found = enumerate_all(elsewhere.path(), std::slice::from_ref(&fixture.repo));
assert!(
found.contains(&canonical),
"an adopted checkout's worktree must be enumerable; got {found:?}"
);
}
#[test]
fn scan_ignores_an_adopted_checkout_inside_the_repos_root() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("walked-and-adopted");
let canonical = std::fs::canonicalize(&wt).expect("canonical worktree");
let rows = scan_registered_worktrees(&fixture.repos_root, std::slice::from_ref(&fixture.repo));
let matching: Vec<&ScannedWorktree> = rows.iter().filter(|s| s.path == canonical).collect();
assert_eq!(
matching.len(),
1,
"the walk owns a checkout under the repos root; got {matching:?}"
);
}
#[test]
fn scan_does_not_duplicate_a_repeated_adopted_checkout() {
let fixture = GitWorktreeFixture::new();
let wt = fixture.add_worktree("adopted-twice");
let canonical = std::fs::canonicalize(&wt).expect("canonical worktree");
let elsewhere = tempfile::tempdir().expect("tempdir");
let rows = scan_registered_worktrees(
elsewhere.path(),
&[fixture.repo.clone(), fixture.repo.clone()],
);
assert_eq!(
rows.iter().filter(|s| s.path == canonical).count(),
1,
"a repeated anchor must not restate a path; got {rows:?}"
);
}