use super::base_lock::{LOCK_STALE_AFTER, lock_is_stale};
use super::*;
use tempfile::TempDir;
fn make_provisioner(root: &TempDir) -> WorkspaceProvisioner<FakeGitBackend> {
WorkspaceProvisioner::without_prepare(FakeGitBackend::new(), root.path().to_owned())
}
#[test]
fn repo_slug_extraction() {
assert_eq!(
repo_slug("https://github.com/owner/trusty-tools"),
"trusty-tools"
);
assert_eq!(
repo_slug("https://github.com/owner/trusty-tools.git"),
"trusty-tools"
);
assert_eq!(repo_slug("git@github.com:owner/my-repo.git"), "my-repo");
}
#[test]
fn provisioner_isolation_path() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let ws = prov
.provision(&id, "https://github.com/owner/trusty-tools", "main", "task")
.unwrap();
assert!(ws.path.starts_with(root.path()));
assert!(ws.path.to_string_lossy().contains("trusty-tools"));
assert!(ws.path.to_string_lossy().contains(&id.to_string()));
}
#[test]
fn provisioner_path_not_in_existing_project() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let ws = prov
.provision(&id, "https://github.com/owner/myrepo.git", "feat/x", "task")
.unwrap();
assert!(ws.path.starts_with(root.path()));
assert_ne!(&ws.path, root.path());
}
#[test]
fn provisioner_uses_session_id_subdir() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let ws = prov
.provision(&id, "https://github.com/owner/repo", "main", "task")
.unwrap();
let leaf = ws.path.file_name().unwrap().to_string_lossy();
assert_eq!(leaf.as_ref(), id.to_string());
}
#[test]
fn provision_in_uses_explicit_project_dir() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let project_dir = root.path().join("bobmatnyc").join("trusty-tools");
let ws = prov
.provision_in(
&project_dir,
&id,
"https://github.com/bobmatnyc/trusty-tools",
"main",
"task",
)
.unwrap();
assert_eq!(
ws.path,
project_dir
.join(".base")
.join(".worktrees")
.join(id.to_string())
);
assert!(ws.path.starts_with(&project_dir));
}
#[test]
fn provision_reuses_base_checkout_across_sessions() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let project_dir = root.path().join("owner").join("repo");
let id1 = ManagedSessionId::new();
let ws1 = prov
.provision_in(
&project_dir,
&id1,
"https://github.com/owner/repo",
"main",
"t1",
)
.unwrap();
let id2 = ManagedSessionId::new();
let ws2 = prov
.provision_in(
&project_dir,
&id2,
"https://github.com/owner/repo",
"main",
"t2",
)
.unwrap();
let base_dir = project_dir.join(".base");
assert!(ws1.path.starts_with(&base_dir));
assert!(ws2.path.starts_with(&base_dir));
assert_ne!(ws1.path, ws2.path);
assert!(base_dir.join("HEAD").is_file());
}
#[test]
fn provisioner_records_repo_url_and_branch() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let ws = prov
.provision(
&id,
"https://github.com/owner/repo",
"feat/my-branch",
"task",
)
.unwrap();
assert_eq!(ws.repo_url, "https://github.com/owner/repo");
assert_eq!(ws.branch, "feat/my-branch");
}
#[test]
fn blank_git_ref_omits_branch_flag() {
let root = crate::test_support::hermetic_temp_dir();
let fake = FakeGitBackend::new();
let prov = WorkspaceProvisioner::without_prepare(FakeGitBackend::new(), root.path().to_owned());
let id = ManagedSessionId::new();
let ws = prov
.provision(&id, "https://github.com/owner/repo", "", "task")
.unwrap();
assert!(ws.path.starts_with(root.path()), "workspace inside root");
assert_eq!(
ws.branch, "",
"blank ref must be stored as-is, not substituted"
);
drop(fake); }
#[test]
fn provision_writes_task_md() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let task = "Fix the authentication bug in the login flow";
let ws = prov
.provision(&id, "https://github.com/owner/repo", "main", task)
.unwrap();
let task_file = ws.path.join("TASK.md");
assert!(
task_file.exists(),
"TASK.md must be written when task is non-empty"
);
let content = std::fs::read_to_string(&task_file).unwrap();
assert_eq!(content, task, "TASK.md must contain the exact task text");
}
#[test]
fn provision_skips_task_md_when_empty() {
let root = crate::test_support::hermetic_temp_dir();
let prov = make_provisioner(&root);
let id = ManagedSessionId::new();
let ws = prov
.provision(&id, "https://github.com/owner/repo", "main", "")
.unwrap();
let task_file = ws.path.join("TASK.md");
assert!(
!task_file.exists(),
"TASK.md must NOT be created when task is empty"
);
}
fn make_local_bare_origin(scratch: &TempDir) -> Option<PathBuf> {
use std::process::Command;
let bare = scratch.path().join("origin.git");
let work = scratch.path().join("seed");
if !Command::new("git")
.args(["init", "--bare", "-b", "main"])
.arg(&bare)
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return None;
}
if !Command::new("git")
.args(["clone"])
.arg(&bare)
.arg(&work)
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return None;
}
std::fs::write(work.join("README.md"), "seed").unwrap();
let work_s = work.to_str().unwrap();
for args in [
vec!["-C", work_s, "add", "."],
vec![
"-C",
work_s,
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"-m",
"seed",
],
vec!["-C", work_s, "push", "origin", "main"],
] {
if !Command::new("git")
.args(&args)
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return None;
}
}
Some(bare)
}
#[test]
fn ensure_base_checkout_recovers_from_concurrent_race() {
let scratch = crate::test_support::hermetic_temp_dir();
let Some(bare_origin) = make_local_bare_origin(&scratch) else {
eprintln!("ensure_base_checkout_recovers_from_concurrent_race: git unavailable, skipping");
return;
};
let repo_url = format!("file://{}", bare_origin.display());
let root = crate::test_support::hermetic_temp_dir();
let base_dir = root.path().join("project").join(".base");
let handles: Vec<_> = (0..4)
.map(|_| {
let repo_url = repo_url.clone();
let base_dir = base_dir.clone();
std::thread::spawn(move || {
RealGitBackend::default().ensure_base_checkout(&repo_url, &base_dir)
})
})
.collect();
for handle in handles {
let result = handle.join().expect("thread must not panic");
assert!(
result.is_ok(),
"every racing ensure_base_checkout call must recover and return Ok, got {result:?}"
);
}
assert!(
is_established_bare_checkout(&base_dir),
"base_dir must be a genuinely established bare checkout after the race settles"
);
}
#[test]
fn ensure_base_checkout_rejects_stale_non_bare_directory() {
let scratch = crate::test_support::hermetic_temp_dir();
let Some(bare_origin) = make_local_bare_origin(&scratch) else {
eprintln!(
"ensure_base_checkout_rejects_stale_non_bare_directory: git unavailable, skipping"
);
return;
};
let repo_url = format!("file://{}", bare_origin.display());
let root = crate::test_support::hermetic_temp_dir();
let base_dir = root.path().join("project").join(".base");
std::fs::create_dir_all(&base_dir).unwrap();
std::fs::write(base_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
assert!(
base_dir.join("HEAD").is_file(),
"sanity check: the stale directory has a file literally named HEAD"
);
assert!(
!is_established_bare_checkout(&base_dir),
"sanity check: a lone HEAD file with no repo structure must NOT read as bare"
);
let result = RealGitBackend::default().ensure_base_checkout(&repo_url, &base_dir);
assert!(
result.is_err(),
"a stale non-repo directory must be rejected loudly, not silently reused: {result:?}"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains(&base_dir.display().to_string()),
"error must name the exact stale base path, got: {msg}"
);
assert_no_destructive_hint(&msg);
}
fn assert_no_destructive_hint(msg: &str) {
for forbidden in ["rm -rf", "rm -fr", "rm -r ", "remove_dir_all", "rmdir"] {
assert!(
!msg.contains(forbidden),
"recovery message must never suggest a recursive delete of the SHARED \
base checkout (#3605), but it contains {forbidden:?}: {msg}"
);
}
assert!(
msg.contains(" mv "),
"recovery message must offer the non-destructive quarantine command, got: {msg}"
);
assert!(
msg.contains("SHARED"),
"recovery message must warn that the base is shared across sessions, got: {msg}"
);
}
#[test]
fn stale_base_dir_error_suggests_quarantine_not_deletion() {
let root = crate::test_support::hermetic_temp_dir();
let base_dir = root.path().join("project").join(".base");
std::fs::create_dir_all(&base_dir).unwrap();
std::fs::write(base_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let err = super::base_lock::stale_base_dir_error(&base_dir)
.expect("a non-empty, non-bare base dir must produce an error");
let msg = err.to_string();
assert!(
msg.contains(&base_dir.display().to_string()),
"error must name the exact stale base path, got: {msg}"
);
assert_no_destructive_hint(&msg);
let quarantine = super::base_lock::stale_base_quarantine_path(&base_dir);
assert_ne!(
quarantine, base_dir,
"quarantine destination must differ from the source, or the `mv` is a no-op"
);
assert!(
quarantine
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.contains(".stale-")),
"quarantine destination must be a timestamped `.stale-` sibling, got: {}",
quarantine.display()
);
assert_eq!(
quarantine.parent(),
base_dir.parent(),
"quarantine must be a sibling so the `mv` is a same-filesystem rename"
);
let empty = root.path().join("project").join(".base-empty");
std::fs::create_dir_all(&empty).unwrap();
assert!(super::base_lock::stale_base_dir_error(&empty).is_none());
assert!(super::base_lock::stale_base_dir_error(&root.path().join("nope")).is_none());
}
#[test]
fn fake_ensure_base_checkout_rejects_stale_non_bare_directory() {
let root = crate::test_support::hermetic_temp_dir();
let base_dir = root.path().join("project").join(".base");
std::fs::create_dir_all(&base_dir).unwrap();
std::fs::write(base_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let fake = FakeGitBackend::new();
let result = fake.ensure_base_checkout("https://github.com/owner/repo", &base_dir);
assert!(
result.is_err(),
"fake must reject a stale non-bare directory loudly, not silently reuse it: {result:?}"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains(&base_dir.display().to_string()),
"fake error must be actionable (must name the path), got: {msg}"
);
assert_no_destructive_hint(&msg);
}
#[test]
fn fake_ensure_base_checkout_is_idempotent_on_valid_base() {
let root = crate::test_support::hermetic_temp_dir();
let base_dir = root.path().join("project").join(".base");
let fake = FakeGitBackend::new();
fake.ensure_base_checkout("https://github.com/owner/repo", &base_dir)
.expect("first ensure must establish the fake base");
assert!(
super::base_lock::fake_is_established_bare_checkout(&base_dir),
"a freshly-established fake base must read as a valid bare checkout"
);
fake.ensure_base_checkout("https://github.com/owner/repo", &base_dir)
.expect("second ensure must reuse the established fake base");
assert!(base_dir.join("HEAD").is_file());
assert!(base_dir.join("config").is_file());
}
#[test]
fn base_checkout_lock_recovers_stale_lock_marker() {
let root = crate::test_support::hermetic_temp_dir();
let lock_path = root.path().join(".base.lock");
std::fs::write(&lock_path, b"").unwrap();
let stale_time =
std::time::SystemTime::now() - (LOCK_STALE_AFTER + std::time::Duration::from_secs(10));
std::fs::File::options()
.write(true)
.open(&lock_path)
.unwrap()
.set_modified(stale_time)
.unwrap();
assert!(
lock_is_stale(&lock_path),
"a lock marker older than LOCK_STALE_AFTER must be considered stale"
);
let start = std::time::Instant::now();
let guard = acquire_base_checkout_lock(&lock_path).expect("must recover from a stale lock");
assert!(
start.elapsed() < std::time::Duration::from_secs(5),
"stale-lock recovery must not wait out the full acquire timeout"
);
drop(guard);
assert!(
!lock_path.exists(),
"dropping the lock guard must remove the marker file"
);
}
#[test]
fn default_identity_produces_plain_git_command() {
let backend = RealGitBackend::default();
let cmd = backend.command();
assert_eq!(
cmd.get_args().count(),
0,
"no -c args for an empty identity"
);
assert_eq!(
cmd.get_envs().count(),
0,
"no env overrides for an empty identity"
);
}
#[test]
fn git_identity_env_applied_to_command() {
let identity = crate::core::git_identity::GitIdentity {
env: vec![("GH_CONFIG_DIR".to_string(), "/cfg/project".to_string())],
commit_name: None,
commit_email: None,
};
let backend = RealGitBackend::new(identity);
let cmd = backend.command();
let envs: Vec<_> = cmd.get_envs().collect();
assert!(
envs.iter().any(|(k, v)| {
*k == std::ffi::OsStr::new("GH_CONFIG_DIR")
&& *v == Some(std::ffi::OsStr::new("/cfg/project"))
}),
"GH_CONFIG_DIR override must be applied: {envs:?}"
);
}
#[test]
fn git_identity_commit_args_applied_to_command() {
let identity = crate::core::git_identity::GitIdentity {
env: vec![],
commit_name: Some("Bot".to_string()),
commit_email: Some("bot@example.com".to_string()),
};
let backend = RealGitBackend::new(identity);
let cmd = backend.command();
let args: Vec<&std::ffi::OsStr> = cmd.get_args().collect();
assert_eq!(
args,
vec![
std::ffi::OsStr::new("-c"),
std::ffi::OsStr::new("user.name=Bot"),
std::ffi::OsStr::new("-c"),
std::ffi::OsStr::new("user.email=bot@example.com"),
]
);
}