use crate::conf;
use crate::edge::Edge;
use crate::git;
use crate::layout::{CloneDir, Xdg};
use crate::substrate;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn edge(tmp: &TempDir) -> 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,
exe_dir: None,
path_dirs: Vec::new(),
color: false,
log_level: None,
}
}
fn founded(e: &Edge) -> CloneDir {
let clone = e.xdg.clone_dir(&e.invocation_path);
substrate::found(&clone.landing(), &clone.store(), &e.xdg, None).unwrap();
clone
}
fn conf(e: &Edge, argv: &[&str]) -> std::io::Result<()> {
conf::run(e, &argv.iter().map(ToString::to_string).collect::<Vec<_>>())
}
fn commits(landing: &Path) -> usize {
git::run(landing, &["rev-list", "--count", "HEAD"], None).unwrap().trim().parse().unwrap()
}
fn subject(landing: &Path) -> String {
git::run(landing, &["log", "-1", "--format=%s"], None).unwrap().trim().to_string()
}
fn landing_text(clone: &CloneDir, name: &str) -> String {
fs::read_to_string(clone.landing().join("config").join(name)).unwrap()
}
#[test]
fn set_task_branch_seals_one_commit_and_converges_on_a_repeat() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("tasks_branch = \"balls/x\""));
assert_eq!(commits(&clone.landing()), before + 1);
assert_eq!(subject(&clone.landing()), "balls: conf set task-branch balls/x");
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
assert_eq!(commits(&clone.landing()), before + 1);
}
#[test]
fn set_task_branch_to_the_landing_is_refused_and_writes_nothing() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
let err = conf(&e, &["set", "task-branch", "balls/config"]).unwrap_err().to_string();
assert!(err.contains("names the landing"), "{err}");
assert_eq!(commits(&clone.landing()), before);
}
#[test]
fn a_conf_seal_carries_the_checkout_scoped_trailers() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
let msg = git::run(&clone.landing(), &["log", "-1", "--format=%B"], None).unwrap();
let md = crate::message::parse(&msg).unwrap();
assert_eq!(md["bl-protocol"], ["1"], "{msg}");
assert_eq!(md["bl-op"], ["conf"], "{msg}");
assert_eq!(md["bl-actor"], ["tester"], "{msg}");
assert!(!md.contains_key("bl-id"), "{msg}");
}
#[test]
fn set_log_level_validates_against_the_ladder() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
assert!(conf(&e, &["set", "log-level", "noisy"]).is_err()); conf(&e, &["set", "log-level", "debug"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("log_level = \"debug\""));
}
#[test]
fn set_task_remote_writes_the_per_clone_binding_preserving_other_keys() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-remote", "git@hub:r"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("remote = \"git@hub:r\""), "{body}");
fs::write(clone.binding(), "tasks_branch = \"balls/x\"\nremote = \"old\"\n").unwrap();
conf(&e, &["set", "task-remote", "git@hub:new"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("remote = \"git@hub:new\"") && body.contains("tasks_branch = \"balls/x\""), "{body}");
assert!(!e.xdg.user_config().exists(), "the global XDG config must not be written");
}
#[test]
fn hook_compose_applies_the_directive_and_converges() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["append", "close.pre", "x"]).unwrap();
assert!(landing_text(&clone, "plugins.toml").contains("\"close.pre\" = [\"x\"]"));
let sealed = commits(&clone.landing());
conf(&e, &["append", "close.pre", "x"]).unwrap();
conf(&e, &["prepend", "close.pre", "x"]).unwrap();
assert_eq!(commits(&clone.landing()), sealed);
conf(&e, &["prepend", "close.pre", "y"]).unwrap();
assert!(landing_text(&clone, "plugins.toml").contains("\"close.pre\" = [\"y\", \"x\"]"));
conf(&e, &["remove", "close.pre", "ghost"]).unwrap();
conf(&e, &["remove", "close.pre", "y"]).unwrap();
conf(&e, &["remove", "close.pre", "x"]).unwrap();
assert!(!landing_text(&clone, "plugins.toml").contains("close.pre"));
}
#[test]
fn set_on_a_hooks_key_bare_replaces_the_whole_list() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "close.pre", "a", "b"]).unwrap();
assert!(landing_text(&clone, "plugins.toml").contains("\"close.pre\" = [\"a\", \"b\"]"));
conf(&e, &["set", "close.pre"]).unwrap();
assert!(!landing_text(&clone, "plugins.toml").contains("close.pre"));
}
#[test]
fn an_empty_plugin_name_is_refused_and_writes_nothing() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
for argv in [
vec!["set", "close.pre", ""],
vec!["set", "close.pre", "a", ""],
vec!["append", "close.pre", ""],
vec!["prepend", "close.pre", ""],
vec!["remove", "close.pre", ""],
] {
let err = conf(&e, &argv).unwrap_err().to_string();
assert!(err.contains("non-empty"), "{argv:?}: {err}");
}
assert!(!landing_text(&clone, "plugins.toml").contains("close.pre"));
assert_eq!(commits(&clone.landing()), before);
}
#[test]
fn foreign_tables_round_trip_untouched() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
fs::write(
clone.landing().join("config").join("plugins.toml"),
"[team]\nkeep = \"this\"\n\n[hooks]\n\"close.pre\" = [\"a\"]\n",
)
.unwrap();
conf(&e, &["append", "close.pre", "b"]).unwrap();
let body = landing_text(&clone, "plugins.toml");
assert!(body.contains("keep = \"this\""), "{body}");
assert!(body.contains("\"close.pre\" = [\"a\", \"b\"]"), "{body}");
}
#[test]
fn a_wired_retired_verb_hook_is_readable_and_removable_but_uncreatable() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
fs::write(
clone.landing().join("config").join("plugins.toml"),
"[hooks]\n\"drop.post\" = [\"bl-delivery\"]\n",
)
.unwrap();
conf(&e, &["drop.post"]).unwrap(); conf(&e, &["remove", "drop.post", "bl-delivery"]).unwrap(); assert!(!landing_text(&clone, "plugins.toml").contains("drop.post"));
let err = conf(&e, &["set", "drop.post", "bl-tracker"]).unwrap_err().to_string();
assert!(err.contains("unknown key"), "{err}");
}
#[test]
fn a_non_table_hooks_root_is_refused() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
fs::write(clone.landing().join("config").join("plugins.toml"), "hooks = \"nope\"\n").unwrap();
let err = conf(&e, &["append", "close.pre", "x"]).unwrap_err().to_string();
assert!(err.contains("[hooks] is not a table"), "{err}");
}
#[test]
fn the_write_grammar_is_enforced() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
founded(&e);
for (argv, expect) in [
(vec!["set"], "needs <key>"),
(vec!["set", "bogus", "v"], "unknown key"),
(vec!["set", "task-branch", "a", "b"], "exactly one value"),
(vec!["set", "task-remote"], "exactly one value"),
(vec!["append", "task-branch", "x"], "is a scalar"),
(vec!["remove", "log-level", "x"], "is a scalar"),
(vec!["append", "close.pre"], "exactly one value"),
(vec!["append", "close.pre", "a", "b"], "exactly one value"),
] {
let err = conf(&e, &argv).unwrap_err().to_string();
assert!(err.contains(expect), "{argv:?}: {err}");
}
}
#[test]
fn set_task_remote_sentinel_is_a_landing_policy_write_a_url_clears_it() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-remote", "none"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("task_remote = \"none\""));
assert_eq!(subject(&clone.landing()), "balls: conf set task-remote none");
conf(&e, &["set", "task-remote", "git@hub:r"]).unwrap();
assert!(!landing_text(&clone, "balls.toml").contains("task_remote"));
assert_eq!(subject(&clone.landing()), "balls: conf set task-remote git@hub:r");
let binding = fs::read_to_string(clone.binding()).unwrap();
assert!(binding.contains("remote = \"git@hub:r\""), "{binding}");
}