use super::*;
use std::fs;
use tempfile::TempDir;
fn project() -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("proj");
fs::create_dir(&root).unwrap();
let g = |args: &[&str]| Project::run(&root, args).unwrap();
g(&["init", "-q", "-b", "main"]);
g(&["config", "user.name", "test"]);
g(&["config", "user.email", "test@example.com"]);
fs::write(root.join("seed.txt"), "seed\n").unwrap();
g(&["add", "-A"]);
g(&["commit", "-q", "-m", "seed"]);
(tmp, root)
}
fn env_at(state: &Path, cwd: &Path) -> Env {
Env {
plugin: Some("bl-delivery".into()),
xdg: Xdg::with(Path::new("/nonexistent-home"), None, state.to_str()),
cwd: cwd.to_path_buf(),
}
}
fn drive(args: &[&str], input: &str, env: &Env) -> (i32, String) {
let args: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
let mut out = Vec::new();
let code = run(&args, &mut input.as_bytes(), &mut out, env);
(code, String::from_utf8(out).unwrap())
}
fn wire(invocation: &Path, rest: &str) -> String {
format!(r#"{{"binding":{{"invocation_path":"{}"}}{rest}}}"#, invocation.display())
}
#[test]
fn protocol_answers_on_out_and_needs_no_env() {
let tmp = TempDir::new().unwrap();
let env = Env {
plugin: None, xdg: Xdg::with(Path::new("/nonexistent-home"), None, None),
cwd: tmp.path().to_path_buf(),
};
let (code, out) = drive(&["protocol"], "", &env);
assert_eq!(code, 0);
assert_eq!(out, format!("{}\n", delivery::PROTOCOL_JSON));
}
#[test]
fn the_version_arm_answers_on_out_and_needs_no_env_either() {
let tmp = TempDir::new().unwrap();
let env = Env {
plugin: None,
xdg: Xdg::with(Path::new("/nonexistent-home"), None, None),
cwd: tmp.path().to_path_buf(),
};
for spelling in ["--version", "-V"] {
let (code, out) = drive(&[spelling], "", &env);
assert_eq!(code, 0);
assert_eq!(out, format!("{}\n", version::plugin_line("bl-delivery")));
}
}
#[test]
fn missing_op_or_phase_is_a_usage_error() {
let tmp = TempDir::new().unwrap();
let env = env_at(tmp.path(), tmp.path());
assert_eq!(drive(&[], "", &env).0, 1);
assert_eq!(drive(&["claim"], "", &env).0, 1);
}
#[test]
fn a_malformed_wire_is_an_error() {
let tmp = TempDir::new().unwrap();
let env = env_at(tmp.path(), tmp.path());
assert_eq!(drive(&["claim", "post"], "not json", &env).0, 1);
}
#[test]
fn an_unsafe_invocation_path_refuses() {
let tmp = TempDir::new().unwrap();
let env = env_at(tmp.path(), tmp.path());
let payload = r#"{"binding":{"invocation_path":"relative/path"}}"#;
assert_eq!(drive(&["claim", "post"], payload, &env).0, 1);
}
#[test]
fn an_unset_plugin_name_is_an_error() {
let tmp = TempDir::new().unwrap();
let env = Env {
plugin: None,
xdg: Xdg::with(Path::new("/nonexistent-home"), None, None),
cwd: tmp.path().to_path_buf(),
};
let payload = wire(tmp.path(), "");
assert_eq!(drive(&["claim", "post"], &payload, &env).0, 1);
}
#[test]
fn a_hook_with_no_ball_on_the_wire_is_an_error() {
let (_tmp, root) = project();
let state = TempDir::new().unwrap();
let env = env_at(state.path(), state.path());
let payload = wire(&root, "");
assert_eq!(drive(&["claim", "post"], &payload, &env).0, 1);
}
#[test]
fn a_hook_on_a_non_repo_fails_the_precondition_gate() {
let tmp = TempDir::new().unwrap(); let env = env_at(tmp.path(), tmp.path());
let payload = wire(tmp.path(), r#","command":{"id":"bl-t1"}"#);
assert_eq!(drive(&["claim", "post"], &payload, &env).0, 1);
}
#[test]
fn claim_post_materializes_the_worktree_and_surfaces_its_path() {
let (_tmp, root) = project();
let state = TempDir::new().unwrap();
let env = env_at(state.path(), state.path());
let payload = wire(&root, r#","command":{"id":"bl-t1"},"current_state":{"title":"T"}"#);
let (code, out) = drive(&["claim", "post"], &payload, &env);
assert_eq!(code, 0);
let surfaced = out.trim();
assert!(surfaced.ends_with("bl-t1"), "surfaced path names the ball: {surfaced}");
assert!(Path::new(surfaced).join("seed.txt").exists(), "worktree materialized at the surfaced path");
}
#[test]
fn prime_rollback_declines_before_touching_anything() {
let tmp = TempDir::new().unwrap();
let env = env_at(tmp.path(), tmp.path());
let payload = wire(tmp.path(), r#","rolling_back":"claim""#);
assert_eq!(drive(&["prime", "post"], &payload, &env).0, 0);
}
#[test]
fn prime_warns_and_continues_on_a_non_repo_invocation_path() {
let tmp = TempDir::new().unwrap(); let env = env_at(tmp.path(), tmp.path());
let payload = wire(tmp.path(), "");
assert_eq!(drive(&["prime", "post"], &payload, &env).0, 0);
}
#[test]
fn prime_pre_on_a_repo_is_a_quiet_no_op() {
let (_tmp, root) = project();
let state = TempDir::new().unwrap();
let env = env_at(state.path(), state.path());
let payload = wire(&root, "");
assert_eq!(drive(&["prime", "pre"], &payload, &env).0, 0);
}
#[test]
fn prime_post_prunes_and_reports_debris() {
let (_tmp, root) = project();
let g = |args: &[&str]| Project::run(&root, args).unwrap();
g(&["branch", "work/bl-dbg", "main"]);
g(&["checkout", "-q", "work/bl-dbg"]);
fs::write(root.join("extra.txt"), "x\n").unwrap();
g(&["add", "-A"]);
g(&["commit", "-q", "-m", "undelivered"]);
g(&["checkout", "-q", "main"]);
let state = TempDir::new().unwrap();
let env = env_at(state.path(), state.path());
let payload = wire(&root, "");
assert_eq!(drive(&["prime", "post"], &payload, &env).0, 0);
}