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
21fn create_task(dir: &TempDir, title: &str) -> String {
22 let out = td(dir)
23 .args(["--json", "create", title])
24 .current_dir(dir)
25 .output()
26 .unwrap();
27 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
28 v["id"].as_str().unwrap().to_string()
29}
30
31// ── search ───────────────────────────────────────────────────────────
32
33#[test]
34fn search_matches_title() {
35 let tmp = init_tmp();
36 create_task(&tmp, "Fix login page");
37 create_task(&tmp, "Update docs");
38
39 td(&tmp)
40 .args(["search", "login"])
41 .current_dir(&tmp)
42 .assert()
43 .success()
44 .stdout(predicate::str::contains("Fix login page"));
45}
46
47#[test]
48fn search_matches_description() {
49 let tmp = init_tmp();
50
51 td(&tmp)
52 .args(["create", "Vague title", "-d", "The frobnicator is broken"])
53 .current_dir(&tmp)
54 .assert()
55 .success();
56
57 td(&tmp)
58 .args(["search", "frobnicator"])
59 .current_dir(&tmp)
60 .assert()
61 .success()
62 .stdout(predicate::str::contains("Vague title"));
63}
64
65#[test]
66fn search_json_returns_array() {
67 let tmp = init_tmp();
68 create_task(&tmp, "Needle in haystack");
69
70 let out = td(&tmp)
71 .args(["--json", "search", "Needle"])
72 .current_dir(&tmp)
73 .output()
74 .unwrap();
75 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
76 assert!(v.is_array());
77 assert_eq!(v[0]["title"].as_str().unwrap(), "Needle in haystack");
78}
79
80// ── ready ────────────────────────────────────────────────────────────
81
82#[test]
83fn ready_excludes_blocked_tasks() {
84 let tmp = init_tmp();
85 let _a = create_task(&tmp, "Ready task");
86 let b = create_task(&tmp, "Blocked task");
87 let c = create_task(&tmp, "Blocker task");
88
89 td(&tmp)
90 .args(["dep", "add", &b, &c])
91 .current_dir(&tmp)
92 .assert()
93 .success();
94
95 let out = td(&tmp)
96 .args(["--json", "ready"])
97 .current_dir(&tmp)
98 .output()
99 .unwrap();
100 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
101 let titles: Vec<&str> = v
102 .as_array()
103 .unwrap()
104 .iter()
105 .map(|t| t["title"].as_str().unwrap())
106 .collect();
107
108 assert!(titles.contains(&"Ready task"));
109 assert!(titles.contains(&"Blocker task"));
110 assert!(!titles.contains(&"Blocked task"));
111
112 // Close the blocker — now the blocked task should become ready.
113 td(&tmp)
114 .args(["done", &c])
115 .current_dir(&tmp)
116 .assert()
117 .success();
118
119 let out = td(&tmp)
120 .args(["--json", "ready"])
121 .current_dir(&tmp)
122 .output()
123 .unwrap();
124 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
125 let titles: Vec<&str> = v
126 .as_array()
127 .unwrap()
128 .iter()
129 .map(|t| t["title"].as_str().unwrap())
130 .collect();
131 assert!(titles.contains(&"Blocked task"));
132 // a is still ready
133 assert!(titles.contains(&"Ready task"));
134}
135
136// ── stats ────────────────────────────────────────────────────────────
137
138#[test]
139fn stats_counts_tasks() {
140 let tmp = init_tmp();
141 let id = create_task(&tmp, "Open one");
142 create_task(&tmp, "Open two");
143 td(&tmp)
144 .args(["done", &id])
145 .current_dir(&tmp)
146 .assert()
147 .success();
148
149 let out = td(&tmp).args(["stats"]).current_dir(&tmp).output().unwrap();
150 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
151 assert_eq!(v["total"].as_i64().unwrap(), 2);
152 assert_eq!(v["open"].as_i64().unwrap(), 1);
153 assert_eq!(v["closed"].as_i64().unwrap(), 1);
154}
155
156// ── tidy ─────────────────────────────────────────────────────────────
157
158#[test]
159fn tidy_succeeds() {
160 let tmp = init_tmp();
161 create_task(&tmp, "Anything");
162 create_task(&tmp, "Anything else");
163
164 let changes = tmp.path().join(".local/share/td/projects/main/changes");
165 let count_before = std::fs::read_dir(&changes)
166 .unwrap()
167 .filter_map(Result::ok)
168 .map(|entry| entry.path())
169 .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("loro"))
170 .count();
171 assert!(count_before > 0);
172
173 td(&tmp)
174 .arg("tidy")
175 .current_dir(&tmp)
176 .assert()
177 .success()
178 .stderr(predicate::str::contains("compacting deltas"))
179 .stderr(predicate::str::contains("removed"));
180
181 let count_after = std::fs::read_dir(&changes)
182 .unwrap()
183 .filter_map(Result::ok)
184 .map(|entry| entry.path())
185 .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("loro"))
186 .count();
187 assert_eq!(count_after, 0);
188}