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