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
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}