use super::*;
fn temp_repo() -> Option<(tempfile::TempDir, PathBuf)> {
let dir = tempfile::Builder::new()
.prefix("tm-test-repairguard-")
.tempdir()
.ok()?;
let path = dir.path().to_path_buf();
let ok = std::process::Command::new("git")
.args(["init", "-q", "-b", "main"])
.arg(&path)
.status()
.ok()?;
if !ok.success() {
return None;
}
Some((dir, path))
}
#[test]
fn installs_into_a_fresh_repo_then_is_idempotent() {
let Some((_dir, repo)) = temp_repo() else {
return;
};
let repo_str = repo.to_string_lossy().to_string();
repair_push_guard(Some(repo_str.clone()), false).expect("a fresh retrofit must succeed");
let hooks = effective_hooks_dir(&repo).expect("hooks dir");
assert!(hooks.join("pre-push").exists(), "the hook must be on disk");
repair_push_guard(Some(repo_str), false)
.expect("re-running an up-to-date guard must still succeed (idempotent)");
assert!(matches!(
inspect_pre_push_guard(&repo),
GuardState::Current(_)
));
}
#[test]
fn dry_run_writes_nothing() {
let Some((_dir, repo)) = temp_repo() else {
return;
};
repair_push_guard(Some(repo.to_string_lossy().to_string()), true).expect("dry run");
let hooks = effective_hooks_dir(&repo).expect("hooks dir");
assert!(
!hooks.join("pre-push").exists(),
"a dry run must never create the hook"
);
}
#[test]
fn refusal_exits_nonzero_and_leaves_the_foreign_hook_intact() {
let Some((_dir, repo)) = temp_repo() else {
return;
};
let hooks = effective_hooks_dir(&repo).expect("hooks dir");
std::fs::create_dir_all(&hooks).expect("mkdir hooks");
let foreign = "#!/bin/sh\n# husky\nexit 0\n";
std::fs::write(hooks.join("pre-push"), foreign).expect("seed foreign hook");
assert!(
repair_push_guard(Some(repo.to_string_lossy().to_string()), false).is_err(),
"a refusal means UNPROTECTED and must not read as success to a script"
);
assert_eq!(
std::fs::read_to_string(hooks.join("pre-push")).expect("read"),
foreign,
"the foreign hook must be untouched"
);
}
#[test]
fn unresolvable_path_exits_nonzero() {
let dir = tempfile::Builder::new()
.prefix("tm-test-repairguard-nogit-")
.tempdir()
.expect("temp dir");
assert!(
repair_push_guard(Some(dir.path().to_string_lossy().to_string()), false).is_err(),
"a directory outside any git working tree must fail loudly"
);
assert!(repair_push_guard(Some("/nonexistent/tm/test/path".to_string()), false).is_err());
}
#[test]
fn retrofits_a_bare_repository() {
let dir = tempfile::Builder::new()
.prefix("tm-test-repairguard-bare-")
.tempdir()
.expect("temp dir");
let bare = dir.path().join("base.git");
let ok = std::process::Command::new("git")
.args(["init", "--bare", "-q", "-b", "main"])
.arg(&bare)
.status();
match ok {
Ok(s) if s.success() => {}
_ => return,
}
repair_push_guard(Some(bare.to_string_lossy().to_string()), false)
.expect("a bare repository must retrofit, not fail on --show-toplevel");
assert!(
bare.join("hooks").join("pre-push").exists(),
"the guard must land in the bare repo's own hooks dir"
);
assert!(matches!(
inspect_pre_push_guard(&bare),
GuardState::Current(_)
));
repair_push_guard(Some(bare.to_string_lossy().to_string()), true).expect("bare dry run");
}
#[test]
fn worktree_count_counts_linked_worktrees() {
let Some((_dir, repo)) = temp_repo() else {
return;
};
let git = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(&repo)
.args(args)
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.output()
.expect("git")
.status
.success()
};
std::fs::write(repo.join("README"), b"seed").expect("write");
assert!(git(&["add", "."]));
assert!(git(&["commit", "-qm", "seed"]));
assert_eq!(worktree_count(&repo), "1");
let linked = repo.join("linked");
assert!(git(&[
"worktree",
"add",
"-q",
"-b",
"side",
linked.to_str().expect("utf8")
]));
assert_eq!(worktree_count(&repo), "2");
repair_push_guard(Some(linked.to_string_lossy().to_string()), false)
.expect("retrofit from a linked worktree");
assert!(matches!(
inspect_pre_push_guard(&repo),
GuardState::Current(_)
));
}