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