todo-agent 0.2.9__tar.gz → 0.3.1__tar.gz
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.
- {todo_agent-0.2.9 → todo_agent-0.3.1}/PKG-INFO +1 -1
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/test_todo_manager.py +159 -76
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_todo_shell.py +31 -41
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/_version.py +3 -3
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/todo_manager.py +33 -1
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/inference.py +4 -3
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/openrouter_client.py +5 -5
- todo_agent-0.3.1/todo_agent/infrastructure/prompts/system_prompt.txt +399 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/todo_shell.py +27 -13
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/cli.py +2 -2
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/tools.py +71 -118
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/PKG-INFO +1 -1
- todo_agent-0.2.9/todo_agent/infrastructure/prompts/system_prompt.txt +0 -413
- {todo_agent-0.2.9 → todo_agent-0.3.1}/.gitignore +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/LICENSE +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/MANIFEST.in +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/Makefile +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/README.md +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/docs/publishing.md +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/pyproject.toml +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/requirements-dev.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/requirements.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/setup.cfg +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/test_conversation_manager.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_calendar_utils.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_inference.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_ollama_client.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_openrouter_client.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_cli.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_formatters.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_tools.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_linting.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_logger.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_main.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/conversation_manager.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/exceptions.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/calendar_utils.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/llm_client.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/ollama_client.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/formatters.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/main.py +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/SOURCES.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/top_level.txt +0 -0
@@ -3,10 +3,9 @@ Tests for TodoManager.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import unittest
|
6
|
-
from unittest.mock import Mock
|
6
|
+
from unittest.mock import Mock, patch
|
7
7
|
|
8
8
|
import pytest
|
9
|
-
from unittest.mock import patch
|
10
9
|
|
11
10
|
try:
|
12
11
|
from todo_agent.core.todo_manager import TodoManager
|
@@ -187,6 +186,36 @@ class TestTodoManager(unittest.TestCase):
|
|
187
186
|
self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
|
188
187
|
self.assertIn("Must be a positive integer", str(context.exception))
|
189
188
|
|
189
|
+
def test_add_task_with_invalid_duration_format(self):
|
190
|
+
"""Test adding a task with invalid duration format raises ValueError."""
|
191
|
+
with self.assertRaises(ValueError) as context:
|
192
|
+
self.todo_manager.add_task("Test task", duration="30s")
|
193
|
+
self.assertIn("Invalid duration format", str(context.exception))
|
194
|
+
|
195
|
+
def test_add_task_with_invalid_duration_unit(self):
|
196
|
+
"""Test adding a task with invalid duration unit raises ValueError."""
|
197
|
+
with self.assertRaises(ValueError) as context:
|
198
|
+
self.todo_manager.add_task("Test task", duration="30s")
|
199
|
+
self.assertIn("Invalid duration format", str(context.exception))
|
200
|
+
|
201
|
+
def test_add_task_with_invalid_duration_value(self):
|
202
|
+
"""Test adding a task with invalid duration value raises ValueError."""
|
203
|
+
with self.assertRaises(ValueError) as context:
|
204
|
+
self.todo_manager.add_task("Test task", duration="0m")
|
205
|
+
self.assertIn("Must be a positive number", str(context.exception))
|
206
|
+
|
207
|
+
def test_add_task_with_negative_duration(self):
|
208
|
+
"""Test adding a task with negative duration raises ValueError."""
|
209
|
+
with self.assertRaises(ValueError) as context:
|
210
|
+
self.todo_manager.add_task("Test task", duration="-30m")
|
211
|
+
self.assertIn("Must be a positive number", str(context.exception))
|
212
|
+
|
213
|
+
def test_add_task_with_empty_duration(self):
|
214
|
+
"""Test adding a task with empty duration raises ValueError."""
|
215
|
+
with self.assertRaises(ValueError) as context:
|
216
|
+
self.todo_manager.add_task("Test task", duration="")
|
217
|
+
self.assertIn("Duration must be a non-empty string", str(context.exception))
|
218
|
+
|
190
219
|
def test_add_task_with_all_parameters_including_recurring(self):
|
191
220
|
"""Test adding a task with all parameters including recurring."""
|
192
221
|
self.todo_shell.add.return_value = "Test task"
|
@@ -205,6 +234,36 @@ class TestTodoManager(unittest.TestCase):
|
|
205
234
|
"(A) Test task +work @office due:2024-01-15 rec:daily"
|
206
235
|
)
|
207
236
|
|
237
|
+
def test_add_task_with_duration(self):
|
238
|
+
"""Test adding a task with duration parameter."""
|
239
|
+
self.todo_shell.add.return_value = "Test task"
|
240
|
+
result = self.todo_manager.add_task(
|
241
|
+
"Test task",
|
242
|
+
duration="30m",
|
243
|
+
)
|
244
|
+
self.assertEqual(result, "Added task: Test task duration:30m")
|
245
|
+
self.todo_shell.add.assert_called_once_with("Test task duration:30m")
|
246
|
+
|
247
|
+
def test_add_task_with_all_parameters_including_duration(self):
|
248
|
+
"""Test adding a task with all parameters including duration."""
|
249
|
+
self.todo_shell.add.return_value = "Test task"
|
250
|
+
result = self.todo_manager.add_task(
|
251
|
+
"Test task",
|
252
|
+
priority="A",
|
253
|
+
project="work",
|
254
|
+
context="office",
|
255
|
+
due="2024-01-15",
|
256
|
+
recurring="rec:daily",
|
257
|
+
duration="2h",
|
258
|
+
)
|
259
|
+
self.assertEqual(
|
260
|
+
result,
|
261
|
+
"Added task: (A) Test task +work @office due:2024-01-15 rec:daily duration:2h",
|
262
|
+
)
|
263
|
+
self.todo_shell.add.assert_called_once_with(
|
264
|
+
"(A) Test task +work @office due:2024-01-15 rec:daily duration:2h"
|
265
|
+
)
|
266
|
+
|
208
267
|
def test_list_tasks(self):
|
209
268
|
"""Test listing tasks."""
|
210
269
|
self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
|
@@ -299,60 +358,62 @@ class TestTodoManager(unittest.TestCase):
|
|
299
358
|
self.assertEqual(result, "No completed tasks found matching the criteria.")
|
300
359
|
self.todo_shell.list_completed.assert_called_once_with("+nonexistent")
|
301
360
|
|
302
|
-
|
303
|
-
|
304
361
|
def test_add_task_sanitizes_inputs(self):
|
305
362
|
"""Test that add_task sanitizes inputs to prevent duplicates."""
|
306
363
|
# Mock the todo_shell
|
307
364
|
mock_shell = Mock()
|
308
365
|
mock_shell.add.return_value = "1 Test task +project1 @context1"
|
309
|
-
|
366
|
+
|
310
367
|
manager = TodoManager(mock_shell)
|
311
|
-
|
368
|
+
|
312
369
|
# Test with inputs that already have + and @ symbols
|
313
370
|
result = manager.add_task(
|
314
371
|
description="Test task",
|
315
372
|
project="+project1", # Already has + symbol
|
316
|
-
context="@context1"
|
373
|
+
context="@context1", # Already has @ symbol
|
317
374
|
)
|
318
|
-
|
375
|
+
|
319
376
|
# Verify the task was added with properly formatted projects and contexts
|
320
377
|
mock_shell.add.assert_called_once()
|
321
378
|
call_args = mock_shell.add.call_args[0][0]
|
322
|
-
|
379
|
+
|
323
380
|
# Should have exactly one instance of each (sanitization prevents duplicates)
|
324
381
|
assert call_args.count("+project1") == 1
|
325
382
|
assert call_args.count("@context1") == 1
|
326
|
-
|
383
|
+
|
327
384
|
# Verify the result message
|
328
385
|
assert "Added task:" in result
|
329
386
|
|
330
|
-
|
331
|
-
|
332
387
|
def test_parse_task_components_deduplicates(self):
|
333
388
|
"""Test that _parse_task_components deduplicates projects and contexts."""
|
334
389
|
from todo_agent.infrastructure.todo_shell import TodoShell
|
335
|
-
|
390
|
+
|
336
391
|
# Create a TodoShell instance
|
337
392
|
shell = TodoShell("/tmp/todo.txt")
|
338
|
-
|
393
|
+
|
339
394
|
# Test with duplicate projects and contexts in the task line
|
340
395
|
task_line = "1 (A) Task description +project1 +project1 +project2 @context1 @context1 @context2"
|
341
396
|
components = shell._parse_task_components(task_line)
|
342
|
-
|
397
|
+
|
343
398
|
# Verify deduplication
|
344
|
-
assert components["projects"] == [
|
345
|
-
|
399
|
+
assert components["projects"] == [
|
400
|
+
"+project1",
|
401
|
+
"+project2",
|
402
|
+
] # Sorted and deduplicated
|
403
|
+
assert components["contexts"] == [
|
404
|
+
"@context1",
|
405
|
+
"@context2",
|
406
|
+
] # Sorted and deduplicated
|
346
407
|
assert components["priority"] == "A"
|
347
408
|
assert components["description"] == "Task description"
|
348
409
|
|
349
410
|
def test_reconstruct_task_maintains_deduplication(self):
|
350
411
|
"""Test that _reconstruct_task maintains deduplication."""
|
351
412
|
from todo_agent.infrastructure.todo_shell import TodoShell
|
352
|
-
|
413
|
+
|
353
414
|
# Create a TodoShell instance
|
354
415
|
shell = TodoShell("/tmp/todo.txt")
|
355
|
-
|
416
|
+
|
356
417
|
# Create components with potential duplicates
|
357
418
|
components = {
|
358
419
|
"priority": "A",
|
@@ -361,12 +422,12 @@ class TestTodoManager(unittest.TestCase):
|
|
361
422
|
"contexts": ["@context1", "@context1", "@context2"], # Duplicate context1
|
362
423
|
"due": "2024-01-01",
|
363
424
|
"recurring": None,
|
364
|
-
"other_tags": []
|
425
|
+
"other_tags": [],
|
365
426
|
}
|
366
|
-
|
427
|
+
|
367
428
|
# Reconstruct the task
|
368
429
|
reconstructed = shell._reconstruct_task(components)
|
369
|
-
|
430
|
+
|
370
431
|
# Verify that duplicates are preserved in reconstruction (since we handle deduplication in parsing)
|
371
432
|
# This is expected behavior - reconstruction should preserve the exact components given
|
372
433
|
assert reconstructed.count("+project1") == 2
|
@@ -376,83 +437,96 @@ class TestTodoManager(unittest.TestCase):
|
|
376
437
|
|
377
438
|
def test_todo_shell_set_project_actually_prevents_duplicates(self):
|
378
439
|
"""Test that the todo_shell set_project method actually prevents duplicates."""
|
379
|
-
from todo_agent.infrastructure.todo_shell import TodoShell
|
380
440
|
from unittest.mock import patch
|
381
|
-
|
441
|
+
|
442
|
+
from todo_agent.infrastructure.todo_shell import TodoShell
|
443
|
+
|
382
444
|
# Create a TodoShell instance
|
383
445
|
shell = TodoShell("/tmp/todo.txt")
|
384
|
-
|
446
|
+
|
385
447
|
# Mock the list_tasks method to return a task with existing projects
|
386
|
-
with patch.object(shell,
|
387
|
-
mock_list_tasks.return_value =
|
388
|
-
|
448
|
+
with patch.object(shell, "list_tasks") as mock_list_tasks:
|
449
|
+
mock_list_tasks.return_value = (
|
450
|
+
"1 (A) Existing task +existing_project @existing_context"
|
451
|
+
)
|
452
|
+
|
389
453
|
# Mock the replace method to capture what gets called
|
390
|
-
with patch.object(shell,
|
391
|
-
mock_replace.return_value =
|
392
|
-
|
454
|
+
with patch.object(shell, "replace") as mock_replace:
|
455
|
+
mock_replace.return_value = (
|
456
|
+
"1 (A) Existing task +existing_project @existing_context"
|
457
|
+
)
|
458
|
+
|
393
459
|
# Try to add a project that already exists
|
394
460
|
result = shell.set_project(1, ["existing_project"])
|
395
|
-
|
461
|
+
|
396
462
|
# Since the project already exists, replace should NOT be called
|
397
463
|
# (our deduplication logic prevents unnecessary updates)
|
398
464
|
mock_replace.assert_not_called()
|
399
|
-
|
465
|
+
|
400
466
|
# The result should be the reconstructed task without duplicates
|
401
467
|
assert "+existing_project" in result
|
402
468
|
assert result.count("+existing_project") == 1 # Only one instance
|
403
469
|
|
404
470
|
def test_todo_shell_set_context_actually_prevents_duplicates(self):
|
405
471
|
"""Test that the todo_shell set_context method actually prevents duplicates."""
|
406
|
-
from todo_agent.infrastructure.todo_shell import TodoShell
|
407
472
|
from unittest.mock import patch
|
408
|
-
|
473
|
+
|
474
|
+
from todo_agent.infrastructure.todo_shell import TodoShell
|
475
|
+
|
409
476
|
# Create a TodoShell instance
|
410
477
|
shell = TodoShell("/tmp/todo.txt")
|
411
|
-
|
478
|
+
|
412
479
|
# Mock the list_tasks method to return a task with existing context
|
413
|
-
with patch.object(shell,
|
414
|
-
mock_list_tasks.return_value =
|
415
|
-
|
480
|
+
with patch.object(shell, "list_tasks") as mock_list_tasks:
|
481
|
+
mock_list_tasks.return_value = (
|
482
|
+
"1 (A) Existing task +existing_project @existing_context"
|
483
|
+
)
|
484
|
+
|
416
485
|
# Mock the replace method to capture what gets called
|
417
|
-
with patch.object(shell,
|
418
|
-
mock_replace.return_value =
|
419
|
-
|
486
|
+
with patch.object(shell, "replace") as mock_replace:
|
487
|
+
mock_replace.return_value = (
|
488
|
+
"1 (A) Existing task +existing_project @existing_context"
|
489
|
+
)
|
490
|
+
|
420
491
|
# Try to set a context that already exists
|
421
492
|
result = shell.set_context(1, "existing_context")
|
422
|
-
|
493
|
+
|
423
494
|
# Since the context already exists, replace should NOT be called
|
424
495
|
# (our deduplication logic prevents unnecessary updates)
|
425
496
|
mock_replace.assert_not_called()
|
426
|
-
|
497
|
+
|
427
498
|
# The result should be the reconstructed task without duplicates
|
428
499
|
assert "@existing_context" in result
|
429
500
|
assert result.count("@existing_context") == 1 # Only one instance
|
430
501
|
|
431
502
|
def test_todo_shell_set_project_adds_new_projects(self):
|
432
503
|
"""Test that the todo_shell set_project method adds new projects and prevents duplicates."""
|
433
|
-
from todo_agent.infrastructure.todo_shell import TodoShell
|
434
504
|
from unittest.mock import patch
|
435
|
-
|
505
|
+
|
506
|
+
from todo_agent.infrastructure.todo_shell import TodoShell
|
507
|
+
|
436
508
|
# Create a TodoShell instance
|
437
509
|
shell = TodoShell("/tmp/todo.txt")
|
438
|
-
|
510
|
+
|
439
511
|
# Mock the list_tasks method to return a task with existing projects
|
440
|
-
with patch.object(shell,
|
441
|
-
mock_list_tasks.return_value =
|
442
|
-
|
512
|
+
with patch.object(shell, "list_tasks") as mock_list_tasks:
|
513
|
+
mock_list_tasks.return_value = (
|
514
|
+
"1 (A) Existing task +existing_project @existing_context"
|
515
|
+
)
|
516
|
+
|
443
517
|
# Mock the replace method to capture what gets called
|
444
|
-
with patch.object(shell,
|
518
|
+
with patch.object(shell, "replace") as mock_replace:
|
445
519
|
mock_replace.return_value = "1 (A) Existing task +existing_project +new_project @existing_context"
|
446
|
-
|
520
|
+
|
447
521
|
# Try to add a new project
|
448
|
-
|
449
|
-
|
522
|
+
shell.set_project(1, ["new_project"])
|
523
|
+
|
450
524
|
# Since we're adding a new project, replace should be called
|
451
525
|
mock_replace.assert_called_once()
|
452
|
-
|
526
|
+
|
453
527
|
# Get the actual task description that was passed to replace
|
454
528
|
actual_task_description = mock_replace.call_args[0][1]
|
455
|
-
|
529
|
+
|
456
530
|
# Verify that the new project was added and no duplicates exist
|
457
531
|
assert "+new_project" in actual_task_description
|
458
532
|
assert actual_task_description.count("+existing_project") == 1
|
@@ -460,56 +534,65 @@ class TestTodoManager(unittest.TestCase):
|
|
460
534
|
|
461
535
|
def test_todo_shell_set_context_adds_new_contexts(self):
|
462
536
|
"""Test that the todo_shell set_context method adds new contexts and prevents duplicates."""
|
463
|
-
from todo_agent.infrastructure.todo_shell import TodoShell
|
464
537
|
from unittest.mock import patch
|
465
|
-
|
538
|
+
|
539
|
+
from todo_agent.infrastructure.todo_shell import TodoShell
|
540
|
+
|
466
541
|
# Create a TodoShell instance
|
467
542
|
shell = TodoShell("/tmp/todo.txt")
|
468
|
-
|
543
|
+
|
469
544
|
# Mock the list_tasks method to return a task with existing context
|
470
|
-
with patch.object(shell,
|
471
|
-
mock_list_tasks.return_value =
|
472
|
-
|
545
|
+
with patch.object(shell, "list_tasks") as mock_list_tasks:
|
546
|
+
mock_list_tasks.return_value = (
|
547
|
+
"1 (A) Existing task +existing_project @existing_context"
|
548
|
+
)
|
549
|
+
|
473
550
|
# Mock the replace method to capture what gets called
|
474
|
-
with patch.object(shell,
|
475
|
-
mock_replace.return_value =
|
476
|
-
|
551
|
+
with patch.object(shell, "replace") as mock_replace:
|
552
|
+
mock_replace.return_value = (
|
553
|
+
"1 (A) Existing task +existing_project @new_context"
|
554
|
+
)
|
555
|
+
|
477
556
|
# Try to set a new context
|
478
|
-
|
479
|
-
|
557
|
+
shell.set_context(1, "new_context")
|
558
|
+
|
480
559
|
# Since we're adding a new context, replace should be called
|
481
560
|
mock_replace.assert_called_once()
|
482
|
-
|
561
|
+
|
483
562
|
# Get the actual task description that was passed to replace
|
484
563
|
actual_task_description = mock_replace.call_args[0][1]
|
485
|
-
|
564
|
+
|
486
565
|
# Verify that the new context was added and the old context was replaced
|
487
566
|
# (set_context replaces all contexts with the new one)
|
488
567
|
assert "@new_context" in actual_task_description
|
489
|
-
assert
|
490
|
-
|
568
|
+
assert (
|
569
|
+
"@existing_context" not in actual_task_description
|
570
|
+
) # Old context replaced
|
571
|
+
assert (
|
572
|
+
actual_task_description.count("@new_context") == 1
|
573
|
+
) # Only one instance
|
491
574
|
|
492
575
|
def test_parse_task_components_actually_deduplicates(self):
|
493
576
|
"""Test that _parse_task_components actually removes duplicates from input."""
|
494
577
|
from todo_agent.infrastructure.todo_shell import TodoShell
|
495
|
-
|
578
|
+
|
496
579
|
# Create a TodoShell instance
|
497
580
|
shell = TodoShell("/tmp/todo.txt")
|
498
|
-
|
581
|
+
|
499
582
|
# Test with a task line that has multiple duplicates
|
500
583
|
task_line = "1 (A) Task description +project1 +project1 +project1 +project2 @context1 @context1 @context2"
|
501
584
|
components = shell._parse_task_components(task_line)
|
502
|
-
|
585
|
+
|
503
586
|
# Verify that duplicates were actually removed
|
504
587
|
assert len(components["projects"]) == 2 # Should only have 2 unique projects
|
505
588
|
assert len(components["contexts"]) == 2 # Should only have 2 unique contexts
|
506
|
-
|
589
|
+
|
507
590
|
# Verify the specific projects and contexts
|
508
591
|
assert "+project1" in components["projects"]
|
509
592
|
assert "+project2" in components["projects"]
|
510
593
|
assert "@context1" in components["contexts"]
|
511
594
|
assert "@context2" in components["contexts"]
|
512
|
-
|
595
|
+
|
513
596
|
# Verify no duplicates exist in the lists
|
514
597
|
assert components["projects"].count("+project1") == 1
|
515
598
|
assert components["projects"].count("+project2") == 1
|
@@ -219,7 +219,7 @@ class TestTodoShell:
|
|
219
219
|
sample_task = "1 (B) Call dentist +health @phone"
|
220
220
|
with patch.object(
|
221
221
|
self.todo_shell, "list_tasks", return_value=sample_task
|
222
|
-
)
|
222
|
+
), patch.object(
|
223
223
|
self.todo_shell, "replace", return_value="Task updated"
|
224
224
|
) as mock_replace:
|
225
225
|
result = self.todo_shell.set_due_date(1, "2025-01-20")
|
@@ -236,7 +236,7 @@ class TestTodoShell:
|
|
236
236
|
sample_task = "1 (C) Review quarterly report +work @office rec:weekly due:2025-01-10 custom:tag"
|
237
237
|
with patch.object(
|
238
238
|
self.todo_shell, "list_tasks", return_value=sample_task
|
239
|
-
)
|
239
|
+
), patch.object(
|
240
240
|
self.todo_shell, "replace", return_value="Task updated"
|
241
241
|
) as mock_replace:
|
242
242
|
result = self.todo_shell.set_due_date(1, "2025-01-25")
|
@@ -253,7 +253,7 @@ class TestTodoShell:
|
|
253
253
|
sample_task = "1 Buy milk +shopping @grocery"
|
254
254
|
with patch.object(
|
255
255
|
self.todo_shell, "list_tasks", return_value=sample_task
|
256
|
-
)
|
256
|
+
), patch.object(
|
257
257
|
self.todo_shell, "replace", return_value="Task updated"
|
258
258
|
) as mock_replace:
|
259
259
|
result = self.todo_shell.set_due_date(1, "2025-01-30")
|
@@ -268,9 +268,7 @@ class TestTodoShell:
|
|
268
268
|
"""Test that set_due_date raises error for invalid task number."""
|
269
269
|
# Mock the list_tasks to return only one task
|
270
270
|
sample_task = "1 Test task"
|
271
|
-
with patch.object(
|
272
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
273
|
-
) as mock_list:
|
271
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
274
272
|
with pytest.raises(TodoShellError, match="Task number 5 not found"):
|
275
273
|
self.todo_shell.set_due_date(5, "2025-01-15")
|
276
274
|
|
@@ -280,7 +278,7 @@ class TestTodoShell:
|
|
280
278
|
sample_task = "1 (A) Buy groceries +shopping @home due:2025-01-10"
|
281
279
|
with patch.object(
|
282
280
|
self.todo_shell, "list_tasks", return_value=sample_task
|
283
|
-
)
|
281
|
+
), patch.object(
|
284
282
|
self.todo_shell, "replace", return_value="Task updated"
|
285
283
|
) as mock_replace:
|
286
284
|
result = self.todo_shell.set_due_date(1, "")
|
@@ -295,7 +293,7 @@ class TestTodoShell:
|
|
295
293
|
sample_task = "1 (B) Call dentist +health @phone due:2025-01-15"
|
296
294
|
with patch.object(
|
297
295
|
self.todo_shell, "list_tasks", return_value=sample_task
|
298
|
-
)
|
296
|
+
), patch.object(
|
299
297
|
self.todo_shell, "replace", return_value="Task updated"
|
300
298
|
) as mock_replace:
|
301
299
|
result = self.todo_shell.set_due_date(1, " ")
|
@@ -310,7 +308,7 @@ class TestTodoShell:
|
|
310
308
|
sample_task = "1 Buy milk +shopping @grocery due:2025-01-20"
|
311
309
|
with patch.object(
|
312
310
|
self.todo_shell, "list_tasks", return_value=sample_task
|
313
|
-
)
|
311
|
+
), patch.object(
|
314
312
|
self.todo_shell, "replace", return_value="Task updated"
|
315
313
|
) as mock_replace:
|
316
314
|
result = self.todo_shell.set_due_date(1, "")
|
@@ -344,7 +342,7 @@ class TestTodoShell:
|
|
344
342
|
sample_task = "1 (B) Call dentist +health"
|
345
343
|
with patch.object(
|
346
344
|
self.todo_shell, "list_tasks", return_value=sample_task
|
347
|
-
)
|
345
|
+
), patch.object(
|
348
346
|
self.todo_shell, "replace", return_value="Task updated"
|
349
347
|
) as mock_replace:
|
350
348
|
result = self.todo_shell.set_context(1, "phone")
|
@@ -359,7 +357,7 @@ class TestTodoShell:
|
|
359
357
|
sample_task = "1 (C) Review quarterly report +work @office rec:weekly due:2025-01-10 custom:tag"
|
360
358
|
with patch.object(
|
361
359
|
self.todo_shell, "list_tasks", return_value=sample_task
|
362
|
-
)
|
360
|
+
), patch.object(
|
363
361
|
self.todo_shell, "replace", return_value="Task updated"
|
364
362
|
) as mock_replace:
|
365
363
|
result = self.todo_shell.set_context(1, "home")
|
@@ -375,7 +373,7 @@ class TestTodoShell:
|
|
375
373
|
sample_task = "1 Buy milk +shopping @grocery"
|
376
374
|
with patch.object(
|
377
375
|
self.todo_shell, "list_tasks", return_value=sample_task
|
378
|
-
)
|
376
|
+
), patch.object(
|
379
377
|
self.todo_shell, "replace", return_value="Task updated"
|
380
378
|
) as mock_replace:
|
381
379
|
result = self.todo_shell.set_context(1, "store")
|
@@ -390,7 +388,7 @@ class TestTodoShell:
|
|
390
388
|
sample_task = "1 (A) Buy groceries +shopping @home due:2025-01-10"
|
391
389
|
with patch.object(
|
392
390
|
self.todo_shell, "list_tasks", return_value=sample_task
|
393
|
-
)
|
391
|
+
), patch.object(
|
394
392
|
self.todo_shell, "replace", return_value="Task updated"
|
395
393
|
) as mock_replace:
|
396
394
|
result = self.todo_shell.set_context(1, "")
|
@@ -407,7 +405,7 @@ class TestTodoShell:
|
|
407
405
|
sample_task = "1 (B) Call dentist +health @phone"
|
408
406
|
with patch.object(
|
409
407
|
self.todo_shell, "list_tasks", return_value=sample_task
|
410
|
-
)
|
408
|
+
), patch.object(
|
411
409
|
self.todo_shell, "replace", return_value="Task updated"
|
412
410
|
) as mock_replace:
|
413
411
|
result = self.todo_shell.set_context(1, " ")
|
@@ -422,7 +420,7 @@ class TestTodoShell:
|
|
422
420
|
sample_task = "1 Test task +project"
|
423
421
|
with patch.object(
|
424
422
|
self.todo_shell, "list_tasks", return_value=sample_task
|
425
|
-
)
|
423
|
+
), patch.object(
|
426
424
|
self.todo_shell, "replace", return_value="Task updated"
|
427
425
|
) as mock_replace:
|
428
426
|
result = self.todo_shell.set_context(1, "@office")
|
@@ -435,9 +433,7 @@ class TestTodoShell:
|
|
435
433
|
"""Test that set_context raises error for context that becomes empty after cleaning."""
|
436
434
|
# Mock the list_tasks to return a task
|
437
435
|
sample_task = "1 Test task"
|
438
|
-
with patch.object(
|
439
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
440
|
-
) as mock_list:
|
436
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
441
437
|
with pytest.raises(TodoShellError, match="Context name cannot be empty"):
|
442
438
|
self.todo_shell.set_context(1, "@")
|
443
439
|
|
@@ -445,9 +441,7 @@ class TestTodoShell:
|
|
445
441
|
"""Test that set_context raises error for invalid task number."""
|
446
442
|
# Mock the list_tasks to return only one task
|
447
443
|
sample_task = "1 Test task"
|
448
|
-
with patch.object(
|
449
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
450
|
-
) as mock_list:
|
444
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
451
445
|
with pytest.raises(TodoShellError, match="Task number 5 not found"):
|
452
446
|
self.todo_shell.set_context(5, "office")
|
453
447
|
|
@@ -590,7 +584,7 @@ class TestTodoShell:
|
|
590
584
|
sample_task = "1 (A) Buy groceries @home due:2025-01-10"
|
591
585
|
with patch.object(
|
592
586
|
self.todo_shell, "list_tasks", return_value=sample_task
|
593
|
-
)
|
587
|
+
), patch.object(
|
594
588
|
self.todo_shell, "replace", return_value="Task updated"
|
595
589
|
) as mock_replace:
|
596
590
|
result = self.todo_shell.set_project(1, ["shopping", "errands"])
|
@@ -607,7 +601,7 @@ class TestTodoShell:
|
|
607
601
|
sample_task = "1 (B) Call dentist +health @phone"
|
608
602
|
with patch.object(
|
609
603
|
self.todo_shell, "list_tasks", return_value=sample_task
|
610
|
-
)
|
604
|
+
), patch.object(
|
611
605
|
self.todo_shell, "replace", return_value="Task updated"
|
612
606
|
) as mock_replace:
|
613
607
|
result = self.todo_shell.set_project(1, ["appointment", "personal"])
|
@@ -624,7 +618,7 @@ class TestTodoShell:
|
|
624
618
|
sample_task = "1 (C) Review report +work +urgent +review @office"
|
625
619
|
with patch.object(
|
626
620
|
self.todo_shell, "list_tasks", return_value=sample_task
|
627
|
-
)
|
621
|
+
), patch.object(
|
628
622
|
self.todo_shell, "replace", return_value="Task updated"
|
629
623
|
) as mock_replace:
|
630
624
|
result = self.todo_shell.set_project(1, ["-urgent", "-review"])
|
@@ -639,7 +633,7 @@ class TestTodoShell:
|
|
639
633
|
sample_task = "1 (A) Task with projects +old +keep @context"
|
640
634
|
with patch.object(
|
641
635
|
self.todo_shell, "list_tasks", return_value=sample_task
|
642
|
-
)
|
636
|
+
), patch.object(
|
643
637
|
self.todo_shell, "replace", return_value="Task updated"
|
644
638
|
) as mock_replace:
|
645
639
|
result = self.todo_shell.set_project(1, ["-old", "new", "another"])
|
@@ -656,7 +650,7 @@ class TestTodoShell:
|
|
656
650
|
sample_task = "1 (B) Test task +existing @context"
|
657
651
|
with patch.object(
|
658
652
|
self.todo_shell, "list_tasks", return_value=sample_task
|
659
|
-
)
|
653
|
+
), patch.object(
|
660
654
|
self.todo_shell, "replace", return_value="Task updated"
|
661
655
|
) as mock_replace:
|
662
656
|
result = self.todo_shell.set_project(1, [])
|
@@ -672,7 +666,7 @@ class TestTodoShell:
|
|
672
666
|
sample_task = "1 (C) Test task +existing @context"
|
673
667
|
with patch.object(
|
674
668
|
self.todo_shell, "list_tasks", return_value=sample_task
|
675
|
-
)
|
669
|
+
), patch.object(
|
676
670
|
self.todo_shell, "replace", return_value="Task updated"
|
677
671
|
) as mock_replace:
|
678
672
|
result = self.todo_shell.set_project(1, ["", "valid", " ", "another"])
|
@@ -689,7 +683,7 @@ class TestTodoShell:
|
|
689
683
|
sample_task = "1 Test task @context"
|
690
684
|
with patch.object(
|
691
685
|
self.todo_shell, "list_tasks", return_value=sample_task
|
692
|
-
)
|
686
|
+
), patch.object(
|
693
687
|
self.todo_shell, "replace", return_value="Task updated"
|
694
688
|
) as mock_replace:
|
695
689
|
result = self.todo_shell.set_project(1, ["+work", "+home"])
|
@@ -704,7 +698,7 @@ class TestTodoShell:
|
|
704
698
|
sample_task = "1 Test task +work +home +shopping @context"
|
705
699
|
with patch.object(
|
706
700
|
self.todo_shell, "list_tasks", return_value=sample_task
|
707
|
-
)
|
701
|
+
), patch.object(
|
708
702
|
self.todo_shell, "replace", return_value="Task updated"
|
709
703
|
) as mock_replace:
|
710
704
|
result = self.todo_shell.set_project(1, ["-+work", "-+shopping"])
|
@@ -716,10 +710,12 @@ class TestTodoShell:
|
|
716
710
|
def test_set_project_preserves_all_other_components(self):
|
717
711
|
"""Test that set_project preserves all task components when modifying projects."""
|
718
712
|
# Mock the list_tasks to return a complex task
|
719
|
-
sample_task =
|
713
|
+
sample_task = (
|
714
|
+
"1 (A) Complex task +old +keep @office rec:weekly due:2025-01-15 custom:tag"
|
715
|
+
)
|
720
716
|
with patch.object(
|
721
717
|
self.todo_shell, "list_tasks", return_value=sample_task
|
722
|
-
)
|
718
|
+
), patch.object(
|
723
719
|
self.todo_shell, "replace", return_value="Task updated"
|
724
720
|
) as mock_replace:
|
725
721
|
result = self.todo_shell.set_project(1, ["-old", "new"])
|
@@ -733,9 +729,7 @@ class TestTodoShell:
|
|
733
729
|
"""Test that set_project raises error for project that becomes empty after cleaning."""
|
734
730
|
# Mock the list_tasks to return a task
|
735
731
|
sample_task = "1 Test task"
|
736
|
-
with patch.object(
|
737
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
738
|
-
) as mock_list:
|
732
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
739
733
|
with pytest.raises(TodoShellError, match="Project name cannot be empty"):
|
740
734
|
self.todo_shell.set_project(1, ["+"])
|
741
735
|
|
@@ -743,9 +737,7 @@ class TestTodoShell:
|
|
743
737
|
"""Test that set_project raises error for remove project that becomes empty after cleaning."""
|
744
738
|
# Mock the list_tasks to return a task
|
745
739
|
sample_task = "1 Test task"
|
746
|
-
with patch.object(
|
747
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
748
|
-
) as mock_list:
|
740
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
749
741
|
with pytest.raises(TodoShellError, match="Project name cannot be empty"):
|
750
742
|
self.todo_shell.set_project(1, ["-+"])
|
751
743
|
|
@@ -753,9 +745,7 @@ class TestTodoShell:
|
|
753
745
|
"""Test that set_project raises error for invalid task number."""
|
754
746
|
# Mock the list_tasks to return only one task
|
755
747
|
sample_task = "1 Test task"
|
756
|
-
with patch.object(
|
757
|
-
self.todo_shell, "list_tasks", return_value=sample_task
|
758
|
-
) as mock_list:
|
748
|
+
with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
|
759
749
|
with pytest.raises(TodoShellError, match="Task number 5 not found"):
|
760
750
|
self.todo_shell.set_project(5, ["work"])
|
761
751
|
|
@@ -765,7 +755,7 @@ class TestTodoShell:
|
|
765
755
|
sample_task = "1 Test task +work @context"
|
766
756
|
with patch.object(
|
767
757
|
self.todo_shell, "list_tasks", return_value=sample_task
|
768
|
-
)
|
758
|
+
), patch.object(
|
769
759
|
self.todo_shell, "replace", return_value="Task updated"
|
770
760
|
) as mock_replace:
|
771
761
|
result = self.todo_shell.set_project(1, ["work", "new"])
|