1use assert_cmd::Command;
2use predicates::prelude::*;
3use tempfile::TempDir;
4
5fn td(home: &TempDir) -> Command {
6 let mut cmd = Command::cargo_bin("td").unwrap();
7 cmd.env("HOME", home.path());
8 cmd
9}
10
11fn init_tmp() -> TempDir {
12 let tmp = TempDir::new().unwrap();
13 td(&tmp)
14 .args(["init", "main"])
15 .current_dir(&tmp)
16 .assert()
17 .success();
18 tmp
19}
20
21fn create_task(dir: &TempDir, title: &str) -> String {
22 let out = td(dir)
23 .args(["--json", "create", title])
24 .current_dir(dir)
25 .output()
26 .unwrap();
27 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
28 v["id"].as_str().unwrap().to_string()
29}
30
31fn get_task_json(dir: &TempDir, id: &str) -> serde_json::Value {
32 let out = td(dir)
33 .args(["--json", "show", id])
34 .current_dir(dir)
35 .output()
36 .unwrap();
37 serde_json::from_slice(&out.stdout).unwrap()
38}
39
40#[test]
41fn rm_deletes_task() {
42 let tmp = init_tmp();
43 let id = create_task(&tmp, "Delete me");
44
45 td(&tmp)
46 .args(["rm", &id])
47 .current_dir(&tmp)
48 .assert()
49 .success()
50 .stdout(predicate::str::contains("deleted"));
51
52 let task = get_task_json(&tmp, &id);
53 assert_eq!(task["status"].as_str().unwrap(), "closed");
54}
55
56#[test]
57fn rm_deletes_multiple_ids() {
58 let tmp = init_tmp();
59 let id1 = create_task(&tmp, "First");
60 let id2 = create_task(&tmp, "Second");
61
62 td(&tmp)
63 .args(["rm", &id1, &id2])
64 .current_dir(&tmp)
65 .assert()
66 .success();
67
68 assert_eq!(get_task_json(&tmp, &id1)["status"], "closed");
69 assert_eq!(get_task_json(&tmp, &id2)["status"], "closed");
70}
71
72#[test]
73fn rm_requires_recursive_for_parent_task() {
74 let tmp = init_tmp();
75 let parent = create_task(&tmp, "Parent");
76 td(&tmp)
77 .args(["create", "Child", "--parent", &parent])
78 .current_dir(&tmp)
79 .assert()
80 .success();
81
82 td(&tmp)
83 .args(["rm", &parent])
84 .current_dir(&tmp)
85 .assert()
86 .failure()
87 .stderr(predicate::str::contains("use --recursive"));
88}
89
90#[test]
91fn rm_recursive_deletes_subtree() {
92 let tmp = init_tmp();
93 let parent = create_task(&tmp, "Parent");
94
95 let out = td(&tmp)
96 .args(["--json", "create", "Child", "--parent", &parent])
97 .current_dir(&tmp)
98 .output()
99 .unwrap();
100 let child: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
101 let child_id = child["id"].as_str().unwrap().to_string();
102
103 let out = td(&tmp)
104 .args(["--json", "create", "Grandchild", "--parent", &child_id])
105 .current_dir(&tmp)
106 .output()
107 .unwrap();
108 let grandchild: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
109 let grandchild_id = grandchild["id"].as_str().unwrap().to_string();
110
111 td(&tmp)
112 .args(["rm", "--recursive", &parent])
113 .current_dir(&tmp)
114 .assert()
115 .success();
116
117 assert_eq!(get_task_json(&tmp, &parent)["status"], "closed");
118 assert_eq!(get_task_json(&tmp, &child_id)["status"], "closed");
119 assert_eq!(get_task_json(&tmp, &grandchild_id)["status"], "closed");
120}
121
122#[test]
123fn rm_detaches_dependents_and_warns() {
124 let tmp = init_tmp();
125 let dependent = create_task(&tmp, "Dependent");
126 let blocker = create_task(&tmp, "Blocker");
127
128 td(&tmp)
129 .args(["dep", "add", &dependent, &blocker])
130 .current_dir(&tmp)
131 .assert()
132 .success();
133
134 td(&tmp)
135 .args(["rm", &blocker])
136 .current_dir(&tmp)
137 .assert()
138 .success()
139 .stderr(predicate::str::contains("warning"))
140 .stderr(predicate::str::contains(&dependent[dependent.len() - 7..]));
141
142 let dependent_task = get_task_json(&tmp, &dependent);
143 let blockers = dependent_task["blockers"].as_array().unwrap();
144 assert!(blockers.is_empty());
145}
146
147#[test]
148fn rm_force_suppresses_unblocked_warning() {
149 let tmp = init_tmp();
150 let dependent = create_task(&tmp, "Dependent");
151 let blocker = create_task(&tmp, "Blocker");
152
153 td(&tmp)
154 .args(["dep", "add", &dependent, &blocker])
155 .current_dir(&tmp)
156 .assert()
157 .success();
158
159 td(&tmp)
160 .args(["rm", "--force", &blocker])
161 .current_dir(&tmp)
162 .assert()
163 .success()
164 .stderr(predicate::str::is_empty());
165}
166
167#[test]
168fn rm_json_includes_deleted_and_unblocked_ids() {
169 let tmp = init_tmp();
170 let dependent = create_task(&tmp, "Dependent");
171 let blocker = create_task(&tmp, "Blocker");
172
173 td(&tmp)
174 .args(["dep", "add", &dependent, &blocker])
175 .current_dir(&tmp)
176 .assert()
177 .success();
178
179 let out = td(&tmp)
180 .args(["--json", "rm", &blocker])
181 .current_dir(&tmp)
182 .output()
183 .unwrap();
184 assert!(out.status.success());
185
186 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
187 let requested = v["requested_ids"].as_array().unwrap();
188 let deleted = v["deleted_ids"].as_array().unwrap();
189 let unblocked = v["unblocked_ids"].as_array().unwrap();
190
191 assert_eq!(requested, &vec![serde_json::Value::String(blocker.clone())]);
192 assert_eq!(deleted, &vec![serde_json::Value::String(blocker)]);
193 assert_eq!(unblocked, &vec![serde_json::Value::String(dependent)]);
194}