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#[test]
32fn log_human_reports_task_id() {
33 let tmp = init_tmp();
34 let id = create_task(&tmp, "Write docs");
35
36 td(&tmp)
37 .args(["log", &id, "Drafted command docs"])
38 .current_dir(&tmp)
39 .assert()
40 .success()
41 .stdout(predicate::str::contains(format!(
42 "logged to td-{}",
43 &id[id.len() - 7..]
44 )));
45}
46
47#[test]
48fn log_json_emits_created_log_entry() {
49 let tmp = init_tmp();
50 let id = create_task(&tmp, "Investigate timeout");
51
52 let out = td(&tmp)
53 .args(["--json", "log", &id, "Collected stack traces"])
54 .current_dir(&tmp)
55 .output()
56 .unwrap();
57 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
58
59 assert!(v["id"].is_string());
60 assert_eq!(v["message"].as_str().unwrap(), "Collected stack traces");
61 assert!(v["timestamp"].as_str().unwrap().ends_with('Z'));
62}
63
64#[test]
65fn log_nonexistent_task_fails() {
66 let tmp = init_tmp();
67
68 td(&tmp)
69 .args(["log", "td-nope", "No task"])
70 .current_dir(&tmp)
71 .assert()
72 .failure()
73 .stderr(predicate::str::contains("task 'nope' not found"));
74}
75
76#[test]
77fn show_human_displays_logs_chronologically() {
78 let tmp = init_tmp();
79 let id = create_task(&tmp, "Investigate auth issue");
80
81 td(&tmp)
82 .args(["log", &id, "First note"])
83 .current_dir(&tmp)
84 .assert()
85 .success();
86 td(&tmp)
87 .args(["log", &id, "Second note"])
88 .current_dir(&tmp)
89 .assert()
90 .success();
91
92 let out = td(&tmp)
93 .args(["show", &id])
94 .current_dir(&tmp)
95 .output()
96 .unwrap();
97 let stdout = String::from_utf8(out.stdout).unwrap();
98 let first = stdout.find("First note").unwrap();
99 let second = stdout.find("Second note").unwrap();
100
101 assert!(stdout.contains("--- log ---"));
102 assert!(first < second, "expected logs in insertion order: {stdout}");
103}
104
105#[test]
106fn show_json_includes_logs_array() {
107 let tmp = init_tmp();
108 let id = create_task(&tmp, "Implement parser");
109
110 td(&tmp)
111 .args(["log", &id, "Mapped grammar rules"])
112 .current_dir(&tmp)
113 .assert()
114 .success();
115
116 let out = td(&tmp)
117 .args(["--json", "show", &id])
118 .current_dir(&tmp)
119 .output()
120 .unwrap();
121 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
122
123 let logs = v["logs"].as_array().unwrap();
124 assert_eq!(logs.len(), 1);
125 assert_eq!(logs[0]["message"].as_str().unwrap(), "Mapped grammar rules");
126}
127
128#[test]
129fn multiple_log_entries_are_ordered() {
130 let tmp = init_tmp();
131 let id = create_task(&tmp, "Refactor planner");
132
133 for msg in ["step one", "step two", "step three"] {
134 td(&tmp)
135 .args(["log", &id, msg])
136 .current_dir(&tmp)
137 .assert()
138 .success();
139 }
140
141 let out = td(&tmp)
142 .args(["--json", "show", &id])
143 .current_dir(&tmp)
144 .output()
145 .unwrap();
146 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
147 let logs = v["logs"].as_array().unwrap();
148
149 assert_eq!(logs.len(), 3);
150 assert_eq!(logs[0]["message"].as_str().unwrap(), "step one");
151 assert_eq!(logs[1]["message"].as_str().unwrap(), "step two");
152 assert_eq!(logs[2]["message"].as_str().unwrap(), "step three");
153}
154
155#[test]
156fn export_import_round_trips_logs() {
157 let tmp = init_tmp();
158 let id = create_task(&tmp, "Port backend");
159 td(&tmp)
160 .args(["log", &id, "Measured baseline"])
161 .current_dir(&tmp)
162 .assert()
163 .success();
164 td(&tmp)
165 .args(["log", &id, "Applied optimization"])
166 .current_dir(&tmp)
167 .assert()
168 .success();
169
170 let export_out = td(&tmp).arg("export").current_dir(&tmp).output().unwrap();
171 let exported = String::from_utf8(export_out.stdout).unwrap();
172 let export_file = tmp.path().join("logs.jsonl");
173 std::fs::write(&export_file, &exported).unwrap();
174
175 let tmp2 = TempDir::new().unwrap();
176 td(&tmp2)
177 .args(["init", "mirror"])
178 .current_dir(&tmp2)
179 .assert()
180 .success();
181 td(&tmp2)
182 .args(["import", export_file.to_str().unwrap()])
183 .current_dir(&tmp2)
184 .assert()
185 .success();
186
187 let out = td(&tmp2)
188 .args(["--json", "show", &id])
189 .current_dir(&tmp2)
190 .output()
191 .unwrap();
192 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
193 let logs = v["logs"].as_array().unwrap();
194
195 assert_eq!(logs.len(), 2);
196 assert_eq!(logs[0]["message"].as_str().unwrap(), "Measured baseline");
197 assert_eq!(logs[1]["message"].as_str().unwrap(), "Applied optimization");
198}
199
200#[test]
201fn list_json_does_not_include_logs() {
202 let tmp = init_tmp();
203 let id = create_task(&tmp, "Keep list lean");
204 td(&tmp)
205 .args(["log", &id, "This should not surface in list"])
206 .current_dir(&tmp)
207 .assert()
208 .success();
209
210 let out = td(&tmp)
211 .args(["--json", "list"])
212 .current_dir(&tmp)
213 .output()
214 .unwrap();
215 let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
216
217 assert!(v[0].get("logs").is_none());
218}