use std::path::{Path, PathBuf};
use std::process::Command;
use trusty_common::search_index::IndexRegistration;
use super::{WorktreeIndexOutcome, index_new_worktree, index_new_worktree_in_background};
fn git_ok(dir: &Path, args: &[&str]) -> bool {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn init_repo(base: &Path) -> Option<()> {
std::fs::create_dir_all(base).ok()?;
if !git_ok(base, &["init", "--initial-branch=main"]) {
return None;
}
let _ = git_ok(base, &["config", "user.email", "ci@test.invalid"]);
let _ = git_ok(base, &["config", "user.name", "CI"]);
std::fs::write(base.join("README.md"), "# fixture\n").ok()?;
if !git_ok(base, &["add", "."]) {
return None;
}
if !git_ok(base, &["commit", "-m", "seed"]) {
return None;
}
Some(())
}
struct Fixture {
_dir: tempfile::TempDir,
base: PathBuf,
worktree: PathBuf,
}
fn fixture(worktree_name: &str) -> Option<Fixture> {
let dir = crate::test_support::hermetic_temp_dir();
let base = dir.path().join("base-repo");
init_repo(&base)?;
let worktree = dir.path().join(worktree_name);
if !git_ok(
&base,
&[
"worktree",
"add",
"-b",
"feature",
worktree.to_str().expect("utf8 path"),
],
) {
return None;
}
Some(Fixture {
_dir: dir,
base,
worktree,
})
}
#[test]
fn skips_missing_path() {
let dir = crate::test_support::hermetic_temp_dir();
let missing = dir.path().join("no-such-worktree");
assert_eq!(
index_new_worktree(&missing),
WorktreeIndexOutcome::PathMissing
);
}
#[test]
fn skips_path_that_is_not_a_worktree() {
let dir = crate::test_support::hermetic_temp_dir();
let base = dir.path().join("plain-clone");
let Some(()) = init_repo(&base) else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
base.join(".git").is_dir(),
"fixture precondition: a primary clone's .git is a directory"
);
assert_eq!(
index_new_worktree(&base),
WorktreeIndexOutcome::NotAWorktree,
"a primary clone must not be auto-indexed by worktree creation"
);
}
#[test]
fn skips_plain_directory() {
let dir = crate::test_support::hermetic_temp_dir();
let plain = dir.path().join("not-a-repo");
std::fs::create_dir_all(&plain).expect("create dir");
assert_eq!(
index_new_worktree(&plain),
WorktreeIndexOutcome::NotAWorktree
);
}
#[test]
fn registers_index_for_real_worktree() {
let Some(fx) = fixture("feat-index-on-create") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
fx.worktree.join(".git").is_file(),
"fixture precondition: a worktree's .git is a file"
);
assert_eq!(
index_new_worktree(&fx.worktree),
WorktreeIndexOutcome::RegistrationUnconfirmed {
index_id: "feat-index-on-create".to_string(),
reason: IndexRegistration::SkippedUnderTest,
},
"index id must be the worktree's own basename, not {:?}",
fx.base.file_name()
);
}
#[test]
fn reports_unconfirmed_rather_than_registered_when_the_daemon_never_answered() {
let Some(fx) = fixture("feat-unconfirmed") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let outcome = index_new_worktree(&fx.worktree);
assert!(
!matches!(outcome, WorktreeIndexOutcome::Registered(_)),
"nothing confirmed this index — `Registered` would be a success we \
never observed, got {outcome:?}"
);
let WorktreeIndexOutcome::RegistrationUnconfirmed { index_id, reason } = outcome else {
panic!("expected RegistrationUnconfirmed, got {outcome:?}");
};
assert_eq!(index_id, "feat-unconfirmed", "the id is still reported");
assert_eq!(reason, IndexRegistration::SkippedUnderTest);
}
#[test]
fn is_idempotent_for_same_worktree() {
let Some(fx) = fixture("feat-idempotent") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let first = index_new_worktree(&fx.worktree);
let second = index_new_worktree(&fx.worktree);
assert_eq!(first, second);
assert_eq!(
first,
WorktreeIndexOutcome::RegistrationUnconfirmed {
index_id: "feat-idempotent".to_string(),
reason: IndexRegistration::SkippedUnderTest,
}
);
}
#[test]
fn spawn_detached_returns_before_work_finishes() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let done = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&done);
let started = std::time::Instant::now();
super::spawn_detached(move || {
std::thread::sleep(std::time::Duration::from_secs(3));
flag.store(true, Ordering::SeqCst);
});
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(500),
"spawn must not wait on its work; returned after {elapsed:?}"
);
assert!(
!done.load(Ordering::SeqCst),
"work must still be running — a joined spawn would have completed it"
);
}
#[test]
fn background_entry_point_routes_through_spawn_detached() {
let Some(fx) = fixture("feat-nonblocking") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let started = std::time::Instant::now();
index_new_worktree_in_background(fx.worktree.clone());
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(500),
"worktree creation must never wait on indexing; returned after {elapsed:?}"
);
}
#[test]
fn is_git_worktree_matches_git_on_disk_shapes() {
let Some(fx) = fixture("feat-shapes") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
super::is_git_worktree(&fx.worktree),
"worktree: .git is a file"
);
assert!(
!super::is_git_worktree(&fx.base),
"clone: .git is a directory"
);
}