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<super::author::Authored> {
super::base_change(verb, store, flags, now, &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 parse_collects_every_flag_and_positional() {
let f = parse(
&strs(&[
"the-id", "k=v", "--as", "ann", "-m", "note", "--body", "para", "--title", "New",
"--parent", "bl-p", "--no-parent", "--subtask-of", "bl-e", "--blocks", "bl-g:close",
"--needs", "bl-n", "--no-needs", "bl-rm", "-p", "3", "--no-priority", "-t", "x",
"--no-tag", "y", "--edit",
]),
"default",
)
.unwrap();
assert_eq!(f.actor, "ann");
assert_eq!(f.message.as_deref(), Some("note"));
assert_eq!(f.body.as_deref(), Some("para"));
assert_eq!(f.title.as_deref(), Some("New"));
assert_eq!(f.parent.as_deref(), Some("bl-p"));
assert!(f.no_parent);
assert_eq!(f.subtask_of.as_deref(), Some("bl-e"));
assert_eq!(f.blocks, ["bl-g:close"]);
assert_eq!(f.needs, ["bl-n"]);
assert_eq!(f.no_needs, ["bl-rm"]);
assert_eq!(f.priority, Some(3));
assert!(f.no_priority);
assert_eq!(f.tags, ["x"]);
assert_eq!(f.no_tags, ["y"]);
assert!(f.edit);
assert_eq!(f.positionals, ["the-id", "k=v"]);
assert_eq!(parse(&[], "default").unwrap().actor, "default");
}
#[test]
fn parse_rejects_an_unknown_flag() {
assert!(parse(&strs(&["--nope"]), "me").is_err());
}
#[test]
fn parse_resolves_the_per_op_remote_override() {
assert_eq!(parse(&strs(&["--remote", "r"]), "me").unwrap().remote.as_deref(), Some("r"));
assert_eq!(parse(&strs(&["--center", "c"]), "me").unwrap().remote.as_deref(), Some("c"));
for order in [["--center", "c", "--remote", "r"], ["--remote", "r", "--center", "c"]] {
assert_eq!(parse(&strs(&order), "me").unwrap().remote.as_deref(), Some("r"));
}
}
#[test]
fn parse_accepts_glued_short_flags() {
let f = parse(&strs(&["a title", "-p1", "-turgent", "-mglued note"]), "me").unwrap();
assert_eq!(f.priority, Some(1));
assert_eq!(f.tags, ["urgent"]);
assert_eq!(f.message.as_deref(), Some("glued note"));
assert_eq!(f.positionals, ["a title"]);
assert_eq!(parse(&strs(&["-p-5"]), "me").unwrap().priority, Some(-5));
assert_eq!(parse(&strs(&["-p", "2"]), "me").unwrap().priority, Some(2));
assert!(parse(&strs(&["-x1"]), "me").is_err());
}
#[test]
fn parse_honors_the_end_of_options_separator() {
let f = parse(&strs(&["-p", "1", "--", "--title", "-p2", "--"]), "me").unwrap();
assert_eq!(f.priority, Some(1));
assert!(f.title.is_none());
assert_eq!(f.positionals, ["--title", "-p2", "--"]);
assert_eq!(parse(&strs(&["--", "-p1"]), "me").unwrap().positionals, ["-p1"]);
assert!(parse(&strs(&["--"]), "me").unwrap().positionals.is_empty());
}
#[test]
fn parse_errors_on_a_flag_missing_its_value() {
let err = parse(&strs(&["--as"]), "me").unwrap_err();
assert!(err.to_string().contains("--as needs a value"));
}
#[test]
fn parse_rejects_a_non_integer_priority() {
let err = parse(&strs(&["-p", "high"]), "me").unwrap_err();
assert!(err.to_string().contains("not an integer"));
}
#[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() {
let mut f = flags();
f.body = Some("para".into());
let c = command(Verb::Create, &f);
assert_eq!(c.op, "create");
assert_eq!(c.body_change.as_deref(), Some("para"));
}
#[test]
fn now_reads_a_positive_clock() {
assert!(now() > 1_700_000_000); }
#[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_create_tests.rs"]
mod create;
#[path = "mutate_update_tests.rs"]
mod update;