cli_log.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(["project", "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(["project", "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}