use super::*;
use crate::task::{Blocker, On};
use crate::taskfile::{read_task, task_ids, write_task};
use std::fs;
use std::path::Path;
use tempfile::tempdir;
const TASK: &str = "+++\ntitle = \"A task\"\ncreated = 0\nupdated = 0\n+++\nbody\n";
fn flags() -> Flags {
Flags { actor: "me".into(), ..Flags::default() }
}
fn base_change(verb: Verb, store: &Path, flags: &Flags, now: i64) -> io::Result<(Box<dyn BaseChange>, Option<Task>)> {
authored(verb, store, flags, now).map(|a| (a.base, a.before))
}
fn authored(verb: Verb, store: &Path, flags: &Flags, now: i64) -> io::Result<Authored> {
super::base_change(verb, store, flags, now, Vec::new(), &mut edit::Editor::detached())
.map(|authored| authored.expect("flag-driven authoring never no-ops"))
}
fn write(dir: &Path, id: &str, md: &str) {
let tasks = dir.join("tasks");
fs::create_dir_all(&tasks).unwrap();
fs::write(tasks.join(format!("{id}.md")), md).unwrap();
}
fn new_id(dir: &Path, known: &[&str]) -> String {
task_ids(dir).unwrap().into_iter().find(|id| !known.contains(&id.as_str())).unwrap()
}
fn strs(args: &[&str]) -> Vec<String> {
args.iter().map(ToString::to_string).collect()
}
#[test]
fn claim_authors_occupancy_and_returns_the_before_state() {
let d = tempdir().unwrap();
let dir = d.path();
write(dir, "bl-1", TASK);
let mut f = flags();
f.positionals = vec!["bl-1".into()];
let (base, before) = base_change(Verb::Claim, dir, &f, 7).unwrap();
assert_eq!(before.unwrap().title, "A task");
base.stage(dir).unwrap();
let t = read_task(dir, "bl-1").unwrap();
assert_eq!(t.claimant.as_deref(), Some("me"));
assert_eq!(t.updated, 7);
}
#[test]
fn unclaim_clears_the_claimant() {
let d = tempdir().unwrap();
let dir = d.path();
write(dir, "bl-1", "+++\ntitle = \"A\"\ncreated = 0\nupdated = 0\nclaimant = \"bob\"\n+++\n");
let mut f = flags();
f.positionals = vec!["bl-1".into()];
let (base, _) = base_change(Verb::Unclaim, dir, &f, 0).unwrap();
base.stage(dir).unwrap();
assert!(read_task(dir, "bl-1").unwrap().claimant.is_none());
}
#[test]
fn an_occupancy_verb_rejects_shaping_flags() {
let d = tempdir().unwrap();
write(d.path(), "bl-1", TASK);
let id = || {
let mut f = flags();
f.positionals = vec!["bl-1".into()];
f
};
let mut structural = id();
structural.parent = Some("bl-2".into());
assert!(base_change(Verb::Claim, d.path(), &structural, 0).is_err());
let mut shaping = id();
shaping.priority = Some(1);
assert!(base_change(Verb::Close, d.path(), &shaping, 0).is_err());
let mut last = id();
last.no_tags = vec!["x".into()];
let err = base_change(Verb::Unclaim, d.path(), &last, 0).err().unwrap();
assert!(err.to_string().contains("no field edits"));
let mut narrated = id();
narrated.message = Some("note".into());
assert!(base_change(Verb::Claim, d.path(), &narrated, 0).is_ok());
let mut edited = id();
edited.edit = true;
let err = base_change(Verb::Claim, d.path(), &edited, 0).err().unwrap();
assert!(err.to_string().contains("no field edits"));
}
#[test]
fn each_shaping_flag_bounces_an_occupancy_verb_on_its_own() {
let d = tempdir().unwrap();
write(d.path(), "bl-1", TASK);
let solo: &[fn(&mut Flags)] = &[
|f| f.title = Some("t".into()),
|f| f.body = Some("b".into()),
|f| f.parent = Some("bl-p".into()),
|f| f.no_parent = true,
|f| f.subtask_of = Some("bl-e".into()),
|f| f.no_priority = true,
|f| f.priority = Some(1),
|f| f.blocks = vec!["close".into()],
|f| f.needs = vec!["bl-n".into()],
|f| f.no_needs = vec!["bl-n".into()],
|f| f.tags = vec!["x".into()],
|f| f.no_tags = vec!["x".into()],
];
for (i, set) in solo.iter().enumerate() {
let mut f = flags();
f.positionals = vec!["bl-1".into()];
set(&mut f);
assert!(base_change(Verb::Claim, d.path(), &f, 0).is_err(), "solo flag #{i} slipped through");
}
}
#[test]
fn create_rejects_title_flag_and_uses_the_positional() {
let mut f = flags();
f.positionals = vec!["the title".into()];
f.title = Some("via flag".into());
let err = base_change(Verb::Create, tempdir().unwrap().path(), &f, 0).err().unwrap();
assert!(err.to_string().contains("positional argument, not --title"));
}
#[test]
fn close_retires_the_ball() {
let d = tempdir().unwrap();
let dir = d.path();
write(dir, "bl-1", TASK);
let mut f = flags();
f.positionals = vec!["bl-1".into()];
let (base, before) = base_change(Verb::Close, dir, &f, 0).unwrap();
assert_eq!(before.unwrap().title, "A task");
base.stage(dir).unwrap();
assert!(!dir.join("tasks/bl-1.md").exists());
assert!(base.finalize(dir).unwrap().starts_with("A task"));
}
#[test]
fn base_change_rejects_a_non_mutating_verb() {
let err = base_change(Verb::Show, tempdir().unwrap().path(), &flags(), 0).err().unwrap();
assert!(err.to_string().contains("not a mutating verb"));
}
#[test]
fn command_marks_a_mutating_op_and_carries_the_body_and_the_ball() {
let mut f = flags();
f.body = Some("para".into());
let c = command(Verb::Create, &f, None, "bl-1234".into());
assert_eq!(c.op, "create");
assert_eq!(c.body_change.as_deref(), Some("para"));
assert_eq!(c.id.as_deref(), Some("bl-1234"));
}
#[test]
fn authoring_names_the_ops_ball() {
let d = tempdir().unwrap();
let dir = d.path();
write(dir, "bl-1", TASK);
let mut named = flags();
named.positionals = vec!["bl-1".into()];
assert_eq!(authored(Verb::Close, dir, &named, 0).unwrap().id, "bl-1");
let mut fresh = flags();
fresh.positionals = vec!["A new ball".into()];
let minted = authored(Verb::Create, dir, &fresh, 0).unwrap();
minted.base.stage(dir).unwrap();
assert_eq!(minted.id, new_id(dir, &["bl-1"]), "create carries the id it minted");
}
#[test]
fn children_notice_agrees_in_number_and_stays_silent_at_zero() {
assert_eq!(super::report::children_notice("bl-x", 0), None);
assert_eq!(
super::report::children_notice("bl-x", 1).unwrap(),
"notice: bl-x closed with 1 open child, not gating โ its parent pointer now dangles (display-only)"
);
assert_eq!(
super::report::children_notice("bl-x", 2).unwrap(),
"notice: bl-x closed with 2 open children, none gating โ their parent pointers now dangle (display-only)"
);
}
#[test]
fn change_tokens_are_hex_and_distinct() {
let (a, b) = (change_token(), change_token());
assert_eq!(a.len(), 32);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(a, b);
}
#[path = "mutate_args_tests.rs"]
mod args;
#[path = "mutate_create_tests.rs"]
mod create;
#[path = "mutate_update_tests.rs"]
mod update;
#[path = "mutate_comment_tests.rs"]
mod comment;