#![cfg(unix)]
use super::*;
use crate::delivery_repo::tests::{project, tip};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
fn xdg(tmp: &TempDir) -> Xdg {
let state = tmp.path().join("state");
Xdg::with(tmp.path(), None, Some(state.to_str().unwrap()))
}
fn work(wt: &Path, name: &str, content: &str) {
fs::write(wt.join(name), content).unwrap();
Project::run(wt, &["add", "-A"]).unwrap();
Project::run(wt, &["commit", "-q", "-m", &format!("add {name}")]).unwrap();
}
#[test]
fn an_attempt_forks_a_private_worktree_at_the_target_tip_and_delivers_by_the_one_law() {
let (tmp, root, p) = project();
let before = Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string();
let target = p.target(None).unwrap(); let a = Attempt::open(&root, &xdg(&tmp), &target).unwrap();
assert!(a.worktree().join("seed.txt").exists());
assert!(a.worktree().starts_with(tmp.path().join("state/balls/attempts")));
assert!(a.handle().starts_with("at-"), "opaque handle, unmistakable for a ball id: {}", a.handle());
assert_eq!(a.base(), before, "the attempt starts from the exact target commit");
work(a.worktree(), "candidate.txt", "one\n");
assert_ne!(a.tip().unwrap(), before, "base..tip IS the project diff, no ref name spelled");
let d = a.deliver("Try the thing", Some("why this way")).unwrap();
let landed = Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string();
assert_eq!(d.target, "main");
assert_eq!(d.base, before);
assert_eq!(d.source, Some(a.tip().unwrap()));
assert_eq!(d.commit, Some(landed));
assert_eq!(tip(&root), format!("Try the thing [{}]", a.handle()));
let body = Project::run(&root, &["log", "-1", "--format=%b", "main"]).unwrap();
assert!(body.contains("why this way") && body.contains("add candidate.txt"), "{body}");
assert_eq!(Project::run(&root, &["show", "main:candidate.txt"]).unwrap(), "one\n");
}
#[test]
fn a_landing_sibling_makes_every_other_attempt_stale_until_its_owner_incorporates() {
let (tmp, root, p) = project();
let dirs = xdg(&tmp);
let target = p.target(None).unwrap();
let first = Attempt::open(&root, &dirs, &target).unwrap();
let second = Attempt::open(&root, &dirs, &target).unwrap();
assert_ne!(first.handle(), second.handle(), "two attempts never name one ref");
assert_ne!(first.worktree(), second.worktree(), "and never share a worktree or index");
assert_eq!(first.base(), second.base(), "same cohort: same (target, base)");
work(first.worktree(), "a.txt", "a\n");
work(second.worktree(), "b.txt", "b\n");
first.deliver("A", None).unwrap();
let err = second.deliver("B", None).unwrap_err().to_string();
assert!(err.contains("stale source"), "{err}");
assert_eq!(tip(&root), format!("A [{}]", first.handle()), "B moved nothing");
Project::run(second.worktree(), &["merge", "--no-edit", "-q", "main"]).unwrap();
let landed = second.deliver("B", None).unwrap();
assert!(landed.commit.is_some());
assert_eq!(tip(&root), format!("B [{}]", second.handle()));
assert_eq!(Project::run(&root, &["show", "main:a.txt"]).unwrap(), "a\n");
}
#[test]
fn resume_remakes_a_lost_worktree_and_converges_on_a_standing_delivery() {
let (tmp, root, p) = project();
let x = xdg(&tmp);
let target = p.target(None).unwrap();
let handle = {
let a = Attempt::open(&root, &x, &target).unwrap();
work(a.worktree(), "c.txt", "c\n");
a.deliver("Landed", None).unwrap();
fs::remove_dir_all(a.worktree()).unwrap();
a.handle().to_string()
};
let landed = Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string();
let again = Attempt::resume(&root, &x, &target, &handle).unwrap();
assert!(again.worktree().join("c.txt").exists(), "the stale registration healed");
assert_eq!(again.base(), Project::run(&root, &["merge-base", "main", &attempt_branch(&handle)])
.unwrap()
.trim(), "a resumed base is the true fork point, not a re-pin");
let d = again.deliver("Landed", None).unwrap();
assert_eq!(d.commit, Some(landed.clone()));
assert_eq!(Project::run(&root, &["rev-parse", "main"]).unwrap().trim(), landed);
}
#[test]
fn a_failing_gate_aborts_before_the_seal_and_leaves_the_target_unmoved() {
let (tmp, root, p) = project();
let target = p.target(None).unwrap();
let a = Attempt::open(&root, &xdg(&tmp), &target).unwrap();
work(a.worktree(), "d.txt", "d\n");
let hook = root.join(".git/hooks/pre-commit");
fs::write(&hook, "#!/bin/sh\nexit 1\n").unwrap();
fs::set_permissions(&hook, fs::Permissions::from_mode(0o755)).unwrap();
let err = a.deliver("Rejected by the gate", None).unwrap_err().to_string();
assert!(err.contains("delivery gate"), "{err}");
assert_eq!(tip(&root), "seed", "nothing landed");
assert!(a.worktree().join("d.txt").exists(), "the source is intact for the fix");
}
#[test]
fn release_keeps_a_rejected_source_addressable_and_discard_removes_it() {
let (tmp, root, p) = project();
let x = xdg(&tmp);
let target = p.target(None).unwrap();
let a = Attempt::open(&root, &x, &target).unwrap();
work(a.worktree(), "loser.txt", "loser\n");
let tip_of_a = a.tip().unwrap();
let handle = a.handle().to_string();
a.release().unwrap();
assert!(!a.worktree().exists());
assert_eq!(tip(&root), "seed");
assert_eq!(a.tip().unwrap(), tip_of_a, "the source ref still resolves");
assert_eq!(
Project::run(&root, &["show", &format!("{tip_of_a}:loser.txt")]).unwrap(),
"loser\n",
"and its bytes are still readable"
);
let back = Attempt::resume(&root, &x, &target, &handle).unwrap();
assert!(back.worktree().join("loser.txt").exists());
back.discard().unwrap();
assert!(!back.worktree().exists());
let err = Attempt::resume(&root, &x, &target, &handle).unwrap_err().to_string();
assert!(err.contains("unknown attempt handle"), "{err}");
}
#[test]
fn a_bare_project_repo_attempts_and_delivers() {
let (tmp, root, _) = project();
let bare = tmp.path().join("bare.git");
Project::run(tmp.path(), &["clone", "--bare", "-q", root.to_str().unwrap(), bare.to_str().unwrap()]).unwrap();
Project::run(&bare, &["config", "user.name", "test"]).unwrap();
Project::run(&bare, &["config", "user.email", "test@example.com"]).unwrap();
let p = Project::at(&bare);
let target = p.target(None).unwrap();
let a = Attempt::open(&bare, &xdg(&tmp), &target).unwrap();
work(a.worktree(), "bare.txt", "bare\n");
let d = a.deliver("From a bare repo", None).unwrap();
assert_eq!(d.target, "main");
assert!(d.commit.is_some());
assert_eq!(Project::run(&bare, &["show", "main:bare.txt"]).unwrap(), "bare\n");
}
#[test]
fn a_target_names_the_ball_graph_an_explicit_branch_or_a_parent_attempt() {
let (tmp, root, p) = project();
let x = xdg(&tmp);
let ball = p.target(Some("bl-epic")).unwrap();
assert!(p.branch_exists("work/bl-epic").unwrap());
Project::run(&root, &["branch", "release"]).unwrap();
p.target_ref("release").unwrap();
let err = p.target_ref("gone").unwrap_err().to_string();
assert!(err.contains("no such delivery target: gone"), "{err}");
let parent = Attempt::open(&root, &x, &ball).unwrap();
let child = Attempt::open(&root, &x, &parent.target()).unwrap();
work(child.worktree(), "child.txt", "child\n");
let d = child.deliver("Child work", None).unwrap();
assert_eq!(d.target, attempt_branch(parent.handle()));
assert_eq!(tip(&root), "seed", "the integration branch is untouched at this depth");
assert_eq!(
Project::run(&root, &["show", &format!("{}:child.txt", attempt_branch(parent.handle()))]).unwrap(),
"child\n"
);
}
#[test]
fn the_ball_path_returns_the_same_identities_the_attempt_path_does() {
let (tmp, root, p) = project();
let before = Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string();
let wt = tmp.path().join("wt");
p.materialize(&wt, "work/bl-x").unwrap();
work(&wt, "ball.txt", "ball\n");
let d = p.deliver(&wt, "work/bl-x", "main", "Ball work [bl-x]", "[bl-x]").unwrap();
assert_eq!(d.target, "main");
assert_eq!(d.base, before);
assert_eq!(d.source, Some(Project::run(&root, &["rev-parse", "work/bl-x"]).unwrap().trim().to_string()));
assert_eq!(d.commit, Some(Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string()));
}
#[test]
fn the_no_op_arms_still_report_the_base_and_target_they_acted_on() {
let (tmp, root, p) = project();
let before = Project::run(&root, &["rev-parse", "main"]).unwrap().trim().to_string();
let d = p.deliver(&tmp.path().join("absent"), "work/bl-none", "main", "Nothing [bl-none]", "[bl-none]").unwrap();
assert_eq!((d.target.as_str(), d.base.as_str(), d.source, d.commit), ("main", before.as_str(), None, None));
let a = Attempt::open(&root, &xdg(&tmp), &p.target(None).unwrap()).unwrap();
let d = a.deliver("Empty", None).unwrap();
assert_eq!(d.base, before);
assert_eq!(d.source, Some(a.tip().unwrap()));
assert_eq!(d.commit, None, "nothing landed, and the identities still read");
assert_eq!(tip(&root), "seed");
}
#[test]
fn the_capability_refuses_a_non_repo_root_and_an_unsafe_invocation_path() {
let tmp = TempDir::new().unwrap();
let err = Project::at(tmp.path()).target(None).unwrap_err().to_string();
assert!(err.contains("not a git repository"), "balls' own voice, not git's: {err}");
let err = Project::at(tmp.path()).target_ref("main").unwrap_err().to_string();
assert!(err.contains("not a git repository"), "{err}");
let (tmp, root, p) = project();
let target = p.target(None).unwrap();
let sneaky = root.parent().unwrap().join("proj/../proj");
let err = Attempt::open(&sneaky, &xdg(&tmp), &target).unwrap_err().to_string();
assert!(err.contains("unsafe invocation path"), "{err}");
}