cli_io.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 export_produces_jsonl() {
 27    let tmp = init_tmp();
 28    create_task(&tmp, "First");
 29    create_task(&tmp, "Second");
 30
 31    let out = td().arg("export").current_dir(&tmp).output().unwrap();
 32    let stdout = String::from_utf8(out.stdout).unwrap();
 33    let lines: Vec<&str> = stdout.lines().collect();
 34    assert_eq!(lines.len(), 2, "expected 2 JSONL lines, got: {stdout}");
 35
 36    // Each line should be valid JSON with an id field.
 37    for line in &lines {
 38        let v: serde_json::Value = serde_json::from_str(line).unwrap();
 39        assert!(v["id"].is_string());
 40    }
 41}
 42
 43#[test]
 44fn export_includes_labels_and_blockers() {
 45    let tmp = init_tmp();
 46    td().args(["create", "With labels", "-l", "bug"])
 47        .current_dir(&tmp)
 48        .assert()
 49        .success();
 50
 51    let out = td().arg("export").current_dir(&tmp).output().unwrap();
 52    let line = String::from_utf8(out.stdout).unwrap();
 53    let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
 54    assert!(v["labels"].is_array());
 55    assert!(v["blockers"].is_array());
 56}
 57
 58#[test]
 59fn import_round_trips_with_export() {
 60    let tmp = init_tmp();
 61    create_task(&tmp, "Alpha");
 62
 63    td().args(["create", "Bravo", "-l", "important"])
 64        .current_dir(&tmp)
 65        .assert()
 66        .success();
 67
 68    // Export.
 69    let export_out = td().arg("export").current_dir(&tmp).output().unwrap();
 70    let exported = String::from_utf8(export_out.stdout).unwrap();
 71
 72    // Write to a file.
 73    let export_file = tmp.path().join("backup.jsonl");
 74    std::fs::write(&export_file, &exported).unwrap();
 75
 76    // Create a fresh directory, init, import.
 77    let tmp2 = TempDir::new().unwrap();
 78    td().arg("init").current_dir(&tmp2).assert().success();
 79
 80    td().args(["import", export_file.to_str().unwrap()])
 81        .current_dir(&tmp2)
 82        .assert()
 83        .success()
 84        .stderr(predicate::str::contains("import complete"));
 85
 86    // Verify tasks exist in the new database.
 87    let out = td()
 88        .args(["--json", "list"])
 89        .current_dir(&tmp2)
 90        .output()
 91        .unwrap();
 92    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
 93    let titles: Vec<&str> = v
 94        .as_array()
 95        .unwrap()
 96        .iter()
 97        .map(|t| t["title"].as_str().unwrap())
 98        .collect();
 99    assert!(titles.contains(&"Alpha"));
100    assert!(titles.contains(&"Bravo"));
101
102    // Verify labels survived.
103    let bravo = v
104        .as_array()
105        .unwrap()
106        .iter()
107        .find(|t| t["title"] == "Bravo")
108        .unwrap();
109    let labels = bravo["labels"].as_array().unwrap();
110    assert!(labels.contains(&serde_json::Value::String("important".into())));
111}
112
113#[test]
114fn export_import_preserves_effort() {
115    let tmp = init_tmp();
116
117    td().args(["create", "High effort", "-e", "high"])
118        .current_dir(&tmp)
119        .assert()
120        .success();
121
122    // Export.
123    let out = td().arg("export").current_dir(&tmp).output().unwrap();
124    let exported = String::from_utf8(out.stdout).unwrap();
125
126    // Verify effort is in the JSONL.
127    let v: serde_json::Value = serde_json::from_str(exported.trim()).unwrap();
128    assert_eq!(v["effort"].as_i64().unwrap(), 3);
129
130    // Round-trip into a fresh database.
131    let export_file = tmp.path().join("effort.jsonl");
132    std::fs::write(&export_file, &exported).unwrap();
133
134    let tmp2 = TempDir::new().unwrap();
135    td().arg("init").current_dir(&tmp2).assert().success();
136    td().args(["import", export_file.to_str().unwrap()])
137        .current_dir(&tmp2)
138        .assert()
139        .success();
140
141    let out2 = td()
142        .args(["--json", "list"])
143        .current_dir(&tmp2)
144        .output()
145        .unwrap();
146    let v2: serde_json::Value = serde_json::from_slice(&out2.stdout).unwrap();
147    assert_eq!(v2[0]["effort"].as_i64().unwrap(), 3);
148}