cli_update.rs

  1use assert_cmd::cargo::cargo_bin_cmd;
  2use predicates::prelude::*;
  3use tempfile::TempDir;
  4
  5fn td(home: &TempDir) -> assert_cmd::Command {
  6    let mut cmd = cargo_bin_cmd!("td");
  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
 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
 31fn get_task_json(dir: &TempDir, id: &str) -> serde_json::Value {
 32    let out = td(dir)
 33        .args(["--json", "show", id])
 34        .current_dir(dir)
 35        .output()
 36        .unwrap();
 37    serde_json::from_slice(&out.stdout).unwrap()
 38}
 39
 40// ── update ───────────────────────────────────────────────────────────
 41
 42#[test]
 43fn update_changes_status() {
 44    let tmp = init_tmp();
 45    let id = create_task(&tmp, "In progress");
 46
 47    td(&tmp)
 48        .args(["update", &id, "-s", "in_progress"])
 49        .current_dir(&tmp)
 50        .assert()
 51        .success()
 52        .stdout(predicate::str::contains("updated"));
 53
 54    let t = get_task_json(&tmp, &id);
 55    assert_eq!(t["status"].as_str().unwrap(), "in_progress");
 56}
 57
 58#[test]
 59fn update_changes_priority() {
 60    let tmp = init_tmp();
 61    let id = create_task(&tmp, "Reprioritise");
 62
 63    td(&tmp)
 64        .args(["update", &id, "-p", "high"])
 65        .current_dir(&tmp)
 66        .assert()
 67        .success();
 68
 69    let t = get_task_json(&tmp, &id);
 70    assert_eq!(t["priority"].as_str().unwrap(), "high");
 71}
 72
 73#[test]
 74fn update_changes_title() {
 75    let tmp = init_tmp();
 76    let id = create_task(&tmp, "Old title");
 77
 78    td(&tmp)
 79        .args(["update", &id, "-t", "New title"])
 80        .current_dir(&tmp)
 81        .assert()
 82        .success();
 83
 84    let t = get_task_json(&tmp, &id);
 85    assert_eq!(t["title"].as_str().unwrap(), "New title");
 86}
 87
 88#[test]
 89fn update_changes_description() {
 90    let tmp = init_tmp();
 91    let id = create_task(&tmp, "Describe me");
 92
 93    td(&tmp)
 94        .args(["update", &id, "-d", "Now with details"])
 95        .current_dir(&tmp)
 96        .assert()
 97        .success();
 98
 99    let t = get_task_json(&tmp, &id);
100    assert_eq!(t["description"].as_str().unwrap(), "Now with details");
101}
102
103#[test]
104fn update_json_returns_task() {
105    let tmp = init_tmp();
106    let id = create_task(&tmp, "JSON update");
107
108    let out = td(&tmp)
109        .args(["--json", "update", &id, "-p", "high"])
110        .current_dir(&tmp)
111        .output()
112        .unwrap();
113    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
114    assert_eq!(v["priority"].as_str().unwrap(), "high");
115}
116
117#[test]
118fn update_changes_effort() {
119    let tmp = init_tmp();
120    let id = create_task(&tmp, "Re-estimate");
121
122    td(&tmp)
123        .args(["update", &id, "-e", "high"])
124        .current_dir(&tmp)
125        .assert()
126        .success();
127
128    let t = get_task_json(&tmp, &id);
129    assert_eq!(t["effort"].as_str().unwrap(), "high");
130}
131
132#[test]
133fn update_changes_task_type() {
134    let tmp = init_tmp();
135    let id = create_task(&tmp, "Reclassify me");
136
137    // Default type is "task"
138    let t = get_task_json(&tmp, &id);
139    assert_eq!(t["type"].as_str().unwrap(), "task");
140
141    td(&tmp)
142        .args(["update", &id, "--type", "bug"])
143        .current_dir(&tmp)
144        .assert()
145        .success();
146
147    let t = get_task_json(&tmp, &id);
148    assert_eq!(t["type"].as_str().unwrap(), "bug");
149}
150
151// ── done ─────────────────────────────────────────────────────────────
152
153#[test]
154fn done_closes_task() {
155    let tmp = init_tmp();
156    let id = create_task(&tmp, "Close me");
157
158    td(&tmp)
159        .args(["done", &id])
160        .current_dir(&tmp)
161        .assert()
162        .success()
163        .stdout(predicate::str::contains("closed"));
164
165    let t = get_task_json(&tmp, &id);
166    assert_eq!(t["status"].as_str().unwrap(), "closed");
167}
168
169#[test]
170fn done_closes_multiple_tasks() {
171    let tmp = init_tmp();
172    let id1 = create_task(&tmp, "First");
173    let id2 = create_task(&tmp, "Second");
174
175    td(&tmp)
176        .args(["done", &id1, &id2])
177        .current_dir(&tmp)
178        .assert()
179        .success();
180
181    assert_eq!(get_task_json(&tmp, &id1)["status"], "closed");
182    assert_eq!(get_task_json(&tmp, &id2)["status"], "closed");
183}
184
185// ── reopen ───────────────────────────────────────────────────────────
186
187#[test]
188fn reopen_reopens_closed_task() {
189    let tmp = init_tmp();
190    let id = create_task(&tmp, "Reopen me");
191
192    td(&tmp)
193        .args(["done", &id])
194        .current_dir(&tmp)
195        .assert()
196        .success();
197    assert_eq!(get_task_json(&tmp, &id)["status"], "closed");
198
199    td(&tmp)
200        .args(["reopen", &id])
201        .current_dir(&tmp)
202        .assert()
203        .success()
204        .stdout(predicate::str::contains("reopened"));
205
206    assert_eq!(get_task_json(&tmp, &id)["status"], "open");
207}
208
209// ── editor fallback ───────────────────────────────────────────────────────────
210
211#[test]
212fn update_via_editor_changes_title_and_desc() {
213    // Bare `td update <id>` with TD_FORCE_EDITOR should open the editor
214    // pre-populated and apply whatever the fake editor writes back.
215    let tmp = init_tmp();
216    let id = create_task(&tmp, "Original title");
217
218    let fake_editor = "sh -c 'printf \"New title\\nNew description\" > \"$1\"' sh";
219
220    td(&tmp)
221        .args(["update", &id])
222        .env("TD_FORCE_EDITOR", fake_editor)
223        .current_dir(&tmp)
224        .assert()
225        .success();
226
227    let t = get_task_json(&tmp, &id);
228    assert_eq!(t["title"].as_str().unwrap(), "New title");
229    assert_eq!(t["description"].as_str().unwrap(), "New description");
230}
231
232#[test]
233fn update_via_editor_aborts_on_empty_file() {
234    // If the fake editor leaves only comments, the update should be aborted.
235    let tmp = init_tmp();
236    let id = create_task(&tmp, "Stays the same");
237
238    let fake_editor = "sh -c 'printf \"TD: comment only\\n\" > \"$1\"' sh";
239
240    td(&tmp)
241        .args(["update", &id])
242        .env("TD_FORCE_EDITOR", fake_editor)
243        .current_dir(&tmp)
244        .assert()
245        .failure()
246        .stderr(predicate::str::contains("aborted"));
247
248    // Title must be unchanged.
249    let t = get_task_json(&tmp, &id);
250    assert_eq!(t["title"].as_str().unwrap(), "Stays the same");
251}
252
253#[test]
254fn update_via_editor_preserves_existing_content_as_template() {
255    // The editor should be pre-populated with the task's current title and
256    // description, so the user can edit rather than retype from scratch.
257    let tmp = init_tmp();
258    let id = create_task(&tmp, "Existing title");
259
260    // Set description first.
261    td(&tmp)
262        .args(["update", &id, "-d", "Existing description"])
263        .current_dir(&tmp)
264        .assert()
265        .success();
266
267    // Fake editor: grep out the first non-comment, non-blank line (the title),
268    // then write "title: <that line>" so we can assert it was pre-populated.
269    let fake_editor = concat!(
270        "sh -c '",
271        r#"title=$(grep -v "^TD: " "$1" | grep -v "^[[:space:]]*$" | head -1); "#,
272        r#"printf "title: %s" "$title" > "$1""#,
273        "' sh"
274    );
275
276    td(&tmp)
277        .args(["update", &id])
278        .env("TD_FORCE_EDITOR", fake_editor)
279        .current_dir(&tmp)
280        .assert()
281        .success();
282
283    let t = get_task_json(&tmp, &id);
284    // The fake editor wrote the existing title back with a prefix, proving it
285    // received the pre-populated template.
286    assert_eq!(t["title"].as_str().unwrap(), "title: Existing title");
287}