cli_log.rs

  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}