use super::*;
use crate::hooks::Hooks;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn xdg_at(root: &Path) -> Xdg {
Xdg::with(root, Some(&root.join("cfg").to_string_lossy()), Some(&root.join("state").to_string_lossy()))
}
fn exe_dir_with(root: &Path, names: &[&str]) -> std::path::PathBuf {
let dir = root.join("bin");
fs::create_dir_all(&dir).unwrap();
for name in names {
fs::write(dir.join(name), "#!/bin/sh\n").unwrap();
}
dir
}
fn bin_link(landing: &Path, name: &str) -> std::path::PathBuf {
landing.join("config/plugins/bin").join(name)
}
#[test]
fn seed_uses_the_embedded_default_without_materializing_the_xdg_override() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let landing = tmp.path().join("landing");
let exe = exe_dir_with(tmp.path(), &["bl-tracker", "bl-delivery"]);
seed_landing(&xdg, &landing, Some(&exe)).unwrap();
assert!(!xdg.default_config().exists());
assert!(fs::read_to_string(landing.join("config/balls.toml")).unwrap().contains("balls/tasks"));
let hooks = Hooks::load(&landing).unwrap();
assert_eq!(hooks.names("close", "post"), ["bl-delivery", "bl-tracker"]);
assert!(bin_link(&landing, "bl-tracker").symlink_metadata().is_ok());
assert!(bin_link(&landing, "bl-delivery").symlink_metadata().is_ok());
assert!(hooks.names("claim", "pre").is_empty());
assert!(hooks.names("unclaim", "pre").is_empty());
assert_eq!(hooks.resolve_read(&crate::registry::Registry::at(&landing), "show").len(), 1);
}
#[test]
fn seed_prunes_a_plugin_whose_binary_is_absent_here() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let landing = tmp.path().join("landing");
let exe = exe_dir_with(tmp.path(), &["bl-tracker"]);
seed_landing(&xdg, &landing, Some(&exe)).unwrap();
let hooks = Hooks::load(&landing).unwrap();
assert_eq!(hooks.names("close", "post"), ["bl-tracker"]); assert!(hooks.names("close", "pre").is_empty()); assert!(bin_link(&landing, "bl-tracker").symlink_metadata().is_ok());
assert!(bin_link(&landing, "bl-delivery").symlink_metadata().is_err());
}
#[test]
fn seed_with_no_exe_dir_prunes_every_plugin() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let landing = tmp.path().join("landing");
seed_landing(&xdg, &landing, None).unwrap();
let hooks = Hooks::load(&landing).unwrap();
assert!(hooks.names("prime", "pre").is_empty());
assert!(!landing.join("config/plugins/bin").exists());
assert!(landing.join("config/balls.toml").is_file());
}
#[test]
fn an_xdg_override_wins_over_the_embedded_default() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let dc = xdg.default_config();
fs::create_dir_all(&dc).unwrap();
fs::write(dc.join("balls.toml"), "tasks_branch = \"team/tasks\"\n").unwrap();
fs::write(dc.join("plugins.toml"), "[hooks]\n\"close.post\" = [\"tracker\"]\n").unwrap();
let landing = tmp.path().join("landing");
let exe = exe_dir_with(tmp.path(), &["tracker", "bl-delivery"]);
seed_landing(&xdg, &landing, Some(&exe)).unwrap();
assert!(fs::read_to_string(landing.join("config/balls.toml")).unwrap().contains("team/tasks"));
let hooks = Hooks::load(&landing).unwrap();
assert_eq!(hooks.names("close", "post"), ["tracker"]); assert!(hooks.names("prime", "pre").is_empty()); }
#[test]
fn a_pruned_hinted_plugin_is_noted_and_its_source_survives_the_rewrite() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let dc = xdg.default_config();
fs::create_dir_all(&dc).unwrap();
fs::write(
dc.join("plugins.toml"),
"[hooks]\n\"close.pre\" = [\"ghost\", \"mute\", \"tracker\"]\n[source]\nghost = \"cargo install ghost\"\n",
)
.unwrap();
let landing = tmp.path().join("landing");
let exe = exe_dir_with(tmp.path(), &["tracker"]);
seed_landing(&xdg, &landing, Some(&exe)).unwrap();
let seeded = Hooks::load(&landing).unwrap();
assert_eq!(seeded.names("close", "pre"), ["tracker"], "ghost and mute pruned");
assert_eq!(seeded.source("ghost").as_deref(), Some("cargo install ghost"), "the hint survived the prune");
}
#[test]
fn an_override_missing_a_file_falls_back_to_the_embedded_default() {
let tmp = TempDir::new().unwrap();
let xdg = xdg_at(tmp.path());
let dc = xdg.default_config();
fs::create_dir_all(&dc).unwrap();
fs::write(dc.join("plugins.toml"), "[hooks]\n\"create.post\" = [\"tracker\"]\n").unwrap();
let landing = tmp.path().join("landing");
let exe = exe_dir_with(tmp.path(), &["tracker"]);
seed_landing(&xdg, &landing, Some(&exe)).unwrap();
assert!(fs::read_to_string(landing.join("config/balls.toml")).unwrap().contains("balls/tasks"));
assert_eq!(Hooks::load(&landing).unwrap().names("create", "post"), ["tracker"]);
}
#[test]
fn rebind_reestablishes_present_bindings_without_touching_the_schedule() {
let tmp = TempDir::new().unwrap();
let landing = tmp.path().join("landing");
let config = landing.join("config");
fs::create_dir_all(&config).unwrap();
let schedule = "[hooks]\n\"close.post\" = [\"bl-delivery\", \"tracker\"]\n";
fs::write(config.join("plugins.toml"), schedule).unwrap();
let exe = exe_dir_with(tmp.path(), &["tracker"]);
rebind(&landing, Some(&exe)).unwrap();
assert!(bin_link(&landing, "tracker").symlink_metadata().is_ok());
assert!(bin_link(&landing, "bl-delivery").symlink_metadata().is_err());
assert_eq!(fs::read_to_string(config.join("plugins.toml")).unwrap(), schedule);
}