use super::*;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
fn at(secs: i64) -> DateTime<Utc> {
DateTime::from_timestamp(1_700_000_000 + secs, 0).expect("valid timestamp")
}
fn entry(path: &Path, project: &str) -> AdoptedWorktree {
AdoptedWorktree {
path: path.to_path_buf(),
project: project.to_string(),
checkout: path.parent().unwrap_or(path).to_path_buf(),
branch: None,
adopted_at: at(0),
}
}
#[test]
fn adoption_store_path_is_beside_the_project_registry() {
assert_eq!(ADOPTED_WORKTREES_FILE, "worktrees.json");
}
#[test]
fn load_of_a_missing_file_is_empty() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(AdoptionStore::load(dir.path()).entries().is_empty());
}
#[test]
fn load_of_a_corrupt_file_is_empty() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join(ADOPTED_WORKTREES_FILE), b"{ not json").expect("seed");
assert!(
AdoptionStore::load(dir.path()).entries().is_empty(),
"a corrupt store reads as empty"
);
}
#[test]
fn update_refuses_to_clobber_a_corrupt_store() {
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join(ADOPTED_WORKTREES_FILE);
std::fs::write(&file, b"{ not json").expect("seed");
let outcome = AdoptionStore::update(dir.path(), |store| {
store.record(entry(Path::new("/tmp/wt-a"), "alpha"))
});
assert!(
outcome.is_err(),
"an unparsable store must not be published over"
);
assert_eq!(
std::fs::read(&file).expect("read back"),
b"{ not json",
"the corrupt document is left exactly as it was"
);
}
#[test]
fn record_writes_one_entry_per_worktree() {
let dir = tempfile::tempdir().expect("tempdir");
AdoptionStore::update(dir.path(), |store| {
assert_eq!(
store.record(entry(Path::new("/tmp/wt-a"), "alpha")),
RecordOutcome::Recorded
);
assert_eq!(
store.record(entry(Path::new("/tmp/wt-b"), "alpha")),
RecordOutcome::Recorded
);
})
.expect("publish");
let reloaded = AdoptionStore::load(dir.path());
assert_eq!(reloaded.entries().len(), 2);
assert!(reloaded.entries().iter().all(|e| e.project == "alpha"));
}
#[test]
fn record_is_idempotent_for_the_same_project() {
let mut store = AdoptionStore::default();
store.record(entry(Path::new("/tmp/wt-a"), "alpha"));
let mut second = entry(Path::new("/tmp/wt-a"), "alpha");
second.adopted_at = at(600);
assert_eq!(store.record(second), RecordOutcome::AlreadyRecorded);
assert_eq!(store.entries().len(), 1);
assert_eq!(
store.entries()[0].adopted_at,
at(0),
"the record keeps naming when the path was FIRST seen"
);
}
#[test]
fn record_never_reattributes_another_projects_worktree() {
let mut store = AdoptionStore::default();
store.record(entry(Path::new("/tmp/wt-a"), "alpha"));
assert_eq!(
store.record(entry(Path::new("/tmp/wt-a"), "beta")),
RecordOutcome::ClaimedByAnother("alpha".to_string())
);
assert_eq!(store.entries().len(), 1);
assert_eq!(store.entries()[0].project, "alpha");
}
#[test]
fn anchors_are_the_distinct_checkouts() {
let dir = tempfile::tempdir().expect("tempdir");
AdoptionStore::update(dir.path(), |store| {
for name in ["wt-a", "wt-b"] {
store.record(AdoptedWorktree {
path: PathBuf::from("/tmp/repo/.worktrees").join(name),
project: "alpha".to_string(),
checkout: PathBuf::from("/tmp/repo"),
branch: None,
adopted_at: at(0),
});
}
})
.expect("publish");
assert_eq!(
adopted_anchors(dir.path()),
vec![PathBuf::from("/tmp/repo")]
);
}
#[test]
fn owning_project_reports_the_name_that_already_claimed_the_checkout_7588() {
let fx = GitWorktreeFixture::new();
fx.add_worktree("pre-one");
let store_dir = tempfile::tempdir().expect("tempdir");
backfill_checkout(store_dir.path(), "gnomish", &fx.repo, at(0));
assert_eq!(
project_owning_checkout(store_dir.path(), &fx.repo),
Some("gnomish".to_string())
);
}
#[test]
fn owning_project_is_none_for_an_unrecorded_checkout_7588() {
let fx = GitWorktreeFixture::new();
let store_dir = tempfile::tempdir().expect("tempdir");
assert_eq!(project_owning_checkout(store_dir.path(), &fx.repo), None);
}
#[test]
fn backfill_records_every_pre_existing_worktree() {
let fx = GitWorktreeFixture::new();
let a = fx.add_worktree("alpha-one");
let b = fx.add_worktree("alpha-two");
let store_dir = tempfile::tempdir().expect("tempdir");
let report = backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(0));
assert_eq!(report.recorded, 2, "both pre-existing worktrees adopted");
assert_eq!(report.claimed_by_another, 0);
let store = AdoptionStore::load(store_dir.path());
let recorded: Vec<&PathBuf> = store.entries().iter().map(|e| &e.path).collect();
for wt in [&a, &b] {
let canonical = std::fs::canonicalize(wt).expect("canonical worktree");
assert!(
recorded.contains(&&canonical),
"{} must have a record; got {recorded:?}",
canonical.display()
);
}
assert!(store.entries().iter().all(|e| e.project == "alpha"));
}
#[test]
fn backfill_is_idempotent() {
let fx = GitWorktreeFixture::new();
fx.add_worktree("alpha-one");
fx.add_worktree("alpha-two");
let store_dir = tempfile::tempdir().expect("tempdir");
let first = backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(0));
let second = backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(600));
assert_eq!(first.recorded, 2);
assert_eq!(second.recorded, 0, "a re-register writes nothing new");
assert_eq!(second.already_recorded, 2);
assert_eq!(AdoptionStore::load(store_dir.path()).entries().len(), 2);
}
#[test]
fn backfill_leaves_another_projects_worktree_alone() {
let fx = GitWorktreeFixture::new();
fx.add_worktree("shared-one");
let store_dir = tempfile::tempdir().expect("tempdir");
backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(0));
let report = backfill_checkout(store_dir.path(), "beta", &fx.repo, at(600));
assert_eq!(report.recorded, 0);
assert_eq!(report.claimed_by_another, 1);
let store = AdoptionStore::load(store_dir.path());
assert_eq!(store.entries().len(), 1);
assert_eq!(store.entries()[0].project, "alpha");
}
#[test]
fn backfill_skips_a_worktree_with_no_gitdir_pointer_and_still_succeeds() {
let fx = GitWorktreeFixture::new();
fx.add_worktree("alpha-one");
let broken = fx.repo.join(".claude").join("worktrees").join("broken");
std::fs::create_dir_all(&broken).expect("broken worktree dir");
let store_dir = tempfile::tempdir().expect("tempdir");
let report = backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(0));
assert_eq!(report.skipped, 1, "the unreadable directory is skipped");
assert_eq!(report.recorded, 1, "the readable worktree is still adopted");
let store = AdoptionStore::load(store_dir.path());
assert!(
!store.entries().iter().any(|e| e.path.ends_with("broken")),
"a directory with no gitdir pointer must never be adopted"
);
}
#[test]
fn backfill_ignores_a_path_that_is_not_a_repository_root() {
let plain = tempfile::tempdir().expect("tempdir");
let store_dir = tempfile::tempdir().expect("tempdir");
let report = backfill_checkout(store_dir.path(), "alpha", plain.path(), at(0));
assert_eq!(report.recorded, 0);
assert_eq!(report.skipped, 1);
assert_eq!(
report.unrecorded,
vec![UnrecordedWorktree {
path: plain.path().to_path_buf(),
reason: SkipReason::NotARepositoryRoot,
}]
);
assert!(!store_dir.path().join(ADOPTED_WORKTREES_FILE).exists());
}
#[test]
fn backfill_names_the_skipped_worktree_and_why() {
let fx = GitWorktreeFixture::new();
fx.add_worktree("alpha-one");
let broken = fx.repo.join(".claude").join("worktrees").join("broken");
std::fs::create_dir_all(&broken).expect("broken worktree dir");
let store_dir = tempfile::tempdir().expect("tempdir");
let report = backfill_checkout(store_dir.path(), "alpha", &fx.repo, at(0));
let named: Vec<&UnrecordedWorktree> = report
.unrecorded
.iter()
.filter(|u| u.reason == SkipReason::NoGitdirPointer)
.collect();
assert_eq!(named.len(), 1, "got {:?}", report.unrecorded);
assert!(
named[0].path.ends_with("broken"),
"the skipped path must be named: {:?}",
named[0].path
);
let second = backfill_checkout(store_dir.path(), "beta", &fx.repo, at(600));
assert!(
second.unrecorded.iter().any(|u| u.reason
== SkipReason::ClaimedByAnother {
project: "alpha".to_string()
}),
"a foreign claim must name the project holding it: {:?}",
second.unrecorded
);
}
#[test]
fn concurrent_backfills_never_lose_a_projects_records() {
let alpha = GitWorktreeFixture::new();
alpha.add_worktree("alpha-one");
let beta = GitWorktreeFixture::new();
beta.add_worktree("beta-one");
for round in 0..10 {
let store_dir = tempfile::tempdir().expect("tempdir");
let store_path = store_dir.path().to_path_buf();
let gate = std::sync::Arc::new(std::sync::Barrier::new(2));
let handles: Vec<std::thread::JoinHandle<()>> = [("alpha", &alpha), ("beta", &beta)]
.into_iter()
.map(|(name, fx)| {
let store_path = store_path.clone();
let checkout = fx.repo.clone();
let gate = std::sync::Arc::clone(&gate);
std::thread::spawn(move || {
gate.wait();
backfill_checkout(&store_path, name, &checkout, at(0));
})
})
.collect();
for h in handles {
h.join().expect("backfill thread");
}
let store = AdoptionStore::load(store_dir.path());
let mut projects: Vec<&str> = store.entries().iter().map(|e| e.project.as_str()).collect();
projects.sort_unstable();
assert_eq!(
projects,
vec!["alpha", "beta"],
"round {round}: both projects' records must survive the race"
);
}
}