use super::*;
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
const SAMPLE: &str = r#"
[hooks]
"close.pre" = ["bl-delivery"]
"close.post" = ["bl-delivery", "tracker"]
"create.post" = ["tracker"]
"show" = ["bl-delivery"]
"#;
#[test]
fn parse_reads_each_op_phase_list_in_order() {
let hooks = Hooks::parse(SAMPLE).unwrap();
assert_eq!(hooks.names("close", "post"), ["bl-delivery", "tracker"]);
assert_eq!(hooks.names("create", "post"), ["tracker"]);
assert!(hooks.names("update", "post").is_empty());
}
#[test]
fn parse_tolerates_a_missing_hooks_table_and_non_string_entries() {
assert!(Hooks::parse("tasks_branch = \"x\"\n").unwrap().names("close", "pre").is_empty());
let hooks = Hooks::parse("[hooks]\n\"close.pre\" = [\"tracker\", 7]\n").unwrap();
assert_eq!(hooks.names("close", "pre"), ["tracker"]);
}
#[test]
fn parse_rejects_malformed_toml() {
assert!(Hooks::parse("[hooks").is_err());
}
#[test]
fn load_from_absent_file_is_an_empty_schedule() {
let tmp = TempDir::new().unwrap();
let hooks = Hooks::load_from(&tmp.path().join("nope.toml")).unwrap();
assert_eq!(hooks, Hooks::default());
}
#[test]
fn load_from_propagates_a_non_not_found_read_error() {
let tmp = TempDir::new().unwrap();
assert!(Hooks::load_from(tmp.path()).is_err());
}
#[test]
fn load_reads_the_landing_plugins_toml() {
let tmp = TempDir::new().unwrap();
let config = tmp.path().join("config");
fs::create_dir_all(&config).unwrap();
fs::write(config.join("plugins.toml"), SAMPLE).unwrap();
assert_eq!(Hooks::load(tmp.path()).unwrap().names("create", "post"), ["tracker"]);
}
fn layered(landing_body: &str, xdg: Option<&str>) -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let config = tmp.path().join("config");
fs::create_dir_all(&config).unwrap();
fs::write(config.join("plugins.toml"), landing_body).unwrap();
let xdg_dir = tmp.path().join("xdg");
fs::create_dir_all(&xdg_dir).unwrap();
if let Some(body) = xdg {
fs::write(xdg_dir.join("plugins.toml"), body).unwrap();
}
let user_config = xdg_dir.join("config.toml"); (tmp, user_config)
}
#[test]
fn effective_with_no_xdg_layer_is_the_landing_schedule() {
let (tmp, user_config) = layered(SAMPLE, None);
let hooks = Hooks::effective(tmp.path(), &user_config).unwrap();
assert_eq!(hooks.names("close", "post"), ["bl-delivery", "tracker"]);
}
#[test]
fn effective_xdg_bare_key_replaces_and_directives_compose() {
let xdg = r#"
[hooks]
"close.post" = ["only-me"]
"create.post_append" = ["mirror"]
"close.pre_prepend" = ["lint"]
"#;
let (tmp, user_config) = layered(SAMPLE, Some(xdg));
let hooks = Hooks::effective(tmp.path(), &user_config).unwrap();
assert_eq!(hooks.names("close", "post"), ["only-me"], "bare key replaces");
assert_eq!(hooks.names("create", "post"), ["tracker", "mirror"], "_append composes");
assert_eq!(hooks.names("close", "pre"), ["lint", "bl-delivery"], "_prepend composes");
}
#[test]
fn effective_xdg_ban_removes_a_landing_entry() {
let xdg = "[hooks]\n\"close.post_ban\" = [\"bl-delivery\"]\n";
let (tmp, user_config) = layered(SAMPLE, Some(xdg));
let hooks = Hooks::effective(tmp.path(), &user_config).unwrap();
assert_eq!(hooks.names("close", "post"), ["tracker"], "_ban drops the named entry");
}
#[test]
fn effective_seeds_from_xdg_when_the_landing_has_none() {
let tmp = TempDir::new().unwrap();
let xdg_dir = tmp.path().join("xdg");
fs::create_dir_all(&xdg_dir).unwrap();
fs::write(xdg_dir.join("plugins.toml"), "[hooks]\n\"sync.pre\" = [\"tracker\"]\n").unwrap();
let hooks = Hooks::effective(tmp.path(), &xdg_dir.join("config.toml")).unwrap();
assert_eq!(hooks.names("sync", "pre"), ["tracker"]);
}
#[test]
fn effective_treats_a_layer_without_a_hooks_table_as_empty() {
let (tmp, user_config) = layered("# nothing wired here\n", Some("[hooks]\n\"sync.pre\" = [\"tracker\"]\n"));
let hooks = Hooks::effective(tmp.path(), &user_config).unwrap();
assert_eq!(hooks.names("sync", "pre"), ["tracker"]);
assert!(hooks.names("close", "post").is_empty());
}
#[test]
fn effective_rejects_a_malformed_layer() {
let (tmp, user_config) = layered("[hooks", None);
assert!(Hooks::effective(tmp.path(), &user_config).is_err());
}
#[test]
fn effective_propagates_a_non_not_found_read_error() {
let tmp = TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("config").join("plugins.toml")).unwrap();
assert!(Hooks::effective(tmp.path(), &tmp.path().join("xdg").join("config.toml")).is_err());
}
#[test]
fn resolve_stitches_names_to_local_binaries_in_order() {
let tmp = TempDir::new().unwrap();
let reg = Registry::at(tmp.path());
let bin = tmp.path().join("real");
fs::write(&bin, "x").unwrap();
reg.bind("tracker", &bin).unwrap(); let hooks = Hooks::parse(SAMPLE).unwrap();
let refs = hooks.resolve(®, "close", "post");
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].name, "bl-delivery");
assert_eq!(refs[0].bin, None);
assert_eq!(refs[1].name, "tracker");
assert_eq!(refs[1].bin, Some(fs::canonicalize(&bin).unwrap()));
}
#[test]
fn resolve_read_uses_the_bare_op_key_for_the_single_read_phase() {
let tmp = TempDir::new().unwrap();
let reg = Registry::at(tmp.path());
let hooks = Hooks::parse(SAMPLE).unwrap();
let refs = hooks.resolve_read(®, "show");
assert_eq!(refs.len(), 1);
assert_eq!((refs[0].name.as_str(), refs[0].bin.as_deref()), ("bl-delivery", None));
assert!(hooks.resolve_read(®, "close").is_empty()); assert!(hooks.names("show", "read").is_empty()); }
#[test]
fn referenced_maps_each_plugin_to_the_ops_it_is_wired_into() {
let refs = Hooks::parse(SAMPLE).unwrap().referenced();
assert_eq!(refs["tracker"], BTreeSet::from(["close".to_string(), "create".to_string()]));
assert_eq!(refs["bl-delivery"], BTreeSet::from(["close".to_string(), "show".to_string()]));
}
#[test]
fn retain_drops_names_failing_the_predicate() {
let mut hooks = Hooks::parse(SAMPLE).unwrap();
hooks.retain(|name| name == "tracker"); assert_eq!(hooks.names("close", "post"), ["tracker"]);
assert!(hooks.names("close", "pre").is_empty()); }
#[test]
fn to_toml_round_trips_through_parse_and_drops_emptied_lists() {
let mut hooks = Hooks::parse(SAMPLE).unwrap();
hooks.retain(|name| name == "tracker");
let body = hooks.to_toml();
let reparsed = Hooks::parse(&body).unwrap();
assert_eq!(reparsed.names("close", "post"), ["tracker"]);
assert!(!body.contains("close.pre"));
}