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