cli_create.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
 11/// Initialise a temp directory and return it.
 12fn init_tmp() -> TempDir {
 13    let tmp = TempDir::new().unwrap();
 14    td(&tmp)
 15        .args(["project", "init", "main"])
 16        .current_dir(&tmp)
 17        .assert()
 18        .success();
 19    tmp
 20}
 21
 22#[test]
 23fn create_prints_id_and_title() {
 24    let tmp = init_tmp();
 25
 26    td(&tmp)
 27        .args(["create", "My first task"])
 28        .current_dir(&tmp)
 29        .assert()
 30        .success()
 31        .stdout(predicate::str::contains("My first task"));
 32}
 33
 34#[test]
 35fn create_json_returns_task_object() {
 36    let tmp = init_tmp();
 37
 38    td(&tmp)
 39        .args(["--json", "create", "Buy milk"])
 40        .current_dir(&tmp)
 41        .assert()
 42        .success()
 43        .stdout(predicate::str::contains(r#""title":"Buy milk"#))
 44        .stdout(predicate::str::contains(r#""status":"open"#))
 45        .stdout(predicate::str::contains(r#""priority":"medium""#));
 46}
 47
 48#[test]
 49fn create_with_priority_and_type() {
 50    let tmp = init_tmp();
 51
 52    td(&tmp)
 53        .args(["--json", "create", "Urgent bug", "-p", "high", "-t", "bug"])
 54        .current_dir(&tmp)
 55        .assert()
 56        .success()
 57        .stdout(predicate::str::contains(r#""priority":"high""#))
 58        .stdout(predicate::str::contains(r#""type":"bug"#));
 59}
 60
 61#[test]
 62fn create_with_description() {
 63    let tmp = init_tmp();
 64
 65    td(&tmp)
 66        .args([
 67            "--json",
 68            "create",
 69            "Fix login",
 70            "-d",
 71            "The login page is broken",
 72        ])
 73        .current_dir(&tmp)
 74        .assert()
 75        .success()
 76        .stdout(predicate::str::contains("The login page is broken"));
 77}
 78
 79#[test]
 80fn create_with_labels() {
 81    let tmp = init_tmp();
 82
 83    td(&tmp)
 84        .args(["--json", "create", "Labelled task", "-l", "frontend,urgent"])
 85        .current_dir(&tmp)
 86        .assert()
 87        .success();
 88
 89    let out = td(&tmp)
 90        .args(["--json", "list", "-l", "frontend"])
 91        .current_dir(&tmp)
 92        .output()
 93        .unwrap();
 94    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
 95    assert_eq!(v.as_array().unwrap().len(), 1);
 96
 97    let task = &v[0];
 98    let labels = task["labels"].as_array().unwrap();
 99    assert!(labels.contains(&serde_json::Value::String("frontend".to_string())));
100    assert!(labels.contains(&serde_json::Value::String("urgent".to_string())));
101}
102
103#[test]
104fn create_without_title_non_interactive_errors() {
105    // Without a title, in non-interactive mode (no tty), td should fail with
106    // a helpful message rather than silently doing nothing.
107    let tmp = init_tmp();
108
109    td(&tmp)
110        .arg("create")
111        .current_dir(&tmp)
112        .assert()
113        .failure()
114        .stderr(predicate::str::contains("title required"));
115}
116
117#[test]
118fn create_via_editor_uses_first_line_as_title() {
119    // When TD_FORCE_EDITOR is set to a command that writes known content, td
120    // should pick up the result and create the task.
121    let tmp = init_tmp();
122
123    // The fake editor overwrites its first argument with a known payload.
124    let fake_editor = "sh -c 'printf \"Editor title\\nEditor description\" > \"$1\"' sh";
125
126    let out = td(&tmp)
127        .args(["--json", "create"])
128        .env("TD_FORCE_EDITOR", fake_editor)
129        .current_dir(&tmp)
130        .output()
131        .unwrap();
132
133    assert!(
134        out.status.success(),
135        "stderr: {}",
136        String::from_utf8_lossy(&out.stderr)
137    );
138    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
139    assert_eq!(v["title"].as_str().unwrap(), "Editor title");
140    assert_eq!(v["description"].as_str().unwrap(), "Editor description");
141}
142
143#[test]
144fn create_via_editor_aborts_on_empty_file() {
145    // If the editor leaves the file empty (or only comments), td should exit
146    // with a non-zero status and not create a task.
147    let tmp = init_tmp();
148
149    let fake_editor = "sh -c 'printf \"TD: just a comment\\n\" > \"$1\"' sh";
150
151    td(&tmp)
152        .args(["create"])
153        .env("TD_FORCE_EDITOR", fake_editor)
154        .current_dir(&tmp)
155        .assert()
156        .failure()
157        .stderr(predicate::str::contains("aborted"));
158}
159
160#[test]
161fn create_subtask_under_parent() {
162    let tmp = init_tmp();
163
164    // Create parent, extract its id.
165    let parent_out = td(&tmp)
166        .args(["--json", "create", "Parent task"])
167        .current_dir(&tmp)
168        .output()
169        .unwrap();
170    let parent: serde_json::Value = serde_json::from_slice(&parent_out.stdout).unwrap();
171    let parent_id = parent["id"].as_str().unwrap();
172
173    // Create child under parent.
174    let child_out = td(&tmp)
175        .args(["--json", "create", "Child task", "--parent", parent_id])
176        .current_dir(&tmp)
177        .output()
178        .unwrap();
179    let child: serde_json::Value = serde_json::from_slice(&child_out.stdout).unwrap();
180    let child_id = child["id"].as_str().unwrap();
181
182    // Child id is its own ULID; relationship is represented by the parent field.
183    assert_ne!(child_id, parent_id);
184    assert_eq!(child["parent"].as_str().unwrap(), parent_id);
185}
186
187#[test]
188fn create_with_effort() {
189    let tmp = init_tmp();
190
191    let out = td(&tmp)
192        .args(["--json", "create", "Hard task", "-e", "high"])
193        .current_dir(&tmp)
194        .output()
195        .unwrap();
196    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
197    assert_eq!(v["effort"].as_str().unwrap(), "high");
198}
199
200#[test]
201fn create_with_priority_label() {
202    let tmp = init_tmp();
203
204    let out = td(&tmp)
205        .args(["--json", "create", "Low prio", "-p", "low"])
206        .current_dir(&tmp)
207        .output()
208        .unwrap();
209    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
210    assert_eq!(v["priority"].as_str().unwrap(), "low");
211}
212
213#[test]
214fn create_rejects_invalid_priority() {
215    let tmp = init_tmp();
216
217    td(&tmp)
218        .args(["create", "Bad", "-p", "urgent"])
219        .current_dir(&tmp)
220        .assert()
221        .failure()
222        .stderr(predicates::prelude::predicate::str::contains(
223            "invalid priority",
224        ));
225}
226
227#[test]
228fn create_rejects_invalid_effort() {
229    let tmp = init_tmp();
230
231    td(&tmp)
232        .args(["create", "Bad", "-e", "huge"])
233        .current_dir(&tmp)
234        .assert()
235        .failure()
236        .stderr(predicates::prelude::predicate::str::contains(
237            "invalid effort",
238        ));
239}