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(["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}