cli_list_show.rs

  1use assert_cmd::cargo::cargo_bin_cmd;
  2use predicates::prelude::*;
  3use tempfile::TempDir;
  4
  5fn td(home: &TempDir) -> assert_cmd::Command {
  6    let mut cmd = cargo_bin_cmd!("td");
  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_hides_closed_by_default() {
 82    let tmp = init_tmp();
 83    let id = create_task(&tmp, "Will close");
 84
 85    td(&tmp)
 86        .args(["done", &id])
 87        .current_dir(&tmp)
 88        .assert()
 89        .success();
 90
 91    // Default list should not show the closed task.
 92    let out = td(&tmp)
 93        .args(["--json", "list"])
 94        .current_dir(&tmp)
 95        .output()
 96        .unwrap();
 97    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
 98    assert_eq!(v.as_array().unwrap().len(), 0);
 99
100    // --all should include it.
101    let out = td(&tmp)
102        .args(["--json", "list", "--all"])
103        .current_dir(&tmp)
104        .output()
105        .unwrap();
106    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
107    assert_eq!(v.as_array().unwrap().len(), 1);
108    assert_eq!(v[0]["title"].as_str().unwrap(), "Will close");
109}
110
111#[test]
112fn list_filter_by_priority() {
113    let tmp = init_tmp();
114
115    td(&tmp)
116        .args(["create", "Low prio", "-p", "low"])
117        .current_dir(&tmp)
118        .assert()
119        .success();
120    td(&tmp)
121        .args(["create", "High prio", "-p", "high"])
122        .current_dir(&tmp)
123        .assert()
124        .success();
125
126    let out = td(&tmp)
127        .args(["--json", "list", "-p", "high"])
128        .current_dir(&tmp)
129        .output()
130        .unwrap();
131    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
132    let tasks = v.as_array().unwrap();
133    assert_eq!(tasks.len(), 1);
134    assert_eq!(tasks[0]["title"].as_str().unwrap(), "High prio");
135}
136
137#[test]
138fn list_filter_by_type() {
139    let tmp = init_tmp();
140
141    td(&tmp)
142        .args(["create", "A task", "--type", "task"])
143        .current_dir(&tmp)
144        .assert()
145        .success();
146    td(&tmp)
147        .args(["create", "A bug", "--type", "bug"])
148        .current_dir(&tmp)
149        .assert()
150        .success();
151
152    let out = td(&tmp)
153        .args(["--json", "list", "--type", "bug"])
154        .current_dir(&tmp)
155        .output()
156        .unwrap();
157    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
158    let tasks = v.as_array().unwrap();
159    assert_eq!(tasks.len(), 1);
160    assert_eq!(tasks[0]["title"].as_str().unwrap(), "A bug");
161}
162
163#[test]
164fn list_filter_by_label() {
165    let tmp = init_tmp();
166
167    td(&tmp)
168        .args(["create", "Tagged", "-l", "urgent"])
169        .current_dir(&tmp)
170        .assert()
171        .success();
172    td(&tmp)
173        .args(["create", "Untagged"])
174        .current_dir(&tmp)
175        .assert()
176        .success();
177
178    let out = td(&tmp)
179        .args(["--json", "list", "-l", "urgent"])
180        .current_dir(&tmp)
181        .output()
182        .unwrap();
183    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
184    let tasks = v.as_array().unwrap();
185    assert_eq!(tasks.len(), 1);
186    assert_eq!(tasks[0]["title"].as_str().unwrap(), "Tagged");
187}
188
189#[test]
190fn list_filter_by_effort() {
191    let tmp = init_tmp();
192
193    td(&tmp)
194        .args(["create", "Easy", "-e", "low"])
195        .current_dir(&tmp)
196        .assert()
197        .success();
198    td(&tmp)
199        .args(["create", "Hard", "-e", "high"])
200        .current_dir(&tmp)
201        .assert()
202        .success();
203
204    let out = td(&tmp)
205        .args(["--json", "list", "-e", "low"])
206        .current_dir(&tmp)
207        .output()
208        .unwrap();
209    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
210    let tasks = v.as_array().unwrap();
211    assert_eq!(tasks.len(), 1);
212    assert_eq!(tasks[0]["title"].as_str().unwrap(), "Easy");
213}
214
215// ── show ─────────────────────────────────────────────────────────────
216
217#[test]
218fn show_displays_task() {
219    let tmp = init_tmp();
220    let id = create_task(&tmp, "Details here");
221
222    td(&tmp)
223        .args(["show", &id])
224        .current_dir(&tmp)
225        .assert()
226        .success()
227        .stdout(predicate::str::contains("Details here"))
228        .stdout(predicate::str::contains(&id[id.len() - 7..]));
229}
230
231#[test]
232fn show_json_includes_labels_and_blockers() {
233    let tmp = init_tmp();
234
235    td(&tmp)
236        .args(["create", "With labels", "-l", "bug,ui"])
237        .current_dir(&tmp)
238        .assert()
239        .success();
240
241    // Get the id via list.
242    let out = td(&tmp)
243        .args(["--json", "list"])
244        .current_dir(&tmp)
245        .output()
246        .unwrap();
247    let list: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
248    let id = list[0]["id"].as_str().unwrap();
249
250    let out = td(&tmp)
251        .args(["--json", "show", id])
252        .current_dir(&tmp)
253        .output()
254        .unwrap();
255    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
256    assert_eq!(v["title"].as_str().unwrap(), "With labels");
257
258    let labels = v["labels"].as_array().unwrap();
259    assert!(labels.contains(&serde_json::Value::String("bug".into())));
260    assert!(labels.contains(&serde_json::Value::String("ui".into())));
261
262    // Blockers should be present (even if empty).
263    assert!(v["blockers"].is_array());
264}
265
266#[test]
267fn show_nonexistent_task_fails() {
268    let tmp = init_tmp();
269
270    td(&tmp)
271        .args(["show", "td-nope"])
272        .current_dir(&tmp)
273        .assert()
274        .failure()
275        .stderr(predicate::str::contains("not found"));
276}
277
278#[test]
279fn show_annotates_closed_blockers() {
280    let tmp = init_tmp();
281    let task = create_task(&tmp, "Blocked task");
282    let open_blocker = create_task(&tmp, "Still open");
283    let closed_blocker = create_task(&tmp, "Will close");
284
285    td(&tmp)
286        .args(["dep", "add", &task, &open_blocker])
287        .current_dir(&tmp)
288        .assert()
289        .success();
290    td(&tmp)
291        .args(["dep", "add", &task, &closed_blocker])
292        .current_dir(&tmp)
293        .assert()
294        .success();
295    td(&tmp)
296        .args(["done", &closed_blocker])
297        .current_dir(&tmp)
298        .assert()
299        .success();
300
301    // Plural label, open blocker bare, closed one annotated.
302    td(&tmp)
303        .args(["show", &task])
304        .current_dir(&tmp)
305        .assert()
306        .success()
307        .stdout(predicate::str::contains("blockers"))
308        .stdout(predicate::str::contains(
309            &open_blocker[open_blocker.len() - 7..],
310        ))
311        .stdout(predicate::str::contains(&format!(
312            "{} [closed]",
313            &closed_blocker[closed_blocker.len() - 7..]
314        )));
315}
316
317#[test]
318fn show_all_closed_blockers_prefixed() {
319    let tmp = init_tmp();
320    let task = create_task(&tmp, "Was blocked");
321    let blocker = create_task(&tmp, "Done now");
322
323    td(&tmp)
324        .args(["dep", "add", &task, &blocker])
325        .current_dir(&tmp)
326        .assert()
327        .success();
328    td(&tmp)
329        .args(["done", &blocker])
330        .current_dir(&tmp)
331        .assert()
332        .success();
333
334    // Singular label, [all closed] prefix, no redundant [closed] on IDs.
335    let out = td(&tmp)
336        .args(["show", &task])
337        .current_dir(&tmp)
338        .output()
339        .unwrap();
340    let stdout = String::from_utf8_lossy(&out.stdout);
341    assert!(stdout.contains("blocker"));
342    assert!(stdout.contains("[all closed]"));
343    assert!(stdout.contains(&blocker[blocker.len() - 7..]));
344    // When all are closed, individual IDs should NOT have [closed] appended.
345    assert!(!stdout.contains("[closed]"));
346}
347
348#[test]
349fn show_single_open_blocker_singular_label() {
350    let tmp = init_tmp();
351    let task = create_task(&tmp, "Blocked");
352    let blocker = create_task(&tmp, "Blocking");
353
354    td(&tmp)
355        .args(["dep", "add", &task, &blocker])
356        .current_dir(&tmp)
357        .assert()
358        .success();
359
360    let out = td(&tmp)
361        .args(["show", &task])
362        .current_dir(&tmp)
363        .output()
364        .unwrap();
365    let stdout = String::from_utf8_lossy(&out.stdout);
366
367    // Singular "blocker", no "blockers".
368    assert!(stdout.contains("blocker"));
369    assert!(stdout.contains(&blocker[blocker.len() - 7..]));
370    // Should not contain [closed] or [all closed].
371    assert!(!stdout.contains("[closed]"));
372}