use super::*;
use tempfile::TempDir;
fn worktree(parent_id: &str) -> TempDir {
let tmp = TempDir::new().unwrap();
let parent = Task {
title: "The claimed ball".into(),
created: 100,
updated: 4242, claimant: Some("tester".into()),
root_commit: Some("deadbeef".into()),
..Task::default()
};
write_task(tmp.path(), parent_id, &parent).unwrap();
tmp
}
fn landing_with(toml: &str) -> TempDir {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("config/plugins/bl-chore");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("chores.toml"), toml).unwrap();
tmp
}
fn wire(landing: &str, tags: &[&str], id: Option<&str>) -> String {
serde_json::json!({
"binding": { "landing": landing },
"command": { "op": "claim", "id": id },
"current_state": { "tags": tags },
})
.to_string()
}
const TWO_CHORES: &str = "[[chore]]\ntitle = \"Run the test suite\"\n[[chore]]\ntitle = \"Review the docs\"\n";
fn children(dir: &Path, except: &str) -> Vec<(String, Task)> {
let mut out: Vec<(String, Task)> = task_ids(dir)
.unwrap()
.into_iter()
.filter(|id| id != except)
.map(|id| {
let t = read_task(dir, &id).unwrap();
(id, t)
})
.collect();
out.sort_by(|a, b| a.1.title.cmp(&b.1.title));
out
}
#[test]
fn only_the_claim_pre_forward_pass_does_anything() {
let land = landing_with(TWO_CHORES);
let l = land.path().to_string_lossy().into_owned();
let w = worktree("bl-9");
let mint_nothing = |op: &str, phase: &str, payload: &str| {
run(op, phase, "bl-chore", w.path(), payload).unwrap();
assert_eq!(children(w.path(), "bl-9"), vec![], "{op}.{phase} minted");
};
mint_nothing("update", "pre", &wire(&l, &[], Some("bl-9")));
mint_nothing("claim", "post", &wire(&l, &[], Some("bl-9")));
let mut rb: serde_json::Value = serde_json::from_str(&wire(&l, &[], Some("bl-9"))).unwrap();
rb["rolling_back"] = serde_json::json!("pre");
mint_nothing("claim", "pre", &rb.to_string());
}
#[test]
fn tag_skip_bails_when_the_claimed_task_carries_the_tag() {
let land = landing_with(TWO_CHORES);
let w = worktree("bl-9");
let payload = wire(&land.path().to_string_lossy(), &["bl-chore"], Some("bl-9"));
run("claim", "pre", "bl-chore", w.path(), &payload).unwrap();
assert_eq!(children(w.path(), "bl-9"), vec![], "a chore must not mint a chore-of-a-chore");
}
#[test]
fn an_absent_or_choreless_config_mints_nothing() {
let w = worktree("bl-9");
for landing in ["/nonexistent-landing".to_string(), landing_with("").path().to_string_lossy().into_owned()] {
run("claim", "pre", "bl-chore", w.path(), &wire(&landing, &[], Some("bl-9"))).unwrap();
assert_eq!(children(w.path(), "bl-9"), vec![]);
}
}
#[test]
fn the_mint_writes_each_chore_as_a_close_gate_child_of_the_claimed_ball() {
let land = landing_with(
"[[chore]]\ntitle = \"Run the test suite\"\nbody = \"cargo test\"\npriority = 3\n\
[[chore]]\ntitle = \"Review the docs\"\n",
);
let w = worktree("bl-9");
run("claim", "pre", "bl-chore", w.path(), &wire(&land.path().to_string_lossy(), &[], Some("bl-9"))).unwrap();
let kids = children(w.path(), "bl-9");
let titles: Vec<&str> = kids.iter().map(|(_, t)| t.title.as_str()).collect();
assert_eq!(titles, vec!["Review the docs", "Run the test suite"]);
for (id, t) in &kids {
assert!(crate::id::is_valid(id), "minted a malformed id: {id}");
assert_eq!(t.parent.as_deref(), Some("bl-9"));
assert_eq!(t.tags, vec!["bl-chore".to_string()], "the recursion-break tag is always injected");
assert_eq!((t.created, t.updated), (4242, 4242));
assert_eq!(t.root_commit.as_deref(), Some("deadbeef"));
}
let suite = &kids[1].1;
assert_eq!(suite.priority, Some(3));
assert_eq!(suite.body, "cargo test");
let parent = read_task(w.path(), "bl-9").unwrap();
let gated: Vec<&str> = parent.blockers.iter().map(|b| b.id.as_str()).collect();
let mut minted: Vec<&str> = kids.iter().map(|(id, _)| id.as_str()).collect();
minted.sort_unstable();
let mut gated_sorted = gated.clone();
gated_sorted.sort_unstable();
assert_eq!(gated_sorted, minted);
assert!(parent.blockers.iter().all(|b| b.on == On::Close));
}
#[test]
fn epic_skip_bails_on_an_existing_child_and_the_knob_turns_it_off() {
let land = landing_with(TWO_CHORES);
let l = land.path().to_string_lossy().into_owned();
let w = worktree("bl-9");
let kid = Task { title: "A real subtask".into(), parent: Some("bl-9".into()), ..Task::default() };
write_task(w.path(), "bl-kid", &kid).unwrap();
run("claim", "pre", "bl-chore", w.path(), &wire(&l, &[], Some("bl-9"))).unwrap();
assert_eq!(children(w.path(), "bl-9").len(), 1, "epic-skip: only the pre-existing child");
let off = landing_with(&format!("epic_skip = false\n{TWO_CHORES}"));
let payload = wire(&off.path().to_string_lossy(), &[], Some("bl-9"));
run("claim", "pre", "bl-chore", w.path(), &payload).unwrap();
assert_eq!(children(w.path(), "bl-9").len(), 3);
}
#[test]
fn a_ball_with_an_unrelated_child_is_not_epic_skipped() {
let land = landing_with(TWO_CHORES);
let w = worktree("bl-9");
let other = Task { title: "Child of someone else".into(), parent: Some("bl-other".into()), ..Task::default() };
write_task(w.path(), "bl-oth", &other).unwrap();
run("claim", "pre", "bl-chore", w.path(), &wire(&land.path().to_string_lossy(), &[], Some("bl-9"))).unwrap();
assert_eq!(children(w.path(), "bl-9").len(), 3); }
#[test]
fn a_wire_that_names_no_ball_is_a_contract_violation() {
let land = landing_with(TWO_CHORES);
let w = worktree("bl-9");
let err = run("claim", "pre", "bl-chore", w.path(), &wire(&land.path().to_string_lossy(), &[], None))
.unwrap_err()
.to_string();
assert!(err.contains("names no ball"), "{err}");
}
#[test]
fn a_malformed_payload_or_config_aborts_the_claim() {
let w = worktree("bl-9");
let bad_wire = run("claim", "pre", "bl-chore", w.path(), "not json").unwrap_err().to_string();
assert!(bad_wire.contains("expected"), "{bad_wire}");
let land = landing_with("[[chore]]\nnot_a_title = 1\n");
let bad_cfg = run("claim", "pre", "bl-chore", w.path(), &wire(&land.path().to_string_lossy(), &[], Some("bl-9")))
.unwrap_err()
.to_string();
assert!(bad_cfg.contains("title"), "{bad_cfg}");
let land = TempDir::new().unwrap();
fs::create_dir_all(land.path().join("config/plugins/bl-chore/chores.toml")).unwrap();
let payload = wire(&land.path().to_string_lossy(), &[], Some("bl-9"));
assert!(run("claim", "pre", "bl-chore", w.path(), &payload).is_err());
}
#[test]
fn an_unreadable_sibling_or_missing_parent_aborts_rather_than_minting_blind() {
let land = landing_with(TWO_CHORES);
let l = land.path().to_string_lossy().into_owned();
let w = worktree("bl-9");
fs::write(w.path().join("tasks").join("bl-bad.md"), "not a ball").unwrap();
assert!(run("claim", "pre", "bl-chore", w.path(), &wire(&l, &[], Some("bl-9"))).is_err());
let empty = TempDir::new().unwrap();
let off = landing_with(&format!("epic_skip = false\n{TWO_CHORES}"));
let payload = wire(&off.path().to_string_lossy(), &[], Some("bl-9"));
assert!(run("claim", "pre", "bl-chore", empty.path(), &payload).is_err());
}