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