todo-agent 0.2.9__tar.gz → 0.3.2__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.
Files changed (66) hide show
  1. {todo_agent-0.2.9 → todo_agent-0.3.2}/PKG-INFO +3 -3
  2. {todo_agent-0.2.9 → todo_agent-0.3.2}/README.md +2 -2
  3. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_core/test_conversation_manager.py +1 -1
  4. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_core/test_todo_manager.py +241 -111
  5. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_calendar_utils.py +2 -2
  6. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_inference.py +4 -4
  7. todo_agent-0.3.2/tests/test_infrastructure/test_ollama_client.py +211 -0
  8. todo_agent-0.3.2/tests/test_infrastructure/test_openrouter_client.py +273 -0
  9. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_todo_shell.py +45 -51
  10. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_interface/test_cli.py +15 -7
  11. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_interface/test_tools.py +27 -0
  12. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/_version.py +3 -3
  13. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/core/conversation_manager.py +1 -1
  14. todo_agent-0.3.2/todo_agent/core/exceptions.py +78 -0
  15. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/core/todo_manager.py +142 -44
  16. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/calendar_utils.py +2 -4
  17. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/inference.py +99 -53
  18. todo_agent-0.3.2/todo_agent/infrastructure/llm_client.py +285 -0
  19. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/ollama_client.py +68 -77
  20. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/openrouter_client.py +75 -78
  21. todo_agent-0.3.2/todo_agent/infrastructure/prompts/system_prompt.txt +441 -0
  22. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/todo_shell.py +28 -28
  23. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/interface/cli.py +110 -18
  24. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/interface/formatters.py +22 -0
  25. todo_agent-0.3.2/todo_agent/interface/progress.py +58 -0
  26. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/interface/tools.py +211 -139
  27. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/PKG-INFO +3 -3
  28. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/SOURCES.txt +1 -0
  29. todo_agent-0.2.9/tests/test_infrastructure/test_ollama_client.py +0 -139
  30. todo_agent-0.2.9/tests/test_infrastructure/test_openrouter_client.py +0 -292
  31. todo_agent-0.2.9/todo_agent/core/exceptions.py +0 -27
  32. todo_agent-0.2.9/todo_agent/infrastructure/llm_client.py +0 -62
  33. todo_agent-0.2.9/todo_agent/infrastructure/prompts/system_prompt.txt +0 -413
  34. {todo_agent-0.2.9 → todo_agent-0.3.2}/.gitignore +0 -0
  35. {todo_agent-0.2.9 → todo_agent-0.3.2}/LICENSE +0 -0
  36. {todo_agent-0.2.9 → todo_agent-0.3.2}/MANIFEST.in +0 -0
  37. {todo_agent-0.2.9 → todo_agent-0.3.2}/Makefile +0 -0
  38. {todo_agent-0.2.9 → todo_agent-0.3.2}/docs/publishing.md +0 -0
  39. {todo_agent-0.2.9 → todo_agent-0.3.2}/pyproject.toml +0 -0
  40. {todo_agent-0.2.9 → todo_agent-0.3.2}/requirements-dev.txt +0 -0
  41. {todo_agent-0.2.9 → todo_agent-0.3.2}/requirements.txt +0 -0
  42. {todo_agent-0.2.9 → todo_agent-0.3.2}/setup.cfg +0 -0
  43. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/__init__.py +0 -0
  44. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_core/__init__.py +0 -0
  45. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/__init__.py +0 -0
  46. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_config.py +0 -0
  47. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  48. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_infrastructure/test_token_counter.py +0 -0
  49. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_interface/__init__.py +0 -0
  50. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_interface/test_formatters.py +0 -0
  51. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_linting.py +0 -0
  52. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_logger.py +0 -0
  53. {todo_agent-0.2.9 → todo_agent-0.3.2}/tests/test_main.py +0 -0
  54. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/__init__.py +0 -0
  55. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/core/__init__.py +0 -0
  56. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/__init__.py +0 -0
  57. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/config.py +0 -0
  58. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  59. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/logger.py +0 -0
  60. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/infrastructure/token_counter.py +0 -0
  61. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/interface/__init__.py +0 -0
  62. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent/main.py +0 -0
  63. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/dependency_links.txt +0 -0
  64. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/entry_points.txt +0 -0
  65. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/requires.txt +0 -0
  66. {todo_agent-0.2.9 → todo_agent-0.3.2}/todo_agent.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.9
3
+ Version: 0.3.2
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -68,7 +68,7 @@ todo-agent "show my work tasks"
68
68
 
69
69
  **Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
70
70
 
71
- **Work smarter** with automatic duplicate detection, recurring task handling, and calendar-aware scheduling.
71
+ **Work smarter** with automatic duplicate detection and calendar-aware scheduling.
72
72
 
73
73
  **Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
74
74
 
@@ -163,7 +163,7 @@ todo-agent "what tasks are blocking other work?"
163
163
  ### Natural Language Intelligence
164
164
  ```bash
165
165
  todo-agent "add dentist appointment next Monday"
166
- todo-agent "set up recurring daily vitamin reminder"
166
+
167
167
  todo-agent "move all completed tasks to archive"
168
168
  todo-agent "show me tasks I can do from home"
169
169
  ```
@@ -21,7 +21,7 @@ todo-agent "show my work tasks"
21
21
 
22
22
  **Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
23
23
 
24
- **Work smarter** with automatic duplicate detection, recurring task handling, and calendar-aware scheduling.
24
+ **Work smarter** with automatic duplicate detection and calendar-aware scheduling.
25
25
 
26
26
  **Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
27
27
 
@@ -116,7 +116,7 @@ todo-agent "what tasks are blocking other work?"
116
116
  ### Natural Language Intelligence
117
117
  ```bash
118
118
  todo-agent "add dentist appointment next Monday"
119
- todo-agent "set up recurring daily vitamin reminder"
119
+
120
120
  todo-agent "move all completed tasks to archive"
121
121
  todo-agent "show me tasks I can do from home"
122
122
  ```
@@ -14,7 +14,7 @@ class TestConversationManager:
14
14
  """Test ConversationManager initialization."""
15
15
  manager = ConversationManager()
16
16
  assert len(manager.history) == 0
17
- assert manager.max_tokens == 32000
17
+ assert manager.max_tokens == 64000
18
18
  assert manager.max_messages == 50
19
19
  assert manager.system_prompt is None
20
20
 
@@ -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
@@ -149,46 +148,48 @@ class TestTodoManager(unittest.TestCase):
149
148
  self.todo_manager.set_due_date(1, "2025/1/5")
150
149
  self.assertIn("Invalid due date format", str(context.exception))
151
150
 
152
- def test_add_task_with_valid_recurring_daily(self):
153
- """Test adding a task with valid daily recurring format."""
154
- self.todo_shell.add.return_value = "Test task"
155
- result = self.todo_manager.add_task("Test task", recurring="rec:daily")
156
- self.assertEqual(result, "Added task: Test task rec:daily")
157
- self.todo_shell.add.assert_called_once_with("Test task rec:daily")
158
-
159
- def test_add_task_with_valid_recurring_weekly_interval(self):
160
- """Test adding a task with valid weekly recurring format with interval."""
161
- self.todo_shell.add.return_value = "Test task"
162
- result = self.todo_manager.add_task("Test task", recurring="rec:weekly:2")
163
- self.assertEqual(result, "Added task: Test task rec:weekly:2")
164
- self.todo_shell.add.assert_called_once_with("Test task rec:weekly:2")
151
+ def test_add_task_with_invalid_duration_format(self):
152
+ """Test adding a task with invalid duration format raises ValueError."""
153
+ with self.assertRaises(ValueError) as context:
154
+ self.todo_manager.add_task("Test task", duration="30s")
155
+ self.assertIn("Invalid duration format", str(context.exception))
165
156
 
166
- def test_add_task_with_invalid_recurring_format(self):
167
- """Test adding a task with invalid recurring format raises ValueError."""
157
+ def test_add_task_with_invalid_duration_unit(self):
158
+ """Test adding a task with invalid duration unit raises ValueError."""
168
159
  with self.assertRaises(ValueError) as context:
169
- self.todo_manager.add_task("Test task", recurring="invalid")
170
- self.assertIn("Invalid recurring format", str(context.exception))
160
+ self.todo_manager.add_task("Test task", duration="30s")
161
+ self.assertIn("Invalid duration format", str(context.exception))
171
162
 
172
- def test_add_task_with_invalid_recurring_frequency(self):
173
- """Test adding a task with invalid recurring frequency raises ValueError."""
163
+ def test_add_task_with_invalid_duration_value(self):
164
+ """Test adding a task with invalid duration value raises ValueError."""
174
165
  with self.assertRaises(ValueError) as context:
175
- self.todo_manager.add_task("Test task", recurring="rec:invalid")
176
- self.assertIn("Invalid frequency", str(context.exception))
166
+ self.todo_manager.add_task("Test task", duration="0m")
167
+ self.assertIn("Must be a positive number", str(context.exception))
177
168
 
178
- def test_add_task_with_invalid_recurring_interval(self):
179
- """Test adding a task with invalid recurring interval raises ValueError."""
169
+ def test_add_task_with_negative_duration(self):
170
+ """Test adding a task with negative duration raises ValueError."""
180
171
  with self.assertRaises(ValueError) as context:
181
- self.todo_manager.add_task("Test task", recurring="rec:weekly:invalid")
182
- self.assertIn("Invalid interval", str(context.exception))
172
+ self.todo_manager.add_task("Test task", duration="-30m")
173
+ self.assertIn("Must be a positive number", str(context.exception))
183
174
 
184
- def test_add_task_with_zero_recurring_interval(self):
185
- """Test adding a task with zero recurring interval raises ValueError."""
175
+ def test_add_task_with_empty_duration(self):
176
+ """Test adding a task with empty duration raises ValueError."""
186
177
  with self.assertRaises(ValueError) as context:
187
- self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
188
- self.assertIn("Must be a positive integer", str(context.exception))
178
+ self.todo_manager.add_task("Test task", duration="")
179
+ self.assertIn("Duration must be a non-empty string", str(context.exception))
180
+
181
+ def test_add_task_with_duration(self):
182
+ """Test adding a task with duration parameter."""
183
+ self.todo_shell.add.return_value = "Test task"
184
+ result = self.todo_manager.add_task(
185
+ "Test task",
186
+ duration="30m",
187
+ )
188
+ self.assertEqual(result, "Added task: Test task duration:30m")
189
+ self.todo_shell.add.assert_called_once_with("Test task duration:30m")
189
190
 
190
- def test_add_task_with_all_parameters_including_recurring(self):
191
- """Test adding a task with all parameters including recurring."""
191
+ def test_add_task_with_all_parameters_including_duration(self):
192
+ """Test adding a task with all parameters including duration."""
192
193
  self.todo_shell.add.return_value = "Test task"
193
194
  result = self.todo_manager.add_task(
194
195
  "Test task",
@@ -196,13 +197,14 @@ class TestTodoManager(unittest.TestCase):
196
197
  project="work",
197
198
  context="office",
198
199
  due="2024-01-15",
199
- recurring="rec:daily",
200
+ duration="2h",
200
201
  )
201
202
  self.assertEqual(
202
- result, "Added task: (A) Test task +work @office due:2024-01-15 rec:daily"
203
+ result,
204
+ "Added task: (A) Test task +work @office due:2024-01-15 duration:2h",
203
205
  )
204
206
  self.todo_shell.add.assert_called_once_with(
205
- "(A) Test task +work @office due:2024-01-15 rec:daily"
207
+ "(A) Test task +work @office due:2024-01-15 duration:2h"
206
208
  )
207
209
 
208
210
  def test_list_tasks(self):
@@ -299,60 +301,62 @@ class TestTodoManager(unittest.TestCase):
299
301
  self.assertEqual(result, "No completed tasks found matching the criteria.")
300
302
  self.todo_shell.list_completed.assert_called_once_with("+nonexistent")
301
303
 
302
-
303
-
304
304
  def test_add_task_sanitizes_inputs(self):
305
305
  """Test that add_task sanitizes inputs to prevent duplicates."""
306
306
  # Mock the todo_shell
307
307
  mock_shell = Mock()
308
308
  mock_shell.add.return_value = "1 Test task +project1 @context1"
309
-
309
+
310
310
  manager = TodoManager(mock_shell)
311
-
311
+
312
312
  # Test with inputs that already have + and @ symbols
313
313
  result = manager.add_task(
314
314
  description="Test task",
315
315
  project="+project1", # Already has + symbol
316
- context="@context1" # Already has @ symbol
316
+ context="@context1", # Already has @ symbol
317
317
  )
318
-
318
+
319
319
  # Verify the task was added with properly formatted projects and contexts
320
320
  mock_shell.add.assert_called_once()
321
321
  call_args = mock_shell.add.call_args[0][0]
322
-
322
+
323
323
  # Should have exactly one instance of each (sanitization prevents duplicates)
324
324
  assert call_args.count("+project1") == 1
325
325
  assert call_args.count("@context1") == 1
326
-
326
+
327
327
  # Verify the result message
328
328
  assert "Added task:" in result
329
329
 
330
-
331
-
332
330
  def test_parse_task_components_deduplicates(self):
333
331
  """Test that _parse_task_components deduplicates projects and contexts."""
334
332
  from todo_agent.infrastructure.todo_shell import TodoShell
335
-
333
+
336
334
  # Create a TodoShell instance
337
335
  shell = TodoShell("/tmp/todo.txt")
338
-
336
+
339
337
  # Test with duplicate projects and contexts in the task line
340
338
  task_line = "1 (A) Task description +project1 +project1 +project2 @context1 @context1 @context2"
341
339
  components = shell._parse_task_components(task_line)
342
-
340
+
343
341
  # Verify deduplication
344
- assert components["projects"] == ["+project1", "+project2"] # Sorted and deduplicated
345
- assert components["contexts"] == ["@context1", "@context2"] # Sorted and deduplicated
342
+ assert components["projects"] == [
343
+ "+project1",
344
+ "+project2",
345
+ ] # Sorted and deduplicated
346
+ assert components["contexts"] == [
347
+ "@context1",
348
+ "@context2",
349
+ ] # Sorted and deduplicated
346
350
  assert components["priority"] == "A"
347
351
  assert components["description"] == "Task description"
348
352
 
349
353
  def test_reconstruct_task_maintains_deduplication(self):
350
354
  """Test that _reconstruct_task maintains deduplication."""
351
355
  from todo_agent.infrastructure.todo_shell import TodoShell
352
-
356
+
353
357
  # Create a TodoShell instance
354
358
  shell = TodoShell("/tmp/todo.txt")
355
-
359
+
356
360
  # Create components with potential duplicates
357
361
  components = {
358
362
  "priority": "A",
@@ -360,13 +364,12 @@ class TestTodoManager(unittest.TestCase):
360
364
  "projects": ["+project1", "+project1", "+project2"], # Duplicate project1
361
365
  "contexts": ["@context1", "@context1", "@context2"], # Duplicate context1
362
366
  "due": "2024-01-01",
363
- "recurring": None,
364
- "other_tags": []
367
+ "other_tags": [],
365
368
  }
366
-
369
+
367
370
  # Reconstruct the task
368
371
  reconstructed = shell._reconstruct_task(components)
369
-
372
+
370
373
  # Verify that duplicates are preserved in reconstruction (since we handle deduplication in parsing)
371
374
  # This is expected behavior - reconstruction should preserve the exact components given
372
375
  assert reconstructed.count("+project1") == 2
@@ -376,83 +379,96 @@ class TestTodoManager(unittest.TestCase):
376
379
 
377
380
  def test_todo_shell_set_project_actually_prevents_duplicates(self):
378
381
  """Test that the todo_shell set_project method actually prevents duplicates."""
379
- from todo_agent.infrastructure.todo_shell import TodoShell
380
382
  from unittest.mock import patch
381
-
383
+
384
+ from todo_agent.infrastructure.todo_shell import TodoShell
385
+
382
386
  # Create a TodoShell instance
383
387
  shell = TodoShell("/tmp/todo.txt")
384
-
388
+
385
389
  # Mock the list_tasks method to return a task with existing projects
386
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
387
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
388
-
390
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
391
+ mock_list_tasks.return_value = (
392
+ "1 (A) Existing task +existing_project @existing_context"
393
+ )
394
+
389
395
  # Mock the replace method to capture what gets called
390
- with patch.object(shell, 'replace') as mock_replace:
391
- mock_replace.return_value = "1 (A) Existing task +existing_project @existing_context"
392
-
396
+ with patch.object(shell, "replace") as mock_replace:
397
+ mock_replace.return_value = (
398
+ "1 (A) Existing task +existing_project @existing_context"
399
+ )
400
+
393
401
  # Try to add a project that already exists
394
402
  result = shell.set_project(1, ["existing_project"])
395
-
403
+
396
404
  # Since the project already exists, replace should NOT be called
397
405
  # (our deduplication logic prevents unnecessary updates)
398
406
  mock_replace.assert_not_called()
399
-
407
+
400
408
  # The result should be the reconstructed task without duplicates
401
409
  assert "+existing_project" in result
402
410
  assert result.count("+existing_project") == 1 # Only one instance
403
411
 
404
412
  def test_todo_shell_set_context_actually_prevents_duplicates(self):
405
413
  """Test that the todo_shell set_context method actually prevents duplicates."""
406
- from todo_agent.infrastructure.todo_shell import TodoShell
407
414
  from unittest.mock import patch
408
-
415
+
416
+ from todo_agent.infrastructure.todo_shell import TodoShell
417
+
409
418
  # Create a TodoShell instance
410
419
  shell = TodoShell("/tmp/todo.txt")
411
-
420
+
412
421
  # Mock the list_tasks method to return a task with existing context
413
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
414
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
415
-
422
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
423
+ mock_list_tasks.return_value = (
424
+ "1 (A) Existing task +existing_project @existing_context"
425
+ )
426
+
416
427
  # Mock the replace method to capture what gets called
417
- with patch.object(shell, 'replace') as mock_replace:
418
- mock_replace.return_value = "1 (A) Existing task +existing_project @existing_context"
419
-
428
+ with patch.object(shell, "replace") as mock_replace:
429
+ mock_replace.return_value = (
430
+ "1 (A) Existing task +existing_project @existing_context"
431
+ )
432
+
420
433
  # Try to set a context that already exists
421
434
  result = shell.set_context(1, "existing_context")
422
-
435
+
423
436
  # Since the context already exists, replace should NOT be called
424
437
  # (our deduplication logic prevents unnecessary updates)
425
438
  mock_replace.assert_not_called()
426
-
439
+
427
440
  # The result should be the reconstructed task without duplicates
428
441
  assert "@existing_context" in result
429
442
  assert result.count("@existing_context") == 1 # Only one instance
430
443
 
431
444
  def test_todo_shell_set_project_adds_new_projects(self):
432
445
  """Test that the todo_shell set_project method adds new projects and prevents duplicates."""
433
- from todo_agent.infrastructure.todo_shell import TodoShell
434
446
  from unittest.mock import patch
435
-
447
+
448
+ from todo_agent.infrastructure.todo_shell import TodoShell
449
+
436
450
  # Create a TodoShell instance
437
451
  shell = TodoShell("/tmp/todo.txt")
438
-
452
+
439
453
  # Mock the list_tasks method to return a task with existing projects
440
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
441
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
442
-
454
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
455
+ mock_list_tasks.return_value = (
456
+ "1 (A) Existing task +existing_project @existing_context"
457
+ )
458
+
443
459
  # Mock the replace method to capture what gets called
444
- with patch.object(shell, 'replace') as mock_replace:
460
+ with patch.object(shell, "replace") as mock_replace:
445
461
  mock_replace.return_value = "1 (A) Existing task +existing_project +new_project @existing_context"
446
-
462
+
447
463
  # Try to add a new project
448
- result = shell.set_project(1, ["new_project"])
449
-
464
+ shell.set_project(1, ["new_project"])
465
+
450
466
  # Since we're adding a new project, replace should be called
451
467
  mock_replace.assert_called_once()
452
-
468
+
453
469
  # Get the actual task description that was passed to replace
454
470
  actual_task_description = mock_replace.call_args[0][1]
455
-
471
+
456
472
  # Verify that the new project was added and no duplicates exist
457
473
  assert "+new_project" in actual_task_description
458
474
  assert actual_task_description.count("+existing_project") == 1
@@ -460,62 +476,176 @@ class TestTodoManager(unittest.TestCase):
460
476
 
461
477
  def test_todo_shell_set_context_adds_new_contexts(self):
462
478
  """Test that the todo_shell set_context method adds new contexts and prevents duplicates."""
463
- from todo_agent.infrastructure.todo_shell import TodoShell
464
479
  from unittest.mock import patch
465
-
480
+
481
+ from todo_agent.infrastructure.todo_shell import TodoShell
482
+
466
483
  # Create a TodoShell instance
467
484
  shell = TodoShell("/tmp/todo.txt")
468
-
485
+
469
486
  # Mock the list_tasks method to return a task with existing context
470
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
471
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
472
-
487
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
488
+ mock_list_tasks.return_value = (
489
+ "1 (A) Existing task +existing_project @existing_context"
490
+ )
491
+
473
492
  # Mock the replace method to capture what gets called
474
- with patch.object(shell, 'replace') as mock_replace:
475
- mock_replace.return_value = "1 (A) Existing task +existing_project @new_context"
476
-
493
+ with patch.object(shell, "replace") as mock_replace:
494
+ mock_replace.return_value = (
495
+ "1 (A) Existing task +existing_project @new_context"
496
+ )
497
+
477
498
  # Try to set a new context
478
- result = shell.set_context(1, "new_context")
479
-
499
+ shell.set_context(1, "new_context")
500
+
480
501
  # Since we're adding a new context, replace should be called
481
502
  mock_replace.assert_called_once()
482
-
503
+
483
504
  # Get the actual task description that was passed to replace
484
505
  actual_task_description = mock_replace.call_args[0][1]
485
-
506
+
486
507
  # Verify that the new context was added and the old context was replaced
487
508
  # (set_context replaces all contexts with the new one)
488
509
  assert "@new_context" in actual_task_description
489
- assert "@existing_context" not in actual_task_description # Old context replaced
490
- assert actual_task_description.count("@new_context") == 1 # Only one instance
510
+ assert (
511
+ "@existing_context" not in actual_task_description
512
+ ) # Old context replaced
513
+ assert (
514
+ actual_task_description.count("@new_context") == 1
515
+ ) # Only one instance
491
516
 
492
517
  def test_parse_task_components_actually_deduplicates(self):
493
518
  """Test that _parse_task_components actually removes duplicates from input."""
494
519
  from todo_agent.infrastructure.todo_shell import TodoShell
495
-
520
+
496
521
  # Create a TodoShell instance
497
522
  shell = TodoShell("/tmp/todo.txt")
498
-
523
+
499
524
  # Test with a task line that has multiple duplicates
500
525
  task_line = "1 (A) Task description +project1 +project1 +project1 +project2 @context1 @context1 @context2"
501
526
  components = shell._parse_task_components(task_line)
502
-
527
+
503
528
  # Verify that duplicates were actually removed
504
529
  assert len(components["projects"]) == 2 # Should only have 2 unique projects
505
530
  assert len(components["contexts"]) == 2 # Should only have 2 unique contexts
506
-
531
+
507
532
  # Verify the specific projects and contexts
508
533
  assert "+project1" in components["projects"]
509
534
  assert "+project2" in components["projects"]
510
535
  assert "@context1" in components["contexts"]
511
536
  assert "@context2" in components["contexts"]
512
-
537
+
513
538
  # Verify no duplicates exist in the lists
514
539
  assert components["projects"].count("+project1") == 1
515
540
  assert components["projects"].count("+project2") == 1
516
541
  assert components["contexts"].count("@context1") == 1
517
542
  assert components["contexts"].count("@context2") == 1
518
543
 
544
+ def test_get_current_datetime(self):
545
+ """Test getting current date and time."""
546
+ with patch("todo_agent.core.todo_manager.datetime") as mock_datetime:
547
+ mock_now = Mock()
548
+ mock_now.strftime.side_effect = lambda fmt: {
549
+ "%Y-%m-%d %H:%M:%S": "2025-01-15 10:30:00",
550
+ "%A, %B %d, %Y at %I:%M %p": "Wednesday, January 15, 2025 at 10:30 AM",
551
+ }[fmt]
552
+ mock_now.isocalendar.return_value = (2025, 3, 3)
553
+ mock_now.astimezone.return_value.tzinfo = Mock()
554
+ mock_datetime.now.return_value = mock_now
555
+
556
+ result = self.todo_manager.get_current_datetime()
557
+ self.assertIn("2025-01-15 10:30:00", result)
558
+ self.assertIn("Week 3", result)
559
+
560
+ def test_created_completed_task_basic(self):
561
+ """Test creating and immediately completing a task."""
562
+ # Mock the todo_shell methods
563
+ self.todo_shell.add.return_value = "Task added"
564
+ self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
565
+ self.todo_shell.complete.return_value = "Task completed"
566
+
567
+ result = self.todo_manager.created_completed_task("Test task")
568
+
569
+ # Verify the task was added
570
+ self.todo_shell.add.assert_called_once_with("Test task")
571
+ # Verify the task was completed (should find task number 1 since it contains "Test task")
572
+ self.todo_shell.complete.assert_called_once_with(1)
573
+ # Verify the result message
574
+ self.assertIn("Created and completed task: Test task", result)
575
+
576
+ def test_created_completed_task_with_project_and_context(self):
577
+ """Test creating and completing a task with project and context."""
578
+ self.todo_shell.add.return_value = "Task added"
579
+ self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
580
+ self.todo_shell.complete.return_value = "Task completed"
581
+
582
+ result = self.todo_manager.created_completed_task(
583
+ "Test task", project="work", context="office"
584
+ )
585
+
586
+ # Verify the task was added with project and context
587
+ self.todo_shell.add.assert_called_once_with("Test task +work @office")
588
+ # Verify the task was completed (should find task number 1 since it contains "Test task")
589
+ self.todo_shell.complete.assert_called_once_with(1)
590
+ self.assertIn("+work @office", result)
591
+
592
+ def test_created_completed_task_with_custom_date(self):
593
+ """Test creating and completing a task with a custom completion date."""
594
+ self.todo_shell.add.return_value = "Task added"
595
+ self.todo_shell.list_tasks.return_value = "1 Test task"
596
+ self.todo_shell.complete.return_value = "Task completed"
597
+
598
+ result = self.todo_manager.created_completed_task(
599
+ "Test task", completion_date="2025-01-10"
600
+ )
601
+
602
+ # Verify the task was completed (should find task number 1 since it contains "Test task")
603
+ self.todo_shell.complete.assert_called_once_with(1)
604
+ self.assertIn("completed on 2025-01-10", result)
605
+
606
+ def test_created_completed_task_with_invalid_date(self):
607
+ """Test that invalid completion date raises ValueError."""
608
+ with self.assertRaises(ValueError) as context:
609
+ self.todo_manager.created_completed_task(
610
+ "Test task", completion_date="invalid-date"
611
+ )
612
+ self.assertIn("Invalid completion date format", str(context.exception))
613
+
614
+ def test_created_completed_task_sanitizes_project_and_context(self):
615
+ """Test that project and context with existing symbols are properly sanitized."""
616
+ self.todo_shell.add.return_value = "Task added"
617
+ self.todo_shell.list_tasks.return_value = "1 Test task"
618
+ self.todo_shell.complete.return_value = "Task completed"
619
+
620
+ result = self.todo_manager.created_completed_task(
621
+ "Test task", project="+work", context="@office"
622
+ )
623
+
624
+ # Verify the task was added with properly sanitized project and context
625
+ self.todo_shell.add.assert_called_once_with("Test task +work @office")
626
+ self.assertIn("+work @office", result)
627
+
628
+ def test_created_completed_task_with_empty_project_after_sanitization(self):
629
+ """Test that empty project after sanitization raises ValueError."""
630
+ with self.assertRaises(ValueError) as context:
631
+ self.todo_manager.created_completed_task("Test task", project="+")
632
+ self.assertIn("Project name cannot be empty", str(context.exception))
633
+
634
+ def test_created_completed_task_with_empty_context_after_sanitization(self):
635
+ """Test that empty context after sanitization raises ValueError."""
636
+ with self.assertRaises(ValueError) as context:
637
+ self.todo_manager.created_completed_task("Test task", context="@")
638
+ self.assertIn("Context name cannot be empty", str(context.exception))
639
+
640
+ def test_created_completed_task_no_tasks_after_addition(self):
641
+ """Test that RuntimeError is raised when no tasks are found after addition."""
642
+ self.todo_shell.add.return_value = "Task added"
643
+ self.todo_shell.list_tasks.return_value = "" # No tasks found
644
+
645
+ with self.assertRaises(RuntimeError) as context:
646
+ self.todo_manager.created_completed_task("Test task")
647
+ self.assertIn("Failed to add task", str(context.exception))
648
+
519
649
 
520
650
  if __name__ == "__main__":
521
651
  unittest.main()
@@ -17,7 +17,7 @@ class TestCalendarUtils:
17
17
 
18
18
  def test_get_calendar_output_success(self):
19
19
  """Test successful calendar output generation."""
20
- mock_output = "Calendar output from cal -3"
20
+ mock_output = "Calendar output from cal"
21
21
 
22
22
  with patch("subprocess.run") as mock_run:
23
23
  mock_result = Mock()
@@ -29,7 +29,7 @@ class TestCalendarUtils:
29
29
  assert result == mock_output
30
30
  assert len(result) > 0
31
31
  mock_run.assert_called_once_with(
32
- ["cal", "-3"], capture_output=True, text=True, check=True
32
+ ["cal"], capture_output=True, text=True, check=True
33
33
  )
34
34
 
35
35
  def test_get_calendar_output_fallback(self):
@@ -104,7 +104,7 @@ class TestInference:
104
104
  def test_system_prompt_datetime_interpolation(self):
105
105
  """Test that current datetime is properly interpolated into system prompt."""
106
106
  # Mock the file reading
107
- mock_prompt_content = "CURRENT DATE/TIME: {current_datetime}\n{tools_section}"
107
+ mock_prompt_content = "CURRENT DATE/TIME: {current_datetime}\nCALENDAR: {calendar_output}"
108
108
 
109
109
  with patch("builtins.open", mock_open(read_data=mock_prompt_content)):
110
110
  # Call the method that loads the system prompt
@@ -122,9 +122,9 @@ class TestInference:
122
122
  "System prompt should contain properly formatted datetime"
123
123
  )
124
124
 
125
- # Verify that tools_section is also interpolated
126
- assert "{tools_section}" not in result
127
- assert "list_tasks" in result # Should contain tool information
125
+ # Verify that calendar_output is also interpolated
126
+ assert "{calendar_output}" not in result
127
+ assert "September" in result # Should contain calendar month information
128
128
 
129
129
  def test_process_request_with_tool_calls(self):
130
130
  """Test request processing with tool calls."""