microsoft-todo-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Unit tests for checklist item wrapper functions"""
3
+
4
+ import unittest
5
+ from todocli.graphapi.wrapper import (
6
+ StepNotFoundByName,
7
+ StepNotFoundByIndex,
8
+ BASE_URL,
9
+ )
10
+
11
+
12
+ class TestStepExceptions(unittest.TestCase):
13
+ """Test step-related exception classes"""
14
+
15
+ def test_step_not_found_by_name_exception(self):
16
+ """Test StepNotFoundByName exception message"""
17
+ step_name = "NonExistentStep"
18
+ task_name = "Buy groceries"
19
+ exc = StepNotFoundByName(step_name, task_name)
20
+
21
+ self.assertIn(step_name, exc.message)
22
+ self.assertIn(task_name, exc.message)
23
+ self.assertIn("could not be found", exc.message)
24
+
25
+ def test_step_not_found_by_index_exception(self):
26
+ """Test StepNotFoundByIndex exception message"""
27
+ step_index = 999
28
+ task_name = "Buy groceries"
29
+ exc = StepNotFoundByIndex(step_index, task_name)
30
+
31
+ self.assertIn(str(step_index), exc.message)
32
+ self.assertIn(task_name, exc.message)
33
+ self.assertIn("could not be found", exc.message)
34
+
35
+
36
+ class TestChecklistEndpointPattern(unittest.TestCase):
37
+ """Test that the checklist endpoint URL is correctly constructed"""
38
+
39
+ def test_checklist_endpoint_format(self):
40
+ """Test the checklistItems endpoint follows the expected pattern"""
41
+ list_id = "list123"
42
+ task_id = "task456"
43
+ endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}/checklistItems"
44
+
45
+ self.assertIn("checklistItems", endpoint)
46
+ self.assertIn(list_id, endpoint)
47
+ self.assertIn(task_id, endpoint)
48
+ self.assertTrue(endpoint.startswith("https://graph.microsoft.com"))
49
+
50
+ def test_checklist_item_endpoint_format(self):
51
+ """Test individual checklistItem endpoint format"""
52
+ list_id = "list123"
53
+ task_id = "task456"
54
+ step_id = "step789"
55
+ endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}/checklistItems/{step_id}"
56
+
57
+ self.assertIn(step_id, endpoint)
58
+ self.assertTrue(endpoint.endswith(step_id))
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+ """Unit tests for CLI command parsing and argument handling"""
3
+
4
+ import unittest
5
+ from todocli.cli import (
6
+ setup_parser,
7
+ parse_task_path,
8
+ try_parse_as_int,
9
+ )
10
+
11
+
12
+ class TestCLIArgumentParsing(unittest.TestCase):
13
+ """Test argparse setup for all commands"""
14
+
15
+ def setUp(self):
16
+ self.parser = setup_parser()
17
+
18
+ def test_ls_command(self):
19
+ """Test 'ls' command parsing"""
20
+ args = self.parser.parse_args(["ls"])
21
+ self.assertTrue(hasattr(args, "func"))
22
+ self.assertIsNotNone(args.func)
23
+
24
+ def test_lst_command_with_list(self):
25
+ """Test 'lst' command with list name"""
26
+ args = self.parser.parse_args(["lst", "personal"])
27
+ self.assertEqual(args.list_name, "personal")
28
+
29
+ def test_lst_command_default_list(self):
30
+ """Test 'lst' command defaults to 'Tasks'"""
31
+ args = self.parser.parse_args(["lst"])
32
+ self.assertEqual(args.list_name, "Tasks")
33
+
34
+ def test_new_command_basic(self):
35
+ """Test 'new' command with task name"""
36
+ args = self.parser.parse_args(["new", "buy milk"])
37
+ self.assertEqual(args.task_name, "buy milk")
38
+ self.assertIsNone(args.reminder)
39
+ self.assertIsNone(getattr(args, "list", None))
40
+
41
+ def test_new_command_with_reminder(self):
42
+ """Test 'new' command with -r flag"""
43
+ args = self.parser.parse_args(["new", "-r", "9:00", "buy milk"])
44
+ self.assertEqual(args.task_name, "buy milk")
45
+ self.assertEqual(args.reminder, "9:00")
46
+
47
+ def test_new_command_with_list_flag(self):
48
+ """Test 'new' command with --list flag"""
49
+ args = self.parser.parse_args(["new", "--list", "personal", "buy milk"])
50
+ self.assertEqual(args.task_name, "buy milk")
51
+ self.assertEqual(args.list, "personal")
52
+
53
+ def test_new_command_with_short_list_flag(self):
54
+ """Test 'new' command with -l flag"""
55
+ args = self.parser.parse_args(["new", "-l", "work", "task"])
56
+ self.assertEqual(args.task_name, "task")
57
+ self.assertEqual(args.list, "work")
58
+
59
+ def test_new_command_with_all_flags(self):
60
+ """Test 'new' command with both -l and -r flags"""
61
+ args = self.parser.parse_args(
62
+ ["new", "-l", "personal", "-r", "9:00", "buy milk"]
63
+ )
64
+ self.assertEqual(args.task_name, "buy milk")
65
+ self.assertEqual(args.list, "personal")
66
+ self.assertEqual(args.reminder, "9:00")
67
+
68
+ def test_new_command_with_important(self):
69
+ """Test 'new' command with -I flag"""
70
+ args = self.parser.parse_args(["new", "-I", "buy milk"])
71
+ self.assertEqual(args.task_name, "buy milk")
72
+ self.assertTrue(args.important)
73
+
74
+ def test_new_command_with_important_long(self):
75
+ """Test 'new' command with --important flag"""
76
+ args = self.parser.parse_args(["new", "--important", "buy milk"])
77
+ self.assertEqual(args.task_name, "buy milk")
78
+ self.assertTrue(args.important)
79
+
80
+ def test_new_command_without_important(self):
81
+ """Test 'new' command defaults important to False"""
82
+ args = self.parser.parse_args(["new", "buy milk"])
83
+ self.assertFalse(args.important)
84
+
85
+ def test_new_command_with_due(self):
86
+ """Test 'new' command with -d flag"""
87
+ args = self.parser.parse_args(["new", "-d", "tomorrow", "buy milk"])
88
+ self.assertEqual(args.task_name, "buy milk")
89
+ self.assertEqual(args.due, "tomorrow")
90
+
91
+ def test_new_command_with_due_long(self):
92
+ """Test 'new' command with --due flag"""
93
+ args = self.parser.parse_args(["new", "--due", "2026-01-15", "buy milk"])
94
+ self.assertEqual(args.task_name, "buy milk")
95
+ self.assertEqual(args.due, "2026-01-15")
96
+
97
+ def test_new_command_with_recurrence(self):
98
+ """Test 'new' command with -R flag"""
99
+ args = self.parser.parse_args(["new", "-R", "daily", "buy milk"])
100
+ self.assertEqual(args.task_name, "buy milk")
101
+ self.assertEqual(args.recurrence, "daily")
102
+
103
+ def test_new_command_with_recurrence_long(self):
104
+ """Test 'new' command with --recurrence flag"""
105
+ args = self.parser.parse_args(["new", "--recurrence", "weekly", "buy milk"])
106
+ self.assertEqual(args.task_name, "buy milk")
107
+ self.assertEqual(args.recurrence, "weekly")
108
+
109
+ def test_new_command_without_recurrence(self):
110
+ """Test 'new' command defaults recurrence to None"""
111
+ args = self.parser.parse_args(["new", "buy milk"])
112
+ self.assertIsNone(args.recurrence)
113
+
114
+ def test_new_command_with_step_flag(self):
115
+ """Test 'new' command with -S flag (repeatable)"""
116
+ args = self.parser.parse_args(
117
+ ["new", "-S", "milk", "-S", "eggs", "buy groceries"]
118
+ )
119
+ self.assertEqual(args.task_name, "buy groceries")
120
+ self.assertEqual(args.step, ["milk", "eggs"])
121
+
122
+ def test_new_command_with_step_long_flag(self):
123
+ """Test 'new' command with --step flag"""
124
+ args = self.parser.parse_args(["new", "--step", "milk", "buy groceries"])
125
+ self.assertEqual(args.step, ["milk"])
126
+
127
+ def test_new_command_without_steps(self):
128
+ """Test 'new' command defaults step to empty list"""
129
+ args = self.parser.parse_args(["new", "buy milk"])
130
+ self.assertEqual(args.step, [])
131
+
132
+ def test_new_command_with_all_flags(self):
133
+ """Test 'new' command with all flags"""
134
+ args = self.parser.parse_args(
135
+ [
136
+ "new",
137
+ "-l",
138
+ "personal",
139
+ "-r",
140
+ "9:00",
141
+ "-d",
142
+ "tomorrow",
143
+ "-I",
144
+ "-R",
145
+ "daily",
146
+ "buy milk",
147
+ ]
148
+ )
149
+ self.assertEqual(args.task_name, "buy milk")
150
+ self.assertEqual(args.list, "personal")
151
+ self.assertEqual(args.reminder, "9:00")
152
+ self.assertEqual(args.due, "tomorrow")
153
+ self.assertTrue(args.important)
154
+ self.assertEqual(args.recurrence, "daily")
155
+
156
+ def test_newl_command(self):
157
+ """Test 'newl' command for creating lists"""
158
+ args = self.parser.parse_args(["newl", "shopping"])
159
+ self.assertEqual(args.list_name, "shopping")
160
+
161
+ def test_new_list_command(self):
162
+ """Test 'new-list' command (primary name for creating lists)"""
163
+ args = self.parser.parse_args(["new-list", "work"])
164
+ self.assertEqual(args.list_name, "work")
165
+
166
+ def test_new_with_json_flag(self):
167
+ """Test 'new' command with --json flag"""
168
+ args = self.parser.parse_args(["new", "--json", "buy milk"])
169
+ self.assertTrue(args.json)
170
+
171
+ def test_complete_with_json_flag(self):
172
+ """Test 'complete' command with --json flag"""
173
+ args = self.parser.parse_args(["complete", "-j", "task"])
174
+ self.assertTrue(args.json)
175
+
176
+ def test_rm_with_json_flag(self):
177
+ """Test 'rm' command with --json flag"""
178
+ args = self.parser.parse_args(["rm", "--json", "-y", "task"])
179
+ self.assertTrue(args.json)
180
+
181
+ def test_update_with_json_flag(self):
182
+ """Test 'update' command with --json flag"""
183
+ args = self.parser.parse_args(["update", "-j", "task", "--title", "new"])
184
+ self.assertTrue(args.json)
185
+
186
+ def test_complete_command_basic(self):
187
+ """Test 'complete' command"""
188
+ args = self.parser.parse_args(["complete", "task"])
189
+ self.assertEqual(args.task_names, ["task"])
190
+ self.assertIsNone(getattr(args, "list", None))
191
+
192
+ def test_complete_command_with_list(self):
193
+ """Test 'complete' command with --list flag"""
194
+ args = self.parser.parse_args(["complete", "--list", "personal", "task"])
195
+ self.assertEqual(args.task_names, ["task"])
196
+ self.assertEqual(args.list, "personal")
197
+
198
+ def test_complete_command_multiple_tasks(self):
199
+ """Test 'complete' command with multiple tasks"""
200
+ args = self.parser.parse_args(["complete", "task1", "task2", "task3"])
201
+ self.assertEqual(args.task_names, ["task1", "task2", "task3"])
202
+
203
+ def test_rm_command_basic(self):
204
+ """Test 'rm' command"""
205
+ args = self.parser.parse_args(["rm", "task"])
206
+ self.assertEqual(args.task_names, ["task"])
207
+
208
+ def test_rm_command_with_list(self):
209
+ """Test 'rm' command with -l flag"""
210
+ args = self.parser.parse_args(["rm", "-l", "work", "task"])
211
+ self.assertEqual(args.task_names, ["task"])
212
+ self.assertEqual(args.list, "work")
213
+
214
+ def test_rm_command_multiple_tasks(self):
215
+ """Test 'rm' command with multiple tasks"""
216
+ args = self.parser.parse_args(["rm", "task1", "task2"])
217
+ self.assertEqual(args.task_names, ["task1", "task2"])
218
+
219
+ def test_rm_command_yes_flag(self):
220
+ """Test 'rm' command with -y/--yes flag"""
221
+ args = self.parser.parse_args(["rm", "-y", "task"])
222
+ self.assertTrue(args.yes)
223
+
224
+ args = self.parser.parse_args(["rm", "--yes", "task"])
225
+ self.assertTrue(args.yes)
226
+
227
+ def test_interactive_flag(self):
228
+ """Test -i/--interactive flag"""
229
+ args = self.parser.parse_args(["-i", "ls"])
230
+ self.assertTrue(args.interactive)
231
+
232
+ args = self.parser.parse_args(["--interactive", "ls"])
233
+ self.assertTrue(args.interactive)
234
+
235
+ def test_lst_date_format_flag(self):
236
+ """Test lst command with --date-format flag"""
237
+ args = self.parser.parse_args(["lst", "--date-format", "iso"])
238
+ self.assertEqual(args.date_format, "iso")
239
+
240
+ args = self.parser.parse_args(["lst", "--date-format", "us"])
241
+ self.assertEqual(args.date_format, "us")
242
+
243
+ args = self.parser.parse_args(["lst", "--date-format", "eu"])
244
+ self.assertEqual(args.date_format, "eu")
245
+
246
+ def test_lst_date_format_default(self):
247
+ """Test lst command defaults date-format to eu"""
248
+ args = self.parser.parse_args(["lst"])
249
+ self.assertEqual(args.date_format, "eu")
250
+
251
+ def test_complete_with_id_flag(self):
252
+ """Test 'complete' command with --id flag"""
253
+ args = self.parser.parse_args(["complete", "--id", "AAMkABC123"])
254
+ self.assertEqual(args.task_id, "AAMkABC123")
255
+ self.assertEqual(args.task_names, [])
256
+
257
+ def test_complete_with_id_and_list_flag(self):
258
+ """Test 'complete' command with --id and -l flags"""
259
+ args = self.parser.parse_args(["complete", "--id", "AAMkABC123", "-l", "Work"])
260
+ self.assertEqual(args.task_id, "AAMkABC123")
261
+ self.assertEqual(args.list, "Work")
262
+
263
+ def test_rm_with_id_flag(self):
264
+ """Test 'rm' command with --id flag"""
265
+ args = self.parser.parse_args(["rm", "--id", "AAMkABC123", "-y"])
266
+ self.assertEqual(args.task_id, "AAMkABC123")
267
+ self.assertTrue(args.yes)
268
+
269
+ def test_show_with_id_flag(self):
270
+ """Test 'show' command with --id flag"""
271
+ args = self.parser.parse_args(["show", "--id", "AAMkABC123"])
272
+ self.assertEqual(args.task_id, "AAMkABC123")
273
+ self.assertIsNone(args.task_name)
274
+
275
+ def test_update_with_id_flag(self):
276
+ """Test 'update' command with --id flag"""
277
+ args = self.parser.parse_args(
278
+ ["update", "--id", "AAMkABC123", "--title", "New"]
279
+ )
280
+ self.assertEqual(args.task_id, "AAMkABC123")
281
+ self.assertEqual(args.title, "New")
282
+
283
+ def test_list_steps_with_id_flag(self):
284
+ """Test 'list-steps' command with --id flag"""
285
+ args = self.parser.parse_args(["list-steps", "--id", "AAMkABC123"])
286
+ self.assertEqual(args.task_id, "AAMkABC123")
287
+
288
+ def test_new_step_with_id_flag(self):
289
+ """Test 'new-step' command with --id flag"""
290
+ args = self.parser.parse_args(["new-step", "--id", "AAMkABC123", "Step 1"])
291
+ self.assertEqual(args.task_id, "AAMkABC123")
292
+ self.assertEqual(args.step_name, "Step 1")
293
+
294
+ def test_complete_step_with_id_flag(self):
295
+ """Test 'complete-step' command with --id flag"""
296
+ # When --id is used, the step arg goes into task_name (first positional)
297
+ # CLI code handles this by using task_name as step when task_id is set
298
+ args = self.parser.parse_args(["complete-step", "--id", "AAMkABC123", "0"])
299
+ self.assertEqual(args.task_id, "AAMkABC123")
300
+ self.assertEqual(args.task_name, "0")
301
+
302
+ def test_rm_step_with_id_flag(self):
303
+ """Test 'rm-step' command with --id flag"""
304
+ # When --id is used, the step arg goes into task_name (first positional)
305
+ # CLI code handles this by using task_name as step when task_id is set
306
+ args = self.parser.parse_args(["rm-step", "--id", "AAMkABC123", "0"])
307
+ self.assertEqual(args.task_id, "AAMkABC123")
308
+ self.assertEqual(args.task_name, "0")
309
+
310
+ def test_tasks_all_flag(self):
311
+ """Test 'tasks' command with --all flag"""
312
+ args = self.parser.parse_args(["tasks", "--all"])
313
+ self.assertTrue(args.all)
314
+
315
+ def test_tasks_completed_flag(self):
316
+ """Test 'tasks' command with --completed flag"""
317
+ args = self.parser.parse_args(["tasks", "--completed"])
318
+ self.assertTrue(args.completed)
319
+
320
+ def test_uncomplete_command_basic(self):
321
+ """Test 'uncomplete' command"""
322
+ args = self.parser.parse_args(["uncomplete", "task"])
323
+ self.assertEqual(args.task_names, ["task"])
324
+
325
+ def test_uncomplete_with_id_flag(self):
326
+ """Test 'uncomplete' command with --id flag"""
327
+ args = self.parser.parse_args(["uncomplete", "--id", "AAMkABC123"])
328
+ self.assertEqual(args.task_id, "AAMkABC123")
329
+
330
+ def test_uncomplete_step_command_basic(self):
331
+ """Test 'uncomplete-step' command"""
332
+ args = self.parser.parse_args(["uncomplete-step", "task", "0"])
333
+ self.assertEqual(args.task_name, "task")
334
+ self.assertEqual(args.step_name, "0")
335
+
336
+ def test_uncomplete_step_with_id_flag(self):
337
+ """Test 'uncomplete-step' command with --id flag"""
338
+ # When --id is used, the step arg goes into task_name (first positional)
339
+ # CLI code handles this by using task_name as step when task_id is set
340
+ args = self.parser.parse_args(["uncomplete-step", "--id", "AAMkABC123", "0"])
341
+ self.assertEqual(args.task_id, "AAMkABC123")
342
+ self.assertEqual(args.task_name, "0")
343
+
344
+
345
+ class TestParseTaskPath(unittest.TestCase):
346
+ """Test parse_task_path function"""
347
+
348
+ def test_simple_task_name(self):
349
+ """Test task name without list defaults to 'Tasks'"""
350
+ list_name, task_name = parse_task_path("buy milk")
351
+ self.assertEqual(list_name, "Tasks")
352
+ self.assertEqual(task_name, "buy milk")
353
+
354
+ def test_task_with_explicit_list(self):
355
+ """Test task with explicit list_name parameter"""
356
+ list_name, task_name = parse_task_path("buy milk", list_name="work")
357
+ self.assertEqual(list_name, "work")
358
+ self.assertEqual(task_name, "buy milk")
359
+
360
+ def test_task_with_slashes_no_autoparsing(self):
361
+ """Test that slashes are NOT auto-parsed as list separator"""
362
+ list_name, task_name = parse_task_path("personal/buy milk")
363
+ self.assertEqual(list_name, "Tasks")
364
+ self.assertEqual(task_name, "personal/buy milk")
365
+
366
+ def test_url_as_task_name(self):
367
+ """Test URL as task name works without special handling"""
368
+ list_name, task_name = parse_task_path("https://www.google.com/")
369
+ self.assertEqual(list_name, "Tasks")
370
+ self.assertEqual(task_name, "https://www.google.com/")
371
+
372
+ def test_url_with_explicit_list(self):
373
+ """Test URL task name with explicit list"""
374
+ list_name, task_name = parse_task_path(
375
+ "https://example.com/path/to/page", list_name="work"
376
+ )
377
+ self.assertEqual(list_name, "work")
378
+ self.assertEqual(task_name, "https://example.com/path/to/page")
379
+
380
+ def test_empty_task_name(self):
381
+ """Test empty task name"""
382
+ list_name, task_name = parse_task_path("")
383
+ self.assertEqual(list_name, "Tasks")
384
+ self.assertEqual(task_name, "")
385
+
386
+ def test_special_characters_in_task_name(self):
387
+ """Test task names with special characters"""
388
+ list_name, task_name = parse_task_path("check A/B testing")
389
+ self.assertEqual(list_name, "Tasks")
390
+ self.assertEqual(task_name, "check A/B testing")
391
+
392
+
393
+ class TestTryParseAsInt(unittest.TestCase):
394
+ """Test try_parse_as_int helper function"""
395
+
396
+ def test_valid_integer_string(self):
397
+ """Test parsing valid integer string"""
398
+ result = try_parse_as_int("42")
399
+ self.assertEqual(result, 42)
400
+ self.assertIsInstance(result, int)
401
+
402
+ def test_zero(self):
403
+ """Test parsing zero"""
404
+ result = try_parse_as_int("0")
405
+ self.assertEqual(result, 0)
406
+
407
+ def test_negative_integer(self):
408
+ """Test parsing negative integer"""
409
+ result = try_parse_as_int("-5")
410
+ self.assertEqual(result, -5)
411
+
412
+ def test_non_integer_string(self):
413
+ """Test that non-integer strings are returned as-is"""
414
+ result = try_parse_as_int("buy milk")
415
+ self.assertEqual(result, "buy milk")
416
+ self.assertIsInstance(result, str)
417
+
418
+ def test_float_string(self):
419
+ """Test that float strings are returned as-is"""
420
+ result = try_parse_as_int("3.14")
421
+ self.assertEqual(result, "3.14")
422
+ self.assertIsInstance(result, str)
423
+
424
+ def test_empty_string(self):
425
+ """Test empty string"""
426
+ result = try_parse_as_int("")
427
+ self.assertEqual(result, "")
428
+
429
+ def test_mixed_alphanumeric(self):
430
+ """Test mixed alphanumeric string"""
431
+ result = try_parse_as_int("task123")
432
+ self.assertEqual(result, "task123")
433
+
434
+
435
+ if __name__ == "__main__":
436
+ unittest.main()
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """Unit tests for confirmation output after mutating CLI commands"""
3
+
4
+ import unittest
5
+ from unittest.mock import patch, MagicMock
6
+ from io import StringIO
7
+
8
+ from todocli.cli import (
9
+ new,
10
+ newl,
11
+ complete,
12
+ rm,
13
+ update,
14
+ new_step,
15
+ complete_step,
16
+ rm_step,
17
+ )
18
+
19
+
20
+ def _make_args(**kwargs):
21
+ args = MagicMock()
22
+ for k, v in kwargs.items():
23
+ setattr(args, k, v)
24
+ # Defaults for optional flags
25
+ if "list" not in kwargs:
26
+ args.list = None
27
+ if "reminder" not in kwargs:
28
+ args.reminder = None
29
+ if "due" not in kwargs:
30
+ args.due = None
31
+ if "important" not in kwargs:
32
+ args.important = False
33
+ if "recurrence" not in kwargs:
34
+ args.recurrence = None
35
+ if "title" not in kwargs:
36
+ args.title = None
37
+ if "task_names" not in kwargs:
38
+ args.task_names = None
39
+ if "yes" not in kwargs:
40
+ args.yes = False
41
+ if "json" not in kwargs:
42
+ args.json = False
43
+ if "task_id" not in kwargs:
44
+ args.task_id = None
45
+ if "task_index" not in kwargs:
46
+ args.task_index = None
47
+ if "step_id" not in kwargs:
48
+ args.step_id = None
49
+ return args
50
+
51
+
52
+ class TestConfirmationOutput(unittest.TestCase):
53
+
54
+ @patch("todocli.cli.wrapper")
55
+ def test_new_prints_confirmation(self, mock_wrapper):
56
+ mock_wrapper.create_task.return_value = "task-id-123"
57
+ args = _make_args(task_name="buy milk")
58
+
59
+ with patch("sys.stdout", new_callable=StringIO) as out:
60
+ new(args)
61
+ self.assertIn("Created task", out.getvalue())
62
+ self.assertIn("buy milk", out.getvalue())
63
+
64
+ @patch("todocli.cli.wrapper")
65
+ def test_new_with_steps_prints_confirmation(self, mock_wrapper):
66
+ mock_wrapper.create_task.return_value = "task-id-123"
67
+ mock_wrapper.get_list_id_by_name.return_value = "list-id"
68
+ mock_wrapper.create_checklist_item.return_value = ("step-id-123", "step name")
69
+ args = _make_args(task_name="buy groceries", step=["milk", "eggs"])
70
+
71
+ with patch("sys.stdout", new_callable=StringIO) as out:
72
+ new(args)
73
+ output = out.getvalue()
74
+ self.assertIn("Created task", output)
75
+ self.assertIn("buy groceries", output)
76
+ self.assertIn("2 step(s)", output)
77
+
78
+ # Verify steps were created
79
+ self.assertEqual(mock_wrapper.create_checklist_item.call_count, 2)
80
+
81
+ @patch("todocli.cli.wrapper")
82
+ def test_newl_prints_confirmation(self, mock_wrapper):
83
+ mock_wrapper.create_list.return_value = ("list-id-123", "Shopping")
84
+ args = _make_args(list_name="Shopping")
85
+
86
+ with patch("sys.stdout", new_callable=StringIO) as out:
87
+ newl(args)
88
+ self.assertIn("Created list", out.getvalue())
89
+ self.assertIn("Shopping", out.getvalue())
90
+
91
+ @patch("todocli.cli.wrapper")
92
+ def test_complete_prints_confirmation(self, mock_wrapper):
93
+ mock_wrapper.complete_task.return_value = ("task-id-123", "buy milk")
94
+ args = _make_args(task_name="Tasks/buy milk")
95
+
96
+ with patch("sys.stdout", new_callable=StringIO) as out:
97
+ complete(args)
98
+ self.assertIn("Completed task", out.getvalue())
99
+
100
+ @patch("todocli.cli.wrapper")
101
+ def test_rm_prints_confirmation(self, mock_wrapper):
102
+ mock_wrapper.remove_task.return_value = ("task-id-123", "buy milk")
103
+ args = _make_args(task_name="Tasks/buy milk", yes=True)
104
+
105
+ with patch("sys.stdout", new_callable=StringIO) as out:
106
+ rm(args)
107
+ self.assertIn("Removed task", out.getvalue())
108
+
109
+ @patch("todocli.cli.confirm_action", return_value=False)
110
+ def test_rm_skipped_no_error(self, mock_confirm):
111
+ """Test rm prints 'Skipped' and does not raise when user declines"""
112
+ args = _make_args(task_name="Tasks/buy milk", yes=False)
113
+
114
+ with patch("sys.stdout", new_callable=StringIO) as out:
115
+ # Should not raise
116
+ rm(args)
117
+ self.assertIn("Skipped", out.getvalue())
118
+
119
+ @patch("todocli.cli.confirm_action", return_value=False)
120
+ def test_rm_multiple_skipped_no_error(self, mock_confirm):
121
+ """Test rm with multiple tasks all skipped does not raise"""
122
+ args = _make_args(task_names=["Tasks/task1", "Tasks/task2"], yes=False)
123
+
124
+ with patch("sys.stdout", new_callable=StringIO) as out:
125
+ rm(args)
126
+ output = out.getvalue()
127
+ self.assertIn("Skipped", output)
128
+ self.assertEqual(output.count("Skipped"), 2)
129
+
130
+ @patch("todocli.cli.wrapper")
131
+ def test_update_prints_confirmation(self, mock_wrapper):
132
+ mock_wrapper.update_task.return_value = ("task-id-123", "new name")
133
+ args = _make_args(task_name="Tasks/buy milk", title="new name")
134
+
135
+ with patch("sys.stdout", new_callable=StringIO) as out:
136
+ update(args)
137
+ self.assertIn("Updated task", out.getvalue())
138
+
139
+ @patch("todocli.cli.wrapper")
140
+ def test_new_step_prints_confirmation(self, mock_wrapper):
141
+ mock_wrapper.create_checklist_item.return_value = ("step-id-123", "get eggs")
142
+ args = _make_args(task_name="Tasks/buy milk", step_name="get eggs")
143
+
144
+ with patch("sys.stdout", new_callable=StringIO) as out:
145
+ new_step(args)
146
+ self.assertIn("Added step", out.getvalue())
147
+ self.assertIn("get eggs", out.getvalue())
148
+
149
+ @patch("todocli.cli.wrapper")
150
+ def test_complete_step_prints_confirmation(self, mock_wrapper):
151
+ mock_wrapper.complete_checklist_item.return_value = ("step-id-123", "get eggs")
152
+ args = _make_args(task_name="Tasks/buy milk", step_name="get eggs")
153
+
154
+ with patch("sys.stdout", new_callable=StringIO) as out:
155
+ complete_step(args)
156
+ self.assertIn("Completed step", out.getvalue())
157
+
158
+ @patch("todocli.cli.wrapper")
159
+ def test_rm_step_prints_confirmation(self, mock_wrapper):
160
+ mock_wrapper.delete_checklist_item.return_value = "step-id-123"
161
+ args = _make_args(task_name="Tasks/buy milk", step_name="get eggs")
162
+
163
+ with patch("sys.stdout", new_callable=StringIO) as out:
164
+ rm_step(args)
165
+ self.assertIn("Removed step", out.getvalue())
166
+
167
+
168
+ if __name__ == "__main__":
169
+ unittest.main()