cli_list_show.rs

  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
 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}