use super::merge::{list_directive, ListOp};
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn landing(tmp: &TempDir, name: &str, body: &str) -> PathBuf {
let dir = tmp.path().join(name);
fs::create_dir_all(dir.join("config")).unwrap();
fs::write(dir.join("config").join("balls.toml"), body).unwrap();
dir
}
fn bare(tmp: &TempDir, name: &str) -> PathBuf {
tmp.path().join(name)
}
fn table(body: &str) -> Table {
toml::from_str(body).unwrap()
}
#[test]
fn with_no_layers_it_resolves_to_the_built_in_defaults() {
let tmp = TempDir::new().unwrap();
let cfg = EffectiveConfig::resolve(&bare(&tmp, "landing"), &bare(&tmp, "absent.toml")).unwrap();
assert_eq!(cfg, EffectiveConfig::default());
assert_eq!(cfg.tasks_branch, DEFAULT_TASKS_BRANCH);
}
#[test]
fn the_landing_value_is_read() {
let tmp = TempDir::new().unwrap();
let land = landing(&tmp, "landing", "tasks_branch = \"land\"\n");
let cfg = EffectiveConfig::resolve(&land, &bare(&tmp, "absent.toml")).unwrap();
assert_eq!(cfg.tasks_branch, "land");
}
#[test]
fn log_level_defaults_to_info_and_reads_from_a_layer() {
let tmp = TempDir::new().unwrap();
let cfg = EffectiveConfig::resolve(&bare(&tmp, "none"), &bare(&tmp, "absent.toml")).unwrap();
assert_eq!(cfg.log_level, "info");
let land = landing(&tmp, "noisy", "log_level = \"debug\"\n");
let cfg = EffectiveConfig::resolve(&land, &bare(&tmp, "absent.toml")).unwrap();
assert_eq!(cfg.log_level, "debug");
}
#[test]
fn the_user_config_overrides_the_landing() {
let tmp = TempDir::new().unwrap();
let land = landing(&tmp, "landing", "tasks_branch = \"land\"\n");
let user = tmp.path().join("user.toml");
fs::write(&user, "tasks_branch = \"user\"\n").unwrap();
let cfg = EffectiveConfig::resolve(&land, &user).unwrap();
assert_eq!(cfg.tasks_branch, "user");
}
#[test]
fn a_malformed_layer_is_an_error_naming_the_file() {
let tmp = TempDir::new().unwrap();
let land = landing(&tmp, "landing", "tasks_branch = [not valid toml\n");
let err = EffectiveConfig::resolve(&land, &bare(&tmp, "absent.toml")).unwrap_err();
assert!(err.to_string().contains("balls.toml"));
}
#[test]
fn a_layer_read_error_other_than_absence_propagates() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("a-dir");
fs::create_dir_all(&dir).unwrap();
assert!(EffectiveConfig::resolve(&bare(&tmp, "landing"), &dir).is_err());
}
#[test]
fn a_tasks_branch_naming_the_landing_is_refused_by_name() {
let tmp = TempDir::new().unwrap();
let land = landing(&tmp, "landing", "tasks_branch = \"balls/config\"\n");
let err = EffectiveConfig::resolve(&land, &bare(&tmp, "absent.toml")).unwrap_err();
assert!(err.to_string().contains("names the landing"), "{err}");
let user = tmp.path().join("user.toml");
fs::write(&user, "tasks_branch = \"balls/config\"\n").unwrap();
let err = EffectiveConfig::resolve(&bare(&tmp, "clean"), &user).unwrap_err();
assert!(err.to_string().contains("names the landing"), "{err}");
}
#[test]
fn a_value_that_clashes_with_the_typed_schema_does_not_resolve() {
let tmp = TempDir::new().unwrap();
let land = landing(&tmp, "landing", "tasks_branch = [1, 2]\n");
let err = EffectiveConfig::resolve(&land, &bare(&tmp, "absent.toml")).unwrap_err();
assert!(err.to_string().contains("config does not resolve"));
}
#[test]
fn list_append_concatenates_incoming_after_the_current() {
let mut base = table("tags = ['a']");
layer_over(&mut base, table("tags_append = ['b']"));
assert_eq!(base["tags"].as_array().unwrap().len(), 2);
assert_eq!(base["tags"][0].as_str(), Some("a"));
assert_eq!(base["tags"][1].as_str(), Some("b"));
}
#[test]
fn list_prepend_puts_incoming_first() {
let mut base = table("tags = ['b']");
layer_over(&mut base, table("tags_prepend = ['a']"));
assert_eq!(base["tags"][0].as_str(), Some("a"));
assert_eq!(base["tags"][1].as_str(), Some("b"));
}
#[test]
fn list_ban_removes_every_named_element() {
let mut base = table("tags = ['a', 'b', 'c']");
layer_over(&mut base, table("tags_ban = ['b']"));
let tags: Vec<_> = base["tags"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
assert_eq!(tags, ["a", "c"]);
}
#[test]
fn a_bare_field_replaces_and_a_directive_composes_in_one_layer() {
let mut base = table("tasks_branch = 'old'\ntags = ['a']");
layer_over(&mut base, table("tasks_branch = 'new'\ntags_append = ['b']"));
assert_eq!(base["tasks_branch"].as_str(), Some("new"));
assert_eq!(base["tags"].as_array().unwrap().len(), 2);
}
#[test]
fn a_directive_seeds_an_absent_field() {
let mut base = Table::new();
layer_over(&mut base, table("tags_append = ['a']"));
assert_eq!(base["tags"].as_array().unwrap().len(), 1);
}
#[test]
fn a_directive_over_a_non_list_treats_the_current_as_empty() {
let mut base = table("tags = 'scalar'");
layer_over(&mut base, table("tags_append = ['a']"));
let tags = base["tags"].as_array().unwrap();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].as_str(), Some("a"));
}
#[test]
fn list_directive_splits_known_suffixes_only() {
assert!(matches!(list_directive("tags_prepend"), Some(("tags", ListOp::Prepend))));
assert!(matches!(list_directive("tags_append"), Some(("tags", ListOp::Append))));
assert!(matches!(list_directive("tags_ban"), Some(("tags", ListOp::Ban))));
assert!(list_directive("tags").is_none()); assert!(list_directive("_append").is_none()); }
#[test]
fn xdg_remote_reads_the_user_layer_remote_else_none() {
let tmp = TempDir::new().unwrap();
let with = tmp.path().join("with.toml");
fs::write(&with, "remote = \"git@hub:xdg\"\n").unwrap();
assert_eq!(xdg_remote(&with).as_deref(), Some("git@hub:xdg"));
let without = tmp.path().join("without.toml");
fs::write(&without, "log_level = \"info\"\n").unwrap();
assert_eq!(xdg_remote(&without), None);
assert_eq!(xdg_remote(&tmp.path().join("absent.toml")), None);
}
#[test]
fn remote_ladder_resolves_cli_over_sentinel_over_xdg() {
let tmp = TempDir::new().unwrap();
let user = tmp.path().join("config.toml");
fs::write(&user, "remote = \"git@hub:xdg\"\n").unwrap();
let no_binding = tmp.path().join("absent-binding.toml");
let empty = landing(&tmp, "empty", "");
assert_eq!(remote_ladder(None, &empty, &no_binding, &user).unwrap(), (Some("git@hub:xdg".into()), false));
let stealth = landing(&tmp, "stealth", "task_remote = \"none\"\n");
assert_eq!(remote_ladder(None, &stealth, &no_binding, &user).unwrap(), (None, true));
assert_eq!(
remote_ladder(Some("git@hub:op".into()), &stealth, &no_binding, &user).unwrap(),
(Some("git@hub:op".into()), false)
);
}
#[test]
fn remote_ladder_binding_outranks_the_legacy_xdg_remote() {
let tmp = TempDir::new().unwrap();
let user = tmp.path().join("config.toml");
fs::write(&user, "remote = \"git@hub:xdg\"\n").unwrap();
let empty = landing(&tmp, "empty", "");
let binding = tmp.path().join("binding.toml");
fs::write(&binding, "remote = \"git@hub:binding\"\n").unwrap();
assert_eq!(remote_ladder(None, &empty, &binding, &user).unwrap(), (Some("git@hub:binding".into()), false));
let no_binding = tmp.path().join("absent-binding.toml");
assert_eq!(remote_ladder(None, &empty, &no_binding, &user).unwrap(), (Some("git@hub:xdg".into()), false));
}
#[test]
fn binding_remote_reads_the_remote_key_else_none() {
let tmp = TempDir::new().unwrap();
let with = tmp.path().join("with.toml");
fs::write(&with, "remote = \"git@hub:bind\"\ntasks_branch = \"balls/x\"\n").unwrap();
assert_eq!(binding_remote(&with).as_deref(), Some("git@hub:bind"));
let without = tmp.path().join("without.toml");
fs::write(&without, "tasks_branch = \"balls/x\"\n").unwrap();
assert_eq!(binding_remote(&without), None);
assert_eq!(binding_remote(&tmp.path().join("absent.toml")), None);
}
#[test]
fn remote_ladder_refuses_a_url_in_the_landing_rung() {
let tmp = TempDir::new().unwrap();
let poison = landing(&tmp, "poison", "task_remote = \"git@hub:evil\"\n");
let absent = tmp.path().join("absent.toml");
let err = remote_ladder(None, &poison, &absent, &absent).unwrap_err().to_string();
assert!(err.contains("clone's binding"), "{err}");
}