use std::path::{Path, PathBuf};
use std::process::Command;
struct Sandbox(PathBuf);
impl Sandbox {
fn new(name: &str) -> Self {
let d = std::env::temp_dir().join(format!("gh-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).expect("mkdir");
Sandbox(d)
}
fn path(&self, rel: &str) -> PathBuf {
rel.split('/').fold(self.0.clone(), |p, c| p.join(c))
}
fn run(&self, cwd: &Path, args: &[&str]) -> (i32, String) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.args(args)
.current_dir(cwd)
.env("HOME", &self.0)
.env("USERPROFILE", &self.0)
.env("XDG_CONFIG_HOME", self.path(".config"))
.output()
.expect("run amont");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn assert_nothing_was_installed(&self, repo: &Path, context: &str) {
for n in ["amont", "amont.exe"] {
assert!(
!self.path(".local/bin").join(n).exists(),
"{context}: the INSTALLER RAN — a binary appeared in ~/.local/bin"
);
}
let tpl = self.path(".config/git/git-templates/templates/hooks");
for name in DISPATCHERS {
assert!(
!tpl.join(name).exists(),
"{context}: a template shim was written"
);
assert!(
!repo.join(".git/hooks").join(name).exists(),
"{context}: a repo shim was baked"
);
}
assert!(
!repo.join("AGENTS.md").exists(),
"{context}: agents-md ran and wrote AGENTS.md"
);
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
const DISPATCHERS: [&str; 4] = ["commit-msg", "pre-commit", "pre-push", "prepare-commit-msg"];
const VERBS: [&str; 7] = [
"list",
"install",
"uninstall",
"run",
"trust",
"restore",
"agents-md",
];
fn git(dir: &Path, args: &[&str]) {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("git");
}
fn init_repo(dir: &Path) {
std::fs::create_dir_all(dir).expect("mkdir");
git(dir, &["init", "-q", "--template=", "."]);
git(dir, &["config", "user.email", "t@t.test"]);
git(dir, &["config", "user.name", "t"]);
}
#[test]
fn a_remote_named_like_a_subcommand_still_runs_the_hook() {
for verb in VERBS {
let s = Sandbox::new(&format!("remote-{}", verb.replace('-', "")));
let repo = s.path("repo");
init_repo(&repo);
let hooks = repo.join(".git/hooks");
std::fs::create_dir_all(&hooks).expect("mkdir");
let (_, out) = s.run(
&repo,
&[
"--hooks-dir",
hooks.to_str().expect("utf8"),
"pre-push",
verb,
"https://example.test/r.git",
],
);
s.assert_nothing_was_installed(&repo, &format!("remote named {verb:?}, output:\n{out}"));
}
}
#[test]
fn a_hook_argument_that_is_a_file_named_install_reaches_the_hook_verbatim() {
let s = Sandbox::new("literal-install");
let repo = s.path("repo");
init_repo(&repo);
let hooks = repo.join(".git/hooks");
std::fs::create_dir_all(&hooks).expect("mkdir");
let msg = repo.join("install");
std::fs::write(
&msg,
"not a conventional subject at all, and far too long to pass\n",
)
.expect("write");
let (code, out) = s.run(
&repo,
&[
"--hooks-dir",
hooks.to_str().expect("utf8"),
"commit-msg",
msg.to_str().expect("utf8"),
],
);
s.assert_nothing_was_installed(&repo, &format!("output:\n{out}"));
assert_eq!(
code, 1,
"the commit-msg check did not read the file it was handed:\n{out}"
);
assert!(
std::fs::read_to_string(&msg)
.expect("read")
.starts_with("not a conventional"),
"the hook argument was treated as something other than a path"
);
}
#[test]
fn a_subcommand_argument_spelled_like_a_verb_is_just_a_value() {
let s = Sandbox::new("verb-value");
let repo = s.path("repo");
init_repo(&repo);
let (code, out) = s.run(&repo, &["agents-md", "--path", "install"]);
assert_eq!(code, 0, "{out}");
assert!(
repo.join("install").is_file(),
"--path was not honoured:\n{out}"
);
for n in ["amont", "amont.exe"] {
assert!(
!s.path(".local/bin").join(n).exists(),
"THE INSTALLER RAN from an argument value:\n{out}"
);
}
}
#[test]
fn help_exits_zero_and_describes_the_program() {
let s = Sandbox::new("help");
let plain = s.path("plain");
std::fs::create_dir_all(&plain).expect("mkdir");
for flag in ["--help", "-h"] {
let (code, out) = s.run(&plain, &[flag]);
assert_eq!(code, 0, "{flag} exited {code}:\n{out}");
for verb in VERBS {
assert!(out.contains(verb), "{flag} never mentions {verb}:\n{out}");
}
assert!(out.contains("--hooks-dir"), "{out}");
assert!(out.contains("--stage"), "{out}");
}
}
#[test]
fn no_arguments_is_a_usage_error_that_still_explains_itself() {
let s = Sandbox::new("noargs");
let plain = s.path("plain");
std::fs::create_dir_all(&plain).expect("mkdir");
let (code, out) = s.run(&plain, &[]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("agents-md"), "{out}");
assert!(out.contains("--hooks-dir"), "{out}");
}