use std::fs;
use std::path::{Path, PathBuf};
use crate::delivery::Repo;
use crate::delivery_path::worktree_path;
use crate::delivery_repo::tests::{project, tip};
use crate::delivery_repo::Project;
use crate::layout::Xdg;
fn xdg_and_worktree(root: &Path, id: &str) -> (Xdg, &'static str, PathBuf) {
let xdg = Xdg::with(root, None, Some(&root.join("state").to_string_lossy()));
let plugin = "bl-delivery";
let wt = worktree_path(&xdg, plugin, &root.to_string_lossy(), id);
(xdg, plugin, wt)
}
fn store(tmp: &Path, ids: &[&str]) -> PathBuf {
let store = tmp.join("store");
fs::create_dir_all(store.join("tasks")).unwrap();
for id in ids {
fs::write(store.join("tasks").join(format!("{id}.md")), "+++\n+++\n").unwrap();
}
store
}
#[test]
fn prune_deletes_a_delivered_branch_after_teardown_and_reruns_clean() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-x");
let store = store(tmp.path(), &["bl-x"]);
p.materialize(&wt, "work/bl-x").unwrap();
fs::write(wt.join("feature.txt"), "shipped\n").unwrap();
p.deliver(&wt, "work/bl-x", "main", "Add feature [bl-x]", "[bl-x]").unwrap();
p.release(&wt).unwrap(); fs::write(root.join("other.txt"), "other\n").unwrap();
Project::run(&root, &["add", "-A"]).unwrap();
Project::run(&root, &["commit", "-qm", "concurrent work"]).unwrap();
assert!(p.prune(&xdg, plugin, &store).unwrap().is_empty()); assert!(!Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-x"]).unwrap());
assert_eq!(tip(&root), "concurrent work");
assert!(p.prune(&xdg, plugin, &store).unwrap().is_empty()); }
#[test]
fn prune_deletes_a_forkless_branch_the_unclaim_of_uncommitted_work_left() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-y");
let store = store(tmp.path(), &["bl-y"]);
p.materialize(&wt, "work/bl-y").unwrap();
p.release(&wt).unwrap();
assert!(p.prune(&xdg, plugin, &store).unwrap().is_empty());
assert!(!Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-y"]).unwrap());
}
#[test]
fn prune_preserves_a_branch_carrying_undelivered_commits_and_reports_its_absent_worktree() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-z");
let store = store(tmp.path(), &["bl-z"]);
p.materialize(&wt, "work/bl-z").unwrap();
fs::write(wt.join("kept.txt"), "undelivered\n").unwrap();
Project::run(&wt, &["add", "-A"]).unwrap();
Project::run(&wt, &["commit", "-qm", "wip"]).unwrap();
p.release(&wt).unwrap();
let reports = p.prune(&xdg, plugin, &store).unwrap();
assert_eq!(
reports,
vec![
"work/bl-z is committed but its worktree is gone — bl claim bl-z \
re-materializes onto it (a later close still delivers, bl-65e0), \
or discard with git branch -D work/bl-z"
]
);
assert!(Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-z"]).unwrap());
p.materialize(&wt, "work/bl-z").unwrap();
p.deliver(&wt, "work/bl-z", "main", "Land it [bl-z]", "[bl-z]").unwrap();
assert_eq!(tip(&root), "Land it [bl-z]");
}
#[test]
fn prune_stays_silent_on_an_undelivered_branch_whose_worktree_is_present() {
let (tmp, root, p) = project();
let (xdg, plugin, wt) = xdg_and_worktree(&root, "bl-live-wt");
let store = store(tmp.path(), &["bl-live-wt"]);
p.materialize(&wt, "work/bl-live-wt").unwrap();
fs::write(wt.join("kept.txt"), "undelivered\n").unwrap();
Project::run(&wt, &["add", "-A"]).unwrap();
Project::run(&wt, &["commit", "-qm", "wip"]).unwrap();
assert!(p.prune(&xdg, plugin, &store).unwrap().is_empty());
}
#[test]
fn prune_leaves_a_checked_out_branch_and_still_succeeds() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-live");
let store = store(tmp.path(), &["bl-live"]);
p.materialize(&wt, "work/bl-live").unwrap();
assert!(p.prune(&xdg, plugin, &store).unwrap().is_empty()); assert!(Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-live"]).unwrap());
assert!(wt.join("seed.txt").exists()); }
#[test]
fn prune_on_a_root_that_is_no_repo_is_a_quiet_no_op() {
let outside = tempfile::TempDir::new().unwrap();
let (xdg, plugin, _) = xdg_and_worktree(outside.path(), "bl-none");
let store = store(outside.path(), &[]);
assert!(Project::at(outside.path()).prune(&xdg, plugin, &store).unwrap().is_empty());
}
#[test]
fn prune_scopes_the_delivery_scan_to_the_fork_so_a_reused_id_survives() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-r");
let store = store(tmp.path(), &["bl-r"]);
p.materialize(&wt, "work/bl-r").unwrap();
fs::write(wt.join("a.txt"), "1\n").unwrap();
p.deliver(&wt, "work/bl-r", "main", "first [bl-r]", "[bl-r]").unwrap();
p.discard(&wt, "work/bl-r").unwrap();
p.materialize(&wt, "work/bl-r").unwrap();
fs::write(wt.join("b.txt"), "2\n").unwrap();
Project::run(&wt, &["add", "-A"]).unwrap();
Project::run(&wt, &["commit", "-qm", "second wip"]).unwrap();
p.release(&wt).unwrap();
let reports = p.prune(&xdg, plugin, &store).unwrap();
assert_eq!(reports.len(), 1); assert!(Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-r"]).unwrap());
}
#[test]
fn prune_reports_a_closed_balls_debris_as_discard_only_once_its_content_landed() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-c");
let store = store(tmp.path(), &[]); p.materialize(&wt, "work/bl-c").unwrap();
fs::write(wt.join("landed.txt"), "content\n").unwrap();
Project::run(&wt, &["add", "-A"]).unwrap();
Project::run(&wt, &["commit", "-qm", "wip"]).unwrap();
p.release(&wt).unwrap();
fs::write(root.join("landed.txt"), "content\n").unwrap();
Project::run(&root, &["add", "-A"]).unwrap();
Project::run(&root, &["commit", "-qm", "Land it [bl-sibling]"]).unwrap();
assert_eq!(
p.prune(&xdg, plugin, &store).unwrap(),
vec![
"work/bl-c is committed but its worktree is gone, and bl-c is closed \
(no task file — absence is the record), so nothing can re-claim or \
deliver it: its content is already contained in main, so discard it \
with git branch -D work/bl-c"
]
);
assert!(Project::ok(&root, &["rev-parse", "--verify", "--quiet", "refs/heads/work/bl-c"]).unwrap());
}
#[test]
fn prune_hands_a_closed_balls_uncontained_debris_the_diff_to_read_first() {
let (tmp, root, p) = project();
let wt = tmp.path().join("wt");
let (xdg, plugin, _) = xdg_and_worktree(&root, "bl-d");
let store = store(tmp.path(), &[]);
p.materialize(&wt, "work/bl-d").unwrap();
fs::write(wt.join("orphan.txt"), "only here\n").unwrap();
Project::run(&wt, &["add", "-A"]).unwrap();
Project::run(&wt, &["commit", "-qm", "wip"]).unwrap();
p.release(&wt).unwrap();
assert_eq!(
p.prune(&xdg, plugin, &store).unwrap(),
vec![
"work/bl-d is committed but its worktree is gone, and bl-d is closed \
(no task file — absence is the record), so nothing can re-claim or \
deliver it: its content is NOT contained in main — read it with \
git diff main...work/bl-d, then discard with git branch -D work/bl-d"
]
);
}