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
15fn create_task(dir: &TempDir, title: &str) -> String {
16 let out = td()
17 .args(["--json", "create", title])
18 .current_dir(dir)
19 .output()
20 .unwrap();
21 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
22 v["id"].as_str().unwrap().to_string()
23}
24
25// ── search ───────────────────────────────────────────────────────────
26
27#[test]
28fn search_matches_title() {
29 let tmp = init_tmp();
30 create_task(&tmp, "Fix login page");
31 create_task(&tmp, "Update docs");
32
33 td().args(["search", "login"])
34 .current_dir(&tmp)
35 .assert()
36 .success()
37 .stdout(predicate::str::contains("Fix login page"));
38}
39
40#[test]
41fn search_matches_description() {
42 let tmp = init_tmp();
43
44 td().args(["create", "Vague title", "-d", "The frobnicator is broken"])
45 .current_dir(&tmp)
46 .assert()
47 .success();
48
49 td().args(["search", "frobnicator"])
50 .current_dir(&tmp)
51 .assert()
52 .success()
53 .stdout(predicate::str::contains("Vague title"));
54}
55
56#[test]
57fn search_json_returns_array() {
58 let tmp = init_tmp();
59 create_task(&tmp, "Needle in haystack");
60
61 let out = td()
62 .args(["--json", "search", "Needle"])
63 .current_dir(&tmp)
64 .output()
65 .unwrap();
66 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
67 assert!(v.is_array());
68 assert_eq!(v[0]["title"].as_str().unwrap(), "Needle in haystack");
69}
70
71// ── ready ────────────────────────────────────────────────────────────
72
73#[test]
74fn ready_excludes_blocked_tasks() {
75 let tmp = init_tmp();
76 let a = create_task(&tmp, "Ready task");
77 let b = create_task(&tmp, "Blocked task");
78 let c = create_task(&tmp, "Blocker task");
79
80 td().args(["dep", "add", &b, &c])
81 .current_dir(&tmp)
82 .assert()
83 .success();
84
85 let out = td()
86 .args(["--json", "ready"])
87 .current_dir(&tmp)
88 .output()
89 .unwrap();
90 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
91 let titles: Vec<&str> = v
92 .as_array()
93 .unwrap()
94 .iter()
95 .map(|t| t["title"].as_str().unwrap())
96 .collect();
97
98 assert!(titles.contains(&"Ready task"));
99 assert!(titles.contains(&"Blocker task"));
100 assert!(!titles.contains(&"Blocked task"));
101
102 // Close the blocker — now the blocked task should become ready.
103 td().args(["done", &c]).current_dir(&tmp).assert().success();
104
105 let out = td()
106 .args(["--json", "ready"])
107 .current_dir(&tmp)
108 .output()
109 .unwrap();
110 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
111 let titles: Vec<&str> = v
112 .as_array()
113 .unwrap()
114 .iter()
115 .map(|t| t["title"].as_str().unwrap())
116 .collect();
117 assert!(titles.contains(&"Blocked task"));
118 // a is still ready
119 assert!(titles.contains(&"Ready task"));
120}
121
122// ── stats ────────────────────────────────────────────────────────────
123
124#[test]
125fn stats_counts_tasks() {
126 let tmp = init_tmp();
127 let id = create_task(&tmp, "Open one");
128 create_task(&tmp, "Open two");
129 td().args(["done", &id])
130 .current_dir(&tmp)
131 .assert()
132 .success();
133
134 let out = td().args(["stats"]).current_dir(&tmp).output().unwrap();
135 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
136 assert_eq!(v["total"].as_i64().unwrap(), 2);
137 assert_eq!(v["open"].as_i64().unwrap(), 1);
138 assert_eq!(v["closed"].as_i64().unwrap(), 1);
139}
140
141// ── compact ──────────────────────────────────────────────────────────
142
143#[test]
144fn compact_succeeds() {
145 let tmp = init_tmp();
146 create_task(&tmp, "Anything");
147
148 td().arg("compact")
149 .current_dir(&tmp)
150 .assert()
151 .success()
152 .stderr(predicate::str::contains("done"));
153}