use super::*;
use crate::edge::Edge;
use crate::layout::Xdg;
use std::path::PathBuf;
use tempfile::TempDir;
fn edge(tmp: &TempDir, exe_dir: Option<PathBuf>) -> Edge {
Edge {
xdg: Xdg::with(tmp.path(), None, Some(&tmp.path().join("state").to_string_lossy())),
invocation_path: tmp.path().join("proj"),
default_actor: "tester".into(),
depth: 0,
held: Vec::new(),
exe_dir,
path_dirs: Vec::new(),
color: false,
log_level: None,
balls_clock: None,
}
}
fn landing(e: &Edge) -> PathBuf {
e.xdg.clone_dir(&e.invocation_path).landing()
}
fn store(e: &Edge) -> PathBuf {
e.xdg.clone_dir(&e.invocation_path).store()
}
fn argv(a: &[&str]) -> Vec<String> {
a.iter().map(ToString::to_string).collect()
}
fn op_log(e: &Edge) -> String {
let path = e.xdg.clone_dir(&e.invocation_path).op_log();
std::fs::read_to_string(path).unwrap_or_default()
}
#[test]
fn prime_founds_both_branches_on_a_miss_then_converges_on_the_hit_path() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &argv(&["--as", "me"])).unwrap();
assert!(landing(&e).join("config").join("balls.toml").is_file());
assert!(store(&e).join("tasks").is_dir());
prime(&e, &[]).unwrap();
}
#[test]
fn prime_founds_over_a_crashed_foundings_config_dir_instead_of_bricking() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let l = landing(&e);
std::fs::create_dir_all(l.join("config")).unwrap();
crate::git::run(&l, &["init", "-q", "-b", crate::LANDING_BRANCH], None).unwrap();
assert!(!crate::substrate::is_landing(&l), "a config/ dir with no commit is not a founded landing");
prime(&e, &argv(&["--as", "me"])).unwrap();
assert!(crate::substrate::is_landing(&l));
assert!(l.join("config").join("plugins.toml").is_file());
assert!(store(&e).join("tasks").is_dir()); prime(&e, &[]).unwrap(); }
#[test]
fn prime_drives_a_sync_after_the_prime_chain() {
let tmp = TempDir::new().unwrap();
let e = Edge { log_level: Some("debug".into()), ..edge(&tmp, None) };
prime(&e, &argv(&["--as", "me"])).unwrap();
let log = op_log(&e);
assert!(log.contains("\"op\":\"prime\""), "prime chain ran: {log}");
assert!(log.contains("\"op\":\"sync\""), "prime drove a sync: {log}");
}
#[test]
fn a_founding_primes_seed_prune_note_lands_in_the_op_log() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let seed = e.xdg.default_config();
std::fs::create_dir_all(&seed).unwrap();
std::fs::write(
seed.join("plugins.toml"),
"[hooks]\n\"close.pre\" = [\"ghost\", \"mute\"]\n[source]\nghost = \"cargo install ghost\"\n",
)
.unwrap();
prime(&e, &argv(&["--as", "me"])).unwrap();
let log = op_log(&e);
assert!(
log.contains("seed: pruned ghost (no binary beside bl) — source: cargo install ghost — re-add with bl conf after acquiring"),
"{log}"
);
assert!(log.contains("\"lvl\":\"info\""), "the note is info-level: {log}");
assert!(!log.contains("pruned mute"), "hintless prune stays silent: {log}");
}
#[test]
fn a_seed_naming_the_landing_as_tasks_branch_fails_prime_named_and_conf_set_recovers() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let seed = e.xdg.default_config();
std::fs::create_dir_all(&seed).unwrap();
std::fs::write(seed.join("balls.toml"), "tasks_branch = \"balls/config\"\n").unwrap();
let err = prime(&e, &[]).unwrap_err().to_string();
assert!(err.contains("names the landing"), "{err}");
assert!(landing(&e).join("config").is_dir(), "the landing founded before the refusal");
crate::conf::run(&e, &argv(&["set", "task-branch", "balls/tasks"])).unwrap();
prime(&e, &[]).unwrap();
assert!(store(&e).join("tasks").is_dir());
}
#[test]
fn sync_before_prime_is_an_error() {
let tmp = TempDir::new().unwrap();
assert!(sync(&edge(&tmp, None), &[]).is_err());
}
#[test]
fn sync_targets_the_store_and_special_cases_no_branch_name() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &[]).unwrap();
sync(&e, &[]).unwrap(); sync(&e, &argv(&["work/bl-1234", "--as", "me"])).unwrap(); sync(&e, &argv(&[crate::LANDING_BRANCH])).unwrap(); let (l, s) = (landing(&e), store(&e));
let (b, _) = bind(&e, &l, &s, None, Some(crate::LANDING_BRANCH.into())).unwrap();
assert_eq!(b.tasks_branch, crate::LANDING_BRANCH); }
#[test]
fn a_named_sync_branch_overrides_the_config_tasks_branch_in_the_binding() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &[]).unwrap();
let (l, s) = (landing(&e), store(&e));
let (default_b, _) = bind(&e, &l, &s, None, None).unwrap();
let (named_b, _) = bind(&e, &l, &s, None, Some("federation/shared".into())).unwrap();
assert_eq!(named_b.tasks_branch, "federation/shared");
assert_ne!(default_b.tasks_branch, named_b.tasks_branch);
}
#[test]
fn prime_rejects_unknown_flags_and_a_missing_value() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
assert!(prime(&e, &argv(&["--bogus"])).is_err());
assert!(prime(&e, &argv(&["--as"])).is_err()); assert!(prime(&e, &argv(&["--remote"])).is_err()); assert!(prime(&e, &argv(&["--center"])).is_err());
assert!(prime(&e, &argv(&["--install"])).is_err()); }
#[test]
fn prime_accepts_the_per_op_remote_override() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &argv(&["--remote", "git@hub:r"])).unwrap();
}
#[test]
fn prime_center_writes_the_durable_binding_before_adopt() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let err = prime(&e, &argv(&["--center", "git@hub:c", "--as", "me"])).unwrap_err();
assert!(err.to_string().contains("install.pre"), "adopt needs a fetch plugin: {err}");
let (l, s) = (landing(&e), store(&e));
let (b, _) = bind(&e, &l, &s, None, None).unwrap();
assert_eq!(b.remote.as_deref(), Some("git@hub:c"));
}
#[test]
fn prime_rejects_center_with_install() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let err = prime(&e, &argv(&["--center", "git@hub:c", "--install", "git@hub:c"])).unwrap_err();
assert!(err.to_string().contains("--center already adopts"), "{err}");
}
#[test]
fn prime_rejects_stealth_combined_with_any_remote_naming_flag() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
for contradictory in [
["--stealth", "--remote", "git@hub:r"],
["--stealth", "--center", "git@hub:c"],
["--install", "git@hub:c", "--stealth"],
] {
let err = prime(&e, &argv(&contradictory)).unwrap_err().to_string();
assert!(err.contains("--stealth contradicts"), "{err}");
}
}
#[test]
fn a_stealth_prime_writes_the_landing_sentinel_that_binds_every_later_op() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &argv(&["--as", "me"])).unwrap();
let user_config = e.xdg.user_config();
std::fs::create_dir_all(user_config.parent().unwrap()).unwrap();
std::fs::write(&user_config, "remote = \"git@hub:r\"\n").unwrap();
let (l, s) = (landing(&e), store(&e));
let (tracked, _) = bind(&e, &l, &s, None, None).unwrap();
assert_eq!(tracked.remote.as_deref(), Some("git@hub:r")); assert!(!tracked.stealth);
prime(&e, &argv(&["--stealth", "--as", "me"])).unwrap(); let (stealth, _) = bind(&e, &l, &s, None, None).unwrap(); assert_eq!(stealth.remote, None); assert!(stealth.stealth);
let (overridden, _) = bind(&e, &l, &s, Some("git@hub:x".into()), None).unwrap();
assert_eq!(overridden.remote.as_deref(), Some("git@hub:x"));
assert!(!overridden.stealth);
}
#[test]
fn sync_rejects_unknown_flags_and_a_second_branch() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &[]).unwrap();
assert!(sync(&e, &argv(&["--bogus"])).is_err());
assert!(sync(&e, &argv(&["-x"])).is_err()); assert!(sync(&e, &argv(&["a", "b"])).is_err());
}
#[test]
fn sync_accepts_the_per_op_remote_override() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
prime(&e, &[]).unwrap();
sync(&e, &argv(&["--remote", "git@hub:r"])).unwrap();
assert!(sync(&e, &argv(&["--remote"])).is_err()); let err = sync(&e, &argv(&["--center", "git@hub:c"])).unwrap_err();
assert!(err.to_string().contains("unexpected flag '--center'"), "{err}");
}