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(["project", "init", "main"])
15 .current_dir(&tmp)
16 .assert()
17 .success();
18 tmp
19}
20
21/// Create a task and return its JSON id.
22fn create_task(dir: &TempDir, title: &str) -> String {
23 let out = td(dir)
24 .args(["--json", "create", title])
25 .current_dir(dir)
26 .output()
27 .unwrap();
28 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
29 v["id"].as_str().unwrap().to_string()
30}
31
32// ── list ─────────────────────────────────────────────────────────────
33
34#[test]
35fn list_shows_created_tasks() {
36 let tmp = init_tmp();
37 create_task(&tmp, "Alpha");
38 create_task(&tmp, "Bravo");
39
40 td(&tmp)
41 .arg("list")
42 .current_dir(&tmp)
43 .assert()
44 .success()
45 .stdout(predicate::str::contains("Alpha"))
46 .stdout(predicate::str::contains("Bravo"));
47}
48
49#[test]
50fn list_json_returns_array() {
51 let tmp = init_tmp();
52 create_task(&tmp, "One");
53
54 let out = td(&tmp)
55 .args(["--json", "list"])
56 .current_dir(&tmp)
57 .output()
58 .unwrap();
59 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
60 assert!(v.is_array(), "expected JSON array, got: {v}");
61 assert_eq!(v.as_array().unwrap().len(), 1);
62 assert_eq!(v[0]["title"].as_str().unwrap(), "One");
63}
64
65#[test]
66fn list_filter_by_status() {
67 let tmp = init_tmp();
68 create_task(&tmp, "Open task");
69
70 // No closed tasks yet.
71 let out = td(&tmp)
72 .args(["--json", "list", "-s", "closed"])
73 .current_dir(&tmp)
74 .output()
75 .unwrap();
76 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
77 assert_eq!(v.as_array().unwrap().len(), 0);
78}
79
80#[test]
81fn list_filter_by_priority() {
82 let tmp = init_tmp();
83
84 td(&tmp)
85 .args(["create", "Low prio", "-p", "low"])
86 .current_dir(&tmp)
87 .assert()
88 .success();
89 td(&tmp)
90 .args(["create", "High prio", "-p", "high"])
91 .current_dir(&tmp)
92 .assert()
93 .success();
94
95 let out = td(&tmp)
96 .args(["--json", "list", "-p", "high"])
97 .current_dir(&tmp)
98 .output()
99 .unwrap();
100 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
101 let tasks = v.as_array().unwrap();
102 assert_eq!(tasks.len(), 1);
103 assert_eq!(tasks[0]["title"].as_str().unwrap(), "High prio");
104}
105
106#[test]
107fn list_filter_by_label() {
108 let tmp = init_tmp();
109
110 td(&tmp)
111 .args(["create", "Tagged", "-l", "urgent"])
112 .current_dir(&tmp)
113 .assert()
114 .success();
115 td(&tmp)
116 .args(["create", "Untagged"])
117 .current_dir(&tmp)
118 .assert()
119 .success();
120
121 let out = td(&tmp)
122 .args(["--json", "list", "-l", "urgent"])
123 .current_dir(&tmp)
124 .output()
125 .unwrap();
126 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
127 let tasks = v.as_array().unwrap();
128 assert_eq!(tasks.len(), 1);
129 assert_eq!(tasks[0]["title"].as_str().unwrap(), "Tagged");
130}
131
132#[test]
133fn list_filter_by_effort() {
134 let tmp = init_tmp();
135
136 td(&tmp)
137 .args(["create", "Easy", "-e", "low"])
138 .current_dir(&tmp)
139 .assert()
140 .success();
141 td(&tmp)
142 .args(["create", "Hard", "-e", "high"])
143 .current_dir(&tmp)
144 .assert()
145 .success();
146
147 let out = td(&tmp)
148 .args(["--json", "list", "-e", "low"])
149 .current_dir(&tmp)
150 .output()
151 .unwrap();
152 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
153 let tasks = v.as_array().unwrap();
154 assert_eq!(tasks.len(), 1);
155 assert_eq!(tasks[0]["title"].as_str().unwrap(), "Easy");
156}
157
158// ── show ─────────────────────────────────────────────────────────────
159
160#[test]
161fn show_displays_task() {
162 let tmp = init_tmp();
163 let id = create_task(&tmp, "Details here");
164
165 td(&tmp)
166 .args(["show", &id])
167 .current_dir(&tmp)
168 .assert()
169 .success()
170 .stdout(predicate::str::contains("Details here"))
171 .stdout(predicate::str::contains(&id[id.len() - 7..]));
172}
173
174#[test]
175fn show_json_includes_labels_and_blockers() {
176 let tmp = init_tmp();
177
178 td(&tmp)
179 .args(["create", "With labels", "-l", "bug,ui"])
180 .current_dir(&tmp)
181 .assert()
182 .success();
183
184 // Get the id via list.
185 let out = td(&tmp)
186 .args(["--json", "list"])
187 .current_dir(&tmp)
188 .output()
189 .unwrap();
190 let list: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
191 let id = list[0]["id"].as_str().unwrap();
192
193 let out = td(&tmp)
194 .args(["--json", "show", id])
195 .current_dir(&tmp)
196 .output()
197 .unwrap();
198 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
199 assert_eq!(v["title"].as_str().unwrap(), "With labels");
200
201 let labels = v["labels"].as_array().unwrap();
202 assert!(labels.contains(&serde_json::Value::String("bug".into())));
203 assert!(labels.contains(&serde_json::Value::String("ui".into())));
204
205 // Blockers should be present (even if empty).
206 assert!(v["blockers"].is_array());
207}
208
209#[test]
210fn show_nonexistent_task_fails() {
211 let tmp = init_tmp();
212
213 td(&tmp)
214 .args(["show", "td-nope"])
215 .current_dir(&tmp)
216 .assert()
217 .failure()
218 .stderr(predicate::str::contains("not found"));
219}
220
221#[test]
222fn show_annotates_closed_blockers() {
223 let tmp = init_tmp();
224 let task = create_task(&tmp, "Blocked task");
225 let open_blocker = create_task(&tmp, "Still open");
226 let closed_blocker = create_task(&tmp, "Will close");
227
228 td(&tmp)
229 .args(["dep", "add", &task, &open_blocker])
230 .current_dir(&tmp)
231 .assert()
232 .success();
233 td(&tmp)
234 .args(["dep", "add", &task, &closed_blocker])
235 .current_dir(&tmp)
236 .assert()
237 .success();
238 td(&tmp)
239 .args(["done", &closed_blocker])
240 .current_dir(&tmp)
241 .assert()
242 .success();
243
244 // Plural label, open blocker bare, closed one annotated.
245 td(&tmp)
246 .args(["show", &task])
247 .current_dir(&tmp)
248 .assert()
249 .success()
250 .stdout(predicate::str::contains("blockers"))
251 .stdout(predicate::str::contains(
252 &open_blocker[open_blocker.len() - 7..],
253 ))
254 .stdout(predicate::str::contains(&format!(
255 "{} [closed]",
256 &closed_blocker[closed_blocker.len() - 7..]
257 )));
258}
259
260#[test]
261fn show_all_closed_blockers_prefixed() {
262 let tmp = init_tmp();
263 let task = create_task(&tmp, "Was blocked");
264 let blocker = create_task(&tmp, "Done now");
265
266 td(&tmp)
267 .args(["dep", "add", &task, &blocker])
268 .current_dir(&tmp)
269 .assert()
270 .success();
271 td(&tmp)
272 .args(["done", &blocker])
273 .current_dir(&tmp)
274 .assert()
275 .success();
276
277 // Singular label, [all closed] prefix.
278 td(&tmp)
279 .args(["show", &task])
280 .current_dir(&tmp)
281 .assert()
282 .success()
283 .stdout(predicate::str::contains("blocker"))
284 .stdout(predicate::str::contains("[all closed]"))
285 .stdout(predicate::str::contains(&blocker[blocker.len() - 7..]));
286}
287
288#[test]
289fn show_single_open_blocker_singular_label() {
290 let tmp = init_tmp();
291 let task = create_task(&tmp, "Blocked");
292 let blocker = create_task(&tmp, "Blocking");
293
294 td(&tmp)
295 .args(["dep", "add", &task, &blocker])
296 .current_dir(&tmp)
297 .assert()
298 .success();
299
300 let out = td(&tmp)
301 .args(["show", &task])
302 .current_dir(&tmp)
303 .output()
304 .unwrap();
305 let stdout = String::from_utf8_lossy(&out.stdout);
306
307 // Singular "blocker", no "blockers".
308 assert!(stdout.contains("blocker"));
309 assert!(stdout.contains(&blocker[blocker.len() - 7..]));
310 // Should not contain [closed] or [all closed].
311 assert!(!stdout.contains("[closed]"));
312}