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}