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
25fn get_task_json(dir: &TempDir, id: &str) -> serde_json::Value {
26 let out = td()
27 .args(["--json", "show", id])
28 .current_dir(dir)
29 .output()
30 .unwrap();
31 serde_json::from_slice(&out.stdout).unwrap()
32}
33
34#[test]
35fn dep_add_creates_blocker() {
36 let tmp = init_tmp();
37 let a = create_task(&tmp, "Blocked task");
38 let b = create_task(&tmp, "Blocker");
39
40 td().args(["dep", "add", &a, &b])
41 .current_dir(&tmp)
42 .assert()
43 .success()
44 .stdout(predicate::str::contains("blocked by"));
45
46 let t = get_task_json(&tmp, &a);
47 let blockers = t["blockers"].as_array().unwrap();
48 assert!(blockers.contains(&serde_json::Value::String(b)));
49}
50
51#[test]
52fn dep_rm_removes_blocker() {
53 let tmp = init_tmp();
54 let a = create_task(&tmp, "Was blocked");
55 let b = create_task(&tmp, "Was blocker");
56
57 td().args(["dep", "add", &a, &b])
58 .current_dir(&tmp)
59 .assert()
60 .success();
61 td().args(["dep", "rm", &a, &b])
62 .current_dir(&tmp)
63 .assert()
64 .success();
65
66 let t = get_task_json(&tmp, &a);
67 let blockers = t["blockers"].as_array().unwrap();
68 assert!(blockers.is_empty());
69}
70
71#[test]
72fn dep_tree_shows_children() {
73 let tmp = init_tmp();
74 let parent = create_task(&tmp, "Parent");
75
76 td().args(["create", "Child one", "--parent", &parent])
77 .current_dir(&tmp)
78 .assert()
79 .success();
80 td().args(["create", "Child two", "--parent", &parent])
81 .current_dir(&tmp)
82 .assert()
83 .success();
84
85 td().args(["dep", "tree", &parent])
86 .current_dir(&tmp)
87 .assert()
88 .success()
89 .stdout(predicate::str::contains(&parent))
90 .stdout(predicate::str::contains(".1"))
91 .stdout(predicate::str::contains(".2"));
92}
93
94#[test]
95fn dep_add_rejects_self_cycle() {
96 let tmp = init_tmp();
97 let a = create_task(&tmp, "Self-referential");
98
99 td().args(["dep", "add", &a, &a])
100 .current_dir(&tmp)
101 .assert()
102 .failure()
103 .stderr(predicate::str::contains("cycle"));
104}
105
106#[test]
107fn dep_add_rejects_direct_cycle() {
108 let tmp = init_tmp();
109 let a = create_task(&tmp, "Task A");
110 let b = create_task(&tmp, "Task B");
111
112 // A blocked by B
113 td().args(["dep", "add", &a, &b])
114 .current_dir(&tmp)
115 .assert()
116 .success();
117
118 // B blocked by A would create A → B → A
119 td().args(["dep", "add", &b, &a])
120 .current_dir(&tmp)
121 .assert()
122 .failure()
123 .stderr(predicate::str::contains("cycle"));
124}
125
126#[test]
127fn dep_add_rejects_transitive_cycle() {
128 let tmp = init_tmp();
129 let a = create_task(&tmp, "Task A");
130 let b = create_task(&tmp, "Task B");
131 let c = create_task(&tmp, "Task C");
132
133 // A blocked by B, B blocked by C
134 td().args(["dep", "add", &a, &b])
135 .current_dir(&tmp)
136 .assert()
137 .success();
138 td().args(["dep", "add", &b, &c])
139 .current_dir(&tmp)
140 .assert()
141 .success();
142
143 // C blocked by A would create A → B → C → A
144 td().args(["dep", "add", &c, &a])
145 .current_dir(&tmp)
146 .assert()
147 .failure()
148 .stderr(predicate::str::contains("cycle"));
149}
150
151#[test]
152fn dep_add_allows_diamond_without_cycle() {
153 let tmp = init_tmp();
154 let a = create_task(&tmp, "Task A");
155 let b = create_task(&tmp, "Task B");
156 let c = create_task(&tmp, "Task C");
157 let d = create_task(&tmp, "Task D");
158
159 // Diamond: D blocked by B and C, both blocked by A
160 td().args(["dep", "add", &d, &b])
161 .current_dir(&tmp)
162 .assert()
163 .success();
164 td().args(["dep", "add", &d, &c])
165 .current_dir(&tmp)
166 .assert()
167 .success();
168 td().args(["dep", "add", &b, &a])
169 .current_dir(&tmp)
170 .assert()
171 .success();
172 td().args(["dep", "add", &c, &a])
173 .current_dir(&tmp)
174 .assert()
175 .success();
176
177 // Verify all edges exist — no false cycle detection
178 let t = get_task_json(&tmp, &d);
179 let blockers = t["blockers"].as_array().unwrap();
180 assert_eq!(blockers.len(), 2);
181}