cli_tidy.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
 21/// td tidy exists and reports what it did.
 22#[test]
 23fn tidy_reports_progress() {
 24    let tmp = init_tmp();
 25
 26    td(&tmp)
 27        .args(["create", "Task 1"])
 28        .current_dir(&tmp)
 29        .assert()
 30        .success();
 31    td(&tmp)
 32        .args(["create", "Task 2"])
 33        .current_dir(&tmp)
 34        .assert()
 35        .success();
 36
 37    td(&tmp)
 38        .arg("tidy")
 39        .current_dir(&tmp)
 40        .assert()
 41        .success()
 42        .stderr(predicate::str::contains("removed 2 delta file(s)"));
 43}
 44
 45/// After td tidy, the changes/ directory exists but contains no delta files.
 46#[test]
 47fn tidy_empties_changes_dir() {
 48    let tmp = init_tmp();
 49
 50    td(&tmp)
 51        .args(["create", "Task A"])
 52        .current_dir(&tmp)
 53        .assert()
 54        .success();
 55
 56    let project_dir = tmp.path().join(".local/share/td/projects/main");
 57    let changes_dir = project_dir.join("changes");
 58
 59    let before = std::fs::read_dir(&changes_dir).unwrap().count();
 60    assert!(before > 0, "deltas should exist before tidy");
 61
 62    td(&tmp).arg("tidy").current_dir(&tmp).assert().success();
 63
 64    // changes/ should still be there (ready for new deltas)…
 65    assert!(changes_dir.exists(), "changes/ must still exist after tidy");
 66    // …but empty.
 67    let after = std::fs::read_dir(&changes_dir).unwrap().count();
 68    assert_eq!(after, 0, "tidy should leave changes/ empty");
 69}
 70
 71/// All tasks created before td tidy are still visible afterwards.
 72#[test]
 73fn tidy_preserves_tasks() {
 74    let tmp = init_tmp();
 75
 76    td(&tmp)
 77        .args(["create", "Keep me"])
 78        .current_dir(&tmp)
 79        .assert()
 80        .success();
 81    td(&tmp)
 82        .args(["create", "Keep me too"])
 83        .current_dir(&tmp)
 84        .assert()
 85        .success();
 86
 87    td(&tmp).arg("tidy").current_dir(&tmp).assert().success();
 88
 89    td(&tmp)
 90        .arg("list")
 91        .current_dir(&tmp)
 92        .assert()
 93        .success()
 94        .stdout(predicate::str::contains("Keep me"))
 95        .stdout(predicate::str::contains("Keep me too"));
 96}
 97
 98/// Running td tidy twice in a row is idempotent: the second run has no deltas
 99/// to remove, succeeds, and reports 0 files removed.
100#[test]
101fn tidy_is_idempotent() {
102    let tmp = init_tmp();
103
104    td(&tmp)
105        .args(["create", "Some task"])
106        .current_dir(&tmp)
107        .assert()
108        .success();
109
110    td(&tmp).arg("tidy").current_dir(&tmp).assert().success();
111
112    td(&tmp)
113        .arg("tidy")
114        .current_dir(&tmp)
115        .assert()
116        .success()
117        .stderr(predicate::str::contains("removed 0 delta file(s)"));
118}
119
120/// Crash-recovery: if a previous tidy renamed changes/ to changes.compacting.X/
121/// but crashed before finishing, the next tidy should absorb those deltas into
122/// the snapshot and remove the orphaned directory.
123#[test]
124fn tidy_recovers_orphaned_compacting_dir() {
125    let tmp = init_tmp();
126
127    td(&tmp)
128        .args(["create", "Orphaned task"])
129        .current_dir(&tmp)
130        .assert()
131        .success();
132
133    let project_dir = tmp.path().join(".local/share/td/projects/main");
134    let changes_dir = project_dir.join("changes");
135
136    // Simulate a crash after phase 1: rename changes/ to changes.compacting.X/
137    // without creating a fresh changes/ or writing the new snapshot.
138    let compacting_dir = project_dir.join("changes.compacting.01JNFAKEULID0000000000000");
139    std::fs::rename(&changes_dir, &compacting_dir)
140        .expect("simulate crash: rename changes/ to compacting dir");
141
142    // No changes/ exists now — a subsequent write would create it via
143    // create_dir_all, but let's leave that to tidy to exercise recovery.
144
145    // tidy must succeed and preserve the orphaned task.
146    td(&tmp).arg("tidy").current_dir(&tmp).assert().success();
147
148    // The orphaned compacting dir must be gone.
149    assert!(
150        !compacting_dir.exists(),
151        "tidy should remove the orphaned changes.compacting.* dir"
152    );
153
154    // The task from the orphaned delta must still be accessible.
155    td(&tmp)
156        .arg("list")
157        .current_dir(&tmp)
158        .assert()
159        .success()
160        .stdout(predicate::str::contains("Orphaned task"));
161}
162
163/// td compact is gone; using it should produce an error.
164#[test]
165fn compact_command_removed() {
166    let tmp = init_tmp();
167
168    td(&tmp).arg("compact").current_dir(&tmp).assert().failure();
169}