todo-agent 0.3.2__tar.gz → 0.3.5__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 (63) hide show
  1. {todo_agent-0.3.2 → todo_agent-0.3.5}/PKG-INFO +1 -1
  2. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/test_conversation_manager.py +1 -1
  3. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/test_todo_manager.py +55 -32
  4. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_inference.py +3 -1
  5. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_ollama_client.py +11 -24
  6. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_openrouter_client.py +18 -25
  7. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_todo_shell.py +49 -6
  8. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_cli.py +12 -4
  9. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_tools.py +6 -6
  10. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/_version.py +3 -3
  11. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/exceptions.py +6 -6
  12. todo_agent-0.3.5/todo_agent/core/todo_manager.py +607 -0
  13. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/inference.py +120 -52
  14. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/llm_client.py +56 -22
  15. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/ollama_client.py +23 -13
  16. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/openrouter_client.py +20 -12
  17. todo_agent-0.3.5/todo_agent/infrastructure/prompts/system_prompt.txt +193 -0
  18. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/todo_shell.py +94 -11
  19. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/cli.py +51 -33
  20. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/formatters.py +7 -4
  21. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/progress.py +30 -19
  22. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/tools.py +73 -30
  23. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/main.py +17 -1
  24. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/PKG-INFO +1 -1
  25. todo_agent-0.3.2/todo_agent/core/todo_manager.py +0 -474
  26. todo_agent-0.3.2/todo_agent/infrastructure/prompts/system_prompt.txt +0 -441
  27. {todo_agent-0.3.2 → todo_agent-0.3.5}/.gitignore +0 -0
  28. {todo_agent-0.3.2 → todo_agent-0.3.5}/LICENSE +0 -0
  29. {todo_agent-0.3.2 → todo_agent-0.3.5}/MANIFEST.in +0 -0
  30. {todo_agent-0.3.2 → todo_agent-0.3.5}/Makefile +0 -0
  31. {todo_agent-0.3.2 → todo_agent-0.3.5}/README.md +0 -0
  32. {todo_agent-0.3.2 → todo_agent-0.3.5}/docs/publishing.md +0 -0
  33. {todo_agent-0.3.2 → todo_agent-0.3.5}/pyproject.toml +0 -0
  34. {todo_agent-0.3.2 → todo_agent-0.3.5}/requirements-dev.txt +0 -0
  35. {todo_agent-0.3.2 → todo_agent-0.3.5}/requirements.txt +0 -0
  36. {todo_agent-0.3.2 → todo_agent-0.3.5}/setup.cfg +0 -0
  37. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/__init__.py +0 -0
  38. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/__init__.py +0 -0
  39. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/__init__.py +0 -0
  40. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_calendar_utils.py +0 -0
  41. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_config.py +0 -0
  42. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  43. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_token_counter.py +0 -0
  44. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/__init__.py +0 -0
  45. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_formatters.py +0 -0
  46. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_linting.py +0 -0
  47. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_logger.py +0 -0
  48. {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_main.py +0 -0
  49. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/__init__.py +0 -0
  50. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/__init__.py +0 -0
  51. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/conversation_manager.py +0 -0
  52. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/__init__.py +0 -0
  53. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/calendar_utils.py +0 -0
  54. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/config.py +0 -0
  55. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  56. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/logger.py +0 -0
  57. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/token_counter.py +0 -0
  58. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/__init__.py +0 -0
  59. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/SOURCES.txt +0 -0
  60. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/dependency_links.txt +0 -0
  61. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/entry_points.txt +0 -0
  62. {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/requires.txt +0 -0
  63. {todo_agent-0.3.2 → todo_agent-0.3.5}/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.3.2
3
+ Version: 0.3.5
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -15,7 +15,7 @@ class TestConversationManager:
15
15
  manager = ConversationManager()
16
16
  assert len(manager.history) == 0
17
17
  assert manager.max_tokens == 64000
18
- assert manager.max_messages == 50
18
+ assert manager.max_messages == 100
19
19
  assert manager.system_prompt is None
20
20
 
21
21
  def test_add_message(self):
@@ -212,7 +212,7 @@ class TestTodoManager(unittest.TestCase):
212
212
  self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
213
213
  result = self.todo_manager.list_tasks()
214
214
  self.assertEqual(result, "1. Task 1\n2. Task 2")
215
- self.todo_shell.list_tasks.assert_called_once_with(None)
215
+ self.todo_shell.list_tasks.assert_called_once_with(None, suppress_color=True)
216
216
 
217
217
  def test_complete_task(self):
218
218
  """Test completing a task."""
@@ -228,28 +228,36 @@ class TestTodoManager(unittest.TestCase):
228
228
  )
229
229
  result = self.todo_manager.list_completed_tasks()
230
230
  self.assertEqual(result, "1. Completed task 1\n2. Completed task 2")
231
- self.todo_shell.list_completed.assert_called_once_with(None)
231
+ self.todo_shell.list_completed.assert_called_once_with(
232
+ None, suppress_color=True
233
+ )
232
234
 
233
235
  def test_list_completed_tasks_with_project_filter(self):
234
236
  """Test listing completed tasks with project filter."""
235
237
  self.todo_shell.list_completed.return_value = "1. Work task completed"
236
238
  result = self.todo_manager.list_completed_tasks(project="work")
237
239
  self.assertEqual(result, "1. Work task completed")
238
- self.todo_shell.list_completed.assert_called_once_with("+work")
240
+ self.todo_shell.list_completed.assert_called_once_with(
241
+ "+work", suppress_color=True
242
+ )
239
243
 
240
244
  def test_list_completed_tasks_with_context_filter(self):
241
245
  """Test listing completed tasks with context filter."""
242
246
  self.todo_shell.list_completed.return_value = "1. Office task completed"
243
247
  result = self.todo_manager.list_completed_tasks(context="office")
244
248
  self.assertEqual(result, "1. Office task completed")
245
- self.todo_shell.list_completed.assert_called_once_with("@office")
249
+ self.todo_shell.list_completed.assert_called_once_with(
250
+ "@office", suppress_color=True
251
+ )
246
252
 
247
253
  def test_list_completed_tasks_with_text_search(self):
248
254
  """Test listing completed tasks with text search."""
249
255
  self.todo_shell.list_completed.return_value = "1. Review task completed"
250
256
  result = self.todo_manager.list_completed_tasks(text_search="review")
251
257
  self.assertEqual(result, "1. Review task completed")
252
- self.todo_shell.list_completed.assert_called_once_with("review")
258
+ self.todo_shell.list_completed.assert_called_once_with(
259
+ "review", suppress_color=True
260
+ )
253
261
 
254
262
  def test_list_completed_tasks_with_date_filters(self):
255
263
  """Test listing completed tasks with date filters."""
@@ -259,14 +267,18 @@ class TestTodoManager(unittest.TestCase):
259
267
  )
260
268
  self.assertEqual(result, "1. Task completed in date range")
261
269
  # With both date_from and date_to, we use the year-month pattern from date_from
262
- self.todo_shell.list_completed.assert_called_once_with("2025-08")
270
+ self.todo_shell.list_completed.assert_called_once_with(
271
+ "2025-08", suppress_color=True
272
+ )
263
273
 
264
274
  def test_list_completed_tasks_with_date_from_only(self):
265
275
  """Test listing completed tasks with only date_from filter."""
266
276
  self.todo_shell.list_completed.return_value = "1. Task completed from date"
267
277
  result = self.todo_manager.list_completed_tasks(date_from="2025-08-01")
268
278
  self.assertEqual(result, "1. Task completed from date")
269
- self.todo_shell.list_completed.assert_called_once_with("2025-08-01")
279
+ self.todo_shell.list_completed.assert_called_once_with(
280
+ "2025-08-01", suppress_color=True
281
+ )
270
282
 
271
283
  def test_list_completed_tasks_with_date_to_only(self):
272
284
  """Test listing completed tasks with only date_to filter."""
@@ -274,7 +286,9 @@ class TestTodoManager(unittest.TestCase):
274
286
  result = self.todo_manager.list_completed_tasks(date_to="2025-08-31")
275
287
  self.assertEqual(result, "1. Task completed in month")
276
288
  # With only date_to, we use the year-month pattern
277
- self.todo_shell.list_completed.assert_called_once_with("2025-08")
289
+ self.todo_shell.list_completed.assert_called_once_with(
290
+ "2025-08", suppress_color=True
291
+ )
278
292
 
279
293
  def test_list_completed_tasks_with_multiple_filters(self):
280
294
  """Test listing completed tasks with multiple filters."""
@@ -285,21 +299,27 @@ class TestTodoManager(unittest.TestCase):
285
299
  project="work", context="office", text_search="review"
286
300
  )
287
301
  self.assertEqual(result, "1. Work task from office completed")
288
- self.todo_shell.list_completed.assert_called_once_with("+work @office review")
302
+ self.todo_shell.list_completed.assert_called_once_with(
303
+ "+work @office review", suppress_color=True
304
+ )
289
305
 
290
306
  def test_list_completed_tasks_with_raw_filter(self):
291
307
  """Test listing completed tasks with raw filter string."""
292
308
  self.todo_shell.list_completed.return_value = "1. Custom filtered task"
293
309
  result = self.todo_manager.list_completed_tasks(filter="+urgent @home")
294
310
  self.assertEqual(result, "1. Custom filtered task")
295
- self.todo_shell.list_completed.assert_called_once_with("+urgent @home")
311
+ self.todo_shell.list_completed.assert_called_once_with(
312
+ "+urgent @home", suppress_color=True
313
+ )
296
314
 
297
315
  def test_list_completed_tasks_no_results(self):
298
316
  """Test listing completed tasks when no results found."""
299
317
  self.todo_shell.list_completed.return_value = ""
300
318
  result = self.todo_manager.list_completed_tasks(project="nonexistent")
301
319
  self.assertEqual(result, "No completed tasks found matching the criteria.")
302
- self.todo_shell.list_completed.assert_called_once_with("+nonexistent")
320
+ self.todo_shell.list_completed.assert_called_once_with(
321
+ "+nonexistent", suppress_color=True
322
+ )
303
323
 
304
324
  def test_add_task_sanitizes_inputs(self):
305
325
  """Test that add_task sanitizes inputs to prevent duplicates."""
@@ -557,14 +577,14 @@ class TestTodoManager(unittest.TestCase):
557
577
  self.assertIn("2025-01-15 10:30:00", result)
558
578
  self.assertIn("Week 3", result)
559
579
 
560
- def test_created_completed_task_basic(self):
580
+ def test_create_completed_task_basic(self):
561
581
  """Test creating and immediately completing a task."""
562
582
  # Mock the todo_shell methods
563
583
  self.todo_shell.add.return_value = "Task added"
564
584
  self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
565
585
  self.todo_shell.complete.return_value = "Task completed"
566
586
 
567
- result = self.todo_manager.created_completed_task("Test task")
587
+ result = self.todo_manager.create_completed_task("Test task")
568
588
 
569
589
  # Verify the task was added
570
590
  self.todo_shell.add.assert_called_once_with("Test task")
@@ -573,13 +593,13 @@ class TestTodoManager(unittest.TestCase):
573
593
  # Verify the result message
574
594
  self.assertIn("Created and completed task: Test task", result)
575
595
 
576
- def test_created_completed_task_with_project_and_context(self):
596
+ def test_create_completed_task_with_project_and_context(self):
577
597
  """Test creating and completing a task with project and context."""
578
598
  self.todo_shell.add.return_value = "Task added"
579
599
  self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
580
600
  self.todo_shell.complete.return_value = "Task completed"
581
601
 
582
- result = self.todo_manager.created_completed_task(
602
+ result = self.todo_manager.create_completed_task(
583
603
  "Test task", project="work", context="office"
584
604
  )
585
605
 
@@ -589,35 +609,38 @@ class TestTodoManager(unittest.TestCase):
589
609
  self.todo_shell.complete.assert_called_once_with(1)
590
610
  self.assertIn("+work @office", result)
591
611
 
592
- def test_created_completed_task_with_custom_date(self):
612
+ def test_create_completed_task_with_custom_date(self):
593
613
  """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"
614
+ self.todo_shell.addto.return_value = "Task added to done.txt"
597
615
 
598
- result = self.todo_manager.created_completed_task(
616
+ result = self.todo_manager.create_completed_task(
599
617
  "Test task", completion_date="2025-01-10"
600
618
  )
601
619
 
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)
620
+ # Verify the task was added directly to done.txt with the custom date
621
+ self.todo_shell.addto.assert_called_once_with(
622
+ "done.txt", "x 2025-01-10 Test task"
623
+ )
624
+ # Verify add and complete were NOT called when using custom date
625
+ self.todo_shell.add.assert_not_called()
626
+ self.todo_shell.complete.assert_not_called()
604
627
  self.assertIn("completed on 2025-01-10", result)
605
628
 
606
- def test_created_completed_task_with_invalid_date(self):
629
+ def test_create_completed_task_with_invalid_date(self):
607
630
  """Test that invalid completion date raises ValueError."""
608
631
  with self.assertRaises(ValueError) as context:
609
- self.todo_manager.created_completed_task(
632
+ self.todo_manager.create_completed_task(
610
633
  "Test task", completion_date="invalid-date"
611
634
  )
612
635
  self.assertIn("Invalid completion date format", str(context.exception))
613
636
 
614
- def test_created_completed_task_sanitizes_project_and_context(self):
637
+ def test_create_completed_task_sanitizes_project_and_context(self):
615
638
  """Test that project and context with existing symbols are properly sanitized."""
616
639
  self.todo_shell.add.return_value = "Task added"
617
640
  self.todo_shell.list_tasks.return_value = "1 Test task"
618
641
  self.todo_shell.complete.return_value = "Task completed"
619
642
 
620
- result = self.todo_manager.created_completed_task(
643
+ result = self.todo_manager.create_completed_task(
621
644
  "Test task", project="+work", context="@office"
622
645
  )
623
646
 
@@ -625,25 +648,25 @@ class TestTodoManager(unittest.TestCase):
625
648
  self.todo_shell.add.assert_called_once_with("Test task +work @office")
626
649
  self.assertIn("+work @office", result)
627
650
 
628
- def test_created_completed_task_with_empty_project_after_sanitization(self):
651
+ def test_create_completed_task_with_empty_project_after_sanitization(self):
629
652
  """Test that empty project after sanitization raises ValueError."""
630
653
  with self.assertRaises(ValueError) as context:
631
- self.todo_manager.created_completed_task("Test task", project="+")
654
+ self.todo_manager.create_completed_task("Test task", project="+")
632
655
  self.assertIn("Project name cannot be empty", str(context.exception))
633
656
 
634
- def test_created_completed_task_with_empty_context_after_sanitization(self):
657
+ def test_create_completed_task_with_empty_context_after_sanitization(self):
635
658
  """Test that empty context after sanitization raises ValueError."""
636
659
  with self.assertRaises(ValueError) as context:
637
- self.todo_manager.created_completed_task("Test task", context="@")
660
+ self.todo_manager.create_completed_task("Test task", context="@")
638
661
  self.assertIn("Context name cannot be empty", str(context.exception))
639
662
 
640
- def test_created_completed_task_no_tasks_after_addition(self):
663
+ def test_create_completed_task_no_tasks_after_addition(self):
641
664
  """Test that RuntimeError is raised when no tasks are found after addition."""
642
665
  self.todo_shell.add.return_value = "Task added"
643
666
  self.todo_shell.list_tasks.return_value = "" # No tasks found
644
667
 
645
668
  with self.assertRaises(RuntimeError) as context:
646
- self.todo_manager.created_completed_task("Test task")
669
+ self.todo_manager.create_completed_task("Test task")
647
670
  self.assertIn("Failed to add task", str(context.exception))
648
671
 
649
672
 
@@ -104,7 +104,9 @@ 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}\nCALENDAR: {calendar_output}"
107
+ mock_prompt_content = (
108
+ "CURRENT DATE/TIME: {current_datetime}\nCALENDAR: {calendar_output}"
109
+ )
108
110
 
109
111
  with patch("builtins.open", mock_open(read_data=mock_prompt_content)):
110
112
  # Call the method that loads the system prompt
@@ -22,9 +22,7 @@ class TestOllamaClient:
22
22
  self.config.ollama_model = "llama3.2"
23
23
 
24
24
  # Patch the dependencies that are now initialized in the parent class
25
- with patch(
26
- "todo_agent.infrastructure.llm_client.Logger"
27
- ) as mock_logger, patch(
25
+ with patch("todo_agent.infrastructure.llm_client.Logger") as mock_logger, patch(
28
26
  "todo_agent.infrastructure.llm_client.get_token_counter"
29
27
  ) as mock_token_counter:
30
28
  mock_logger.return_value = Mock()
@@ -61,7 +59,7 @@ class TestOllamaClient:
61
59
  """Test request payload generation."""
62
60
  messages = [{"role": "user", "content": "Hello"}]
63
61
  tools = [{"name": "test_tool", "description": "A test tool"}]
64
-
62
+
65
63
  payload = self.client._get_request_payload(messages, tools)
66
64
  expected = {
67
65
  "model": "llama3.2",
@@ -102,7 +100,7 @@ class TestOllamaClient:
102
100
  "error_type": "general_error",
103
101
  "provider": "ollama",
104
102
  "status_code": 500,
105
- "raw_error": "Internal Server Error"
103
+ "raw_error": "Internal Server Error",
106
104
  }
107
105
  mock_make_request.return_value = error_response
108
106
 
@@ -144,11 +142,7 @@ class TestOllamaClient:
144
142
 
145
143
  def test_extract_tool_calls_with_error_response(self):
146
144
  """Test extracting tool calls from error response."""
147
- error_response = {
148
- "error": True,
149
- "error_type": "timeout",
150
- "provider": "ollama"
151
- }
145
+ error_response = {"error": True, "error_type": "timeout", "provider": "ollama"}
152
146
 
153
147
  tool_calls = self.client.extract_tool_calls(error_response)
154
148
  assert len(tool_calls) == 0
@@ -166,9 +160,7 @@ class TestOllamaClient:
166
160
 
167
161
  def test_extract_content_without_content(self):
168
162
  """Test extracting content from response without content."""
169
- response = {
170
- "message": {}
171
- }
163
+ response = {"message": {}}
172
164
 
173
165
  content = self.client.extract_content(response)
174
166
  assert content == ""
@@ -182,11 +174,7 @@ class TestOllamaClient:
182
174
 
183
175
  def test_extract_content_with_error_response(self):
184
176
  """Test extracting content from error response."""
185
- error_response = {
186
- "error": True,
187
- "error_type": "timeout",
188
- "provider": "ollama"
189
- }
177
+ error_response = {"error": True, "error_type": "timeout", "provider": "ollama"}
190
178
 
191
179
  content = self.client.extract_content(error_response)
192
180
  assert content == ""
@@ -196,16 +184,15 @@ class TestOllamaClient:
196
184
  response_data = {
197
185
  "message": {
198
186
  "content": "Hello",
199
- "tool_calls": [
200
- {"id": "call_1", "function": {"name": "test_tool"}}
201
- ]
187
+ "tool_calls": [{"id": "call_1", "function": {"name": "test_tool"}}],
202
188
  }
203
189
  }
204
190
 
205
- with patch.object(self.client.logger, "info") as mock_info, \
206
- patch.object(self.client.logger, "debug") as mock_debug:
191
+ with patch.object(self.client.logger, "info") as mock_info, patch.object(
192
+ self.client.logger, "debug"
193
+ ) as mock_debug:
207
194
  self.client._process_response(response_data, 0.0)
208
-
195
+
209
196
  # Should log response details
210
197
  assert mock_info.call_count >= 1
211
198
  assert mock_debug.call_count >= 1
@@ -21,9 +21,7 @@ class TestOpenRouterClient:
21
21
  def setup_method(self):
22
22
  """Set up test fixtures."""
23
23
  # Patch the dependencies that are now initialized in the parent class
24
- with patch(
25
- "todo_agent.infrastructure.llm_client.Logger"
26
- ) as mock_logger, patch(
24
+ with patch("todo_agent.infrastructure.llm_client.Logger") as mock_logger, patch(
27
25
  "todo_agent.infrastructure.llm_client.get_token_counter"
28
26
  ) as mock_token_counter:
29
27
  mock_logger.return_value = Mock()
@@ -62,7 +60,7 @@ class TestOpenRouterClient:
62
60
  """Test request payload generation."""
63
61
  messages = [{"role": "user", "content": "Hello"}]
64
62
  tools = [{"name": "test_tool", "description": "A test tool"}]
65
-
63
+
66
64
  payload = self.client._get_request_payload(messages, tools)
67
65
  expected = {
68
66
  "model": "test-model",
@@ -77,7 +75,9 @@ class TestOpenRouterClient:
77
75
  endpoint = self.client._get_api_endpoint()
78
76
  assert endpoint == "https://openrouter.ai/api/v1/chat/completions"
79
77
 
80
- @patch("todo_agent.infrastructure.openrouter_client.OpenRouterClient._make_http_request")
78
+ @patch(
79
+ "todo_agent.infrastructure.openrouter_client.OpenRouterClient._make_http_request"
80
+ )
81
81
  def test_chat_with_tools_success(self, mock_make_request):
82
82
  """Test successful chat with tools."""
83
83
  messages = [{"role": "user", "content": "Hello"}]
@@ -104,7 +104,9 @@ class TestOpenRouterClient:
104
104
  assert result == expected_response
105
105
  mock_make_request.assert_called_once_with(messages, tools)
106
106
 
107
- @patch("todo_agent.infrastructure.openrouter_client.OpenRouterClient._make_http_request")
107
+ @patch(
108
+ "todo_agent.infrastructure.openrouter_client.OpenRouterClient._make_http_request"
109
+ )
108
110
  def test_chat_with_tools_api_error(self, mock_make_request):
109
111
  """Test API error handling."""
110
112
  messages = [{"role": "user", "content": "Hello"}]
@@ -115,7 +117,7 @@ class TestOpenRouterClient:
115
117
  "error_type": "auth_error",
116
118
  "provider": "openrouter",
117
119
  "status_code": 401,
118
- "raw_error": "Unauthorized"
120
+ "raw_error": "Unauthorized",
119
121
  }
120
122
  mock_make_request.return_value = error_response
121
123
 
@@ -179,7 +181,7 @@ class TestOpenRouterClient:
179
181
  error_response = {
180
182
  "error": True,
181
183
  "error_type": "timeout",
182
- "provider": "openrouter"
184
+ "provider": "openrouter",
183
185
  }
184
186
 
185
187
  tool_calls = self.client.extract_tool_calls(error_response)
@@ -202,13 +204,7 @@ class TestOpenRouterClient:
202
204
 
203
205
  def test_extract_content_no_content(self):
204
206
  """Test extracting content from response without content."""
205
- response = {
206
- "choices": [
207
- {
208
- "message": {}
209
- }
210
- ]
211
- }
207
+ response = {"choices": [{"message": {}}]}
212
208
 
213
209
  content = self.client.extract_content(response)
214
210
  assert content == ""
@@ -232,7 +228,7 @@ class TestOpenRouterClient:
232
228
  error_response = {
233
229
  "error": True,
234
230
  "error_type": "timeout",
235
- "provider": "openrouter"
231
+ "provider": "openrouter",
236
232
  }
237
233
 
238
234
  content = self.client.extract_content(error_response)
@@ -253,21 +249,18 @@ class TestOpenRouterClient:
253
249
  "content": "Hello",
254
250
  "tool_calls": [
255
251
  {"id": "call_1", "function": {"name": "test_tool"}}
256
- ]
252
+ ],
257
253
  }
258
254
  }
259
255
  ],
260
- "usage": {
261
- "prompt_tokens": 10,
262
- "completion_tokens": 5,
263
- "total_tokens": 15
264
- }
256
+ "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
265
257
  }
266
258
 
267
- with patch.object(self.client.logger, "info") as mock_info, \
268
- patch.object(self.client.logger, "debug") as mock_debug:
259
+ with patch.object(self.client.logger, "info") as mock_info, patch.object(
260
+ self.client.logger, "debug"
261
+ ) as mock_debug:
269
262
  self.client._process_response(response_data, 0.0)
270
-
263
+
271
264
  # Should log response details
272
265
  assert mock_info.call_count >= 2 # Latency and token usage
273
266
  assert mock_debug.call_count >= 1 # Raw response
@@ -100,7 +100,7 @@ class TestTodoShell:
100
100
  result = self.todo_shell.list_tasks()
101
101
 
102
102
  # Verify the correct command was used
103
- mock_execute.assert_called_once_with(["todo.sh", "ls"])
103
+ mock_execute.assert_called_once_with(["todo.sh", "ls"], suppress_color=True)
104
104
  assert result == "1. Task 1\n2. Task 2"
105
105
 
106
106
  def test_list_tasks_with_filter_appends_filter_to_command(self):
@@ -111,7 +111,9 @@ class TestTodoShell:
111
111
  result = self.todo_shell.list_tasks("+work")
112
112
 
113
113
  # Verify filter was appended to command
114
- mock_execute.assert_called_once_with(["todo.sh", "ls", "+work"])
114
+ mock_execute.assert_called_once_with(
115
+ ["todo.sh", "ls", "+work"], suppress_color=True
116
+ )
115
117
  assert result == "1. Work task"
116
118
 
117
119
  def test_complete_task_uses_do_command_with_task_number(self):
@@ -500,7 +502,9 @@ class TestTodoShell:
500
502
  result = self.todo_shell.list_projects()
501
503
 
502
504
  # Verify lsp command
503
- mock_execute.assert_called_once_with(["todo.sh", "lsp"])
505
+ mock_execute.assert_called_once_with(
506
+ ["todo.sh", "lsp"], suppress_color=True
507
+ )
504
508
  assert result == "+work\n+home\n+shopping"
505
509
 
506
510
  def test_list_contexts_uses_lsc_command(self):
@@ -511,7 +515,9 @@ class TestTodoShell:
511
515
  result = self.todo_shell.list_contexts()
512
516
 
513
517
  # Verify lsc command
514
- mock_execute.assert_called_once_with(["todo.sh", "lsc"])
518
+ mock_execute.assert_called_once_with(
519
+ ["todo.sh", "lsc"], suppress_color=True
520
+ )
515
521
  assert result == "@work\n@home\n@shopping"
516
522
 
517
523
  def test_list_completed_uses_listfile_command(self):
@@ -522,7 +528,9 @@ class TestTodoShell:
522
528
  result = self.todo_shell.list_completed()
523
529
 
524
530
  # Verify listfile command with done.txt
525
- mock_execute.assert_called_once_with(["todo.sh", "listfile", "done.txt"])
531
+ mock_execute.assert_called_once_with(
532
+ ["todo.sh", "listfile", "done.txt"], suppress_color=True
533
+ )
526
534
  assert result == "1. Completed task"
527
535
 
528
536
  def test_list_completed_with_filter_appends_filter(self):
@@ -534,7 +542,7 @@ class TestTodoShell:
534
542
 
535
543
  # Verify filter was appended to command
536
544
  mock_execute.assert_called_once_with(
537
- ["todo.sh", "listfile", "done.txt", "+work"]
545
+ ["todo.sh", "listfile", "done.txt", "+work"], suppress_color=True
538
546
  )
539
547
  assert result == "1. Work task completed"
540
548
 
@@ -584,6 +592,41 @@ class TestTodoShell:
584
592
  ):
585
593
  self.todo_shell.execute(["todo.sh", "add", "test"])
586
594
 
595
+ def test_execute_suppresses_color_codes_when_requested(self):
596
+ """Test that execute method strips ANSI color codes when suppress_color=True."""
597
+ # Mock subprocess to return output with ANSI color codes
598
+ mock_result = type(
599
+ "MockResult",
600
+ (),
601
+ {
602
+ "stdout": "\033[1;33m1\033[0m (A) \033[1;32m2025-08-29\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m \033[1;31mdue:2025-08-29\033[0m",
603
+ "stderr": "",
604
+ "returncode": 0,
605
+ },
606
+ )()
607
+
608
+ with patch("subprocess.run", return_value=mock_result):
609
+ # Test with suppress_color=True (default for LLM consumption)
610
+ result = self.todo_shell.execute(["todo.sh", "ls"], suppress_color=True)
611
+
612
+ # Should return clean text without ANSI codes
613
+ assert (
614
+ result == "1 (A) 2025-08-29 Clean cat box @home +chores due:2025-08-29"
615
+ )
616
+ assert "\033[" not in result # No ANSI escape sequences
617
+
618
+ # Test with suppress_color=False (for interactive display)
619
+ result_with_color = self.todo_shell.execute(
620
+ ["todo.sh", "ls"], suppress_color=False
621
+ )
622
+
623
+ # Should preserve ANSI codes
624
+ assert "\033[" in result_with_color # ANSI escape sequences preserved
625
+ assert (
626
+ result_with_color
627
+ == "\033[1;33m1\033[0m (A) \033[1;32m2025-08-29\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m \033[1;31mdue:2025-08-29\033[0m"
628
+ )
629
+
587
630
  def test_set_project_adds_projects_to_task_without_projects(self):
588
631
  """Test that set_project adds projects to task that doesn't have any."""
589
632
  # Mock the list_tasks to return a task without projects
@@ -141,7 +141,9 @@ class TestCLI:
141
141
  # Verify inference engine was called with correct input and progress callback
142
142
  call_args = self.cli.inference.process_request.call_args
143
143
  assert call_args[0][0] == user_input # First argument should be user_input
144
- assert call_args[0][1] is not None # Second argument should be progress callback
144
+ assert (
145
+ call_args[0][1] is not None
146
+ ) # Second argument should be progress callback
145
147
 
146
148
  def test_handle_request_exception_handling(self):
147
149
  """Test that exceptions in handle_request are properly caught and formatted."""
@@ -159,7 +161,9 @@ class TestCLI:
159
161
  # Verify inference engine was called with progress callback
160
162
  call_args = self.cli.inference.process_request.call_args
161
163
  assert call_args[0][0] == user_input # First argument should be user_input
162
- assert call_args[0][1] is not None # Second argument should be progress callback
164
+ assert (
165
+ call_args[0][1] is not None
166
+ ) # Second argument should be progress callback
163
167
 
164
168
  def test_run_single_request_delegates_to_handle_request(self):
165
169
  """Test that run_single_request properly delegates to handle_request."""
@@ -382,7 +386,9 @@ class TestCLI:
382
386
  # Should still call inference engine (let it handle empty input)
383
387
  call_args = self.cli.inference.process_request.call_args
384
388
  assert call_args[0][0] == "" # First argument should be empty string
385
- assert call_args[0][1] is not None # Second argument should be progress callback
389
+ assert (
390
+ call_args[0][1] is not None
391
+ ) # Second argument should be progress callback
386
392
 
387
393
  def test_long_input_truncation_in_logging(self):
388
394
  """Test that long inputs are properly truncated in logging."""
@@ -396,7 +402,9 @@ class TestCLI:
396
402
  # Verify inference engine was called with full input and progress callback
397
403
  call_args = self.cli.inference.process_request.call_args
398
404
  assert call_args[0][0] == long_input # First argument should be long_input
399
- assert call_args[0][1] is not None # Second argument should be progress callback
405
+ assert (
406
+ call_args[0][1] is not None
407
+ ) # Second argument should be progress callback
400
408
 
401
409
  # Verify response is correct
402
410
  assert result == "Response"
@@ -108,16 +108,16 @@ class TestToolErrorHandling:
108
108
  assert "Permission denied" in result["user_message"]
109
109
  assert "check file permissions" in result["user_message"]
110
110
 
111
- def test_created_completed_task_tool_execution(self):
112
- """Test that the created_completed_task tool can be executed successfully."""
111
+ def test_create_completed_task_tool_execution(self):
112
+ """Test that the create_completed_task tool can be executed successfully."""
113
113
  # Mock the todo_manager method
114
- self.mock_todo_manager.created_completed_task.return_value = (
114
+ self.mock_todo_manager.create_completed_task.return_value = (
115
115
  "Created and completed task: Test task (completed on 2025-01-15)"
116
116
  )
117
117
 
118
118
  tool_call = {
119
119
  "function": {
120
- "name": "created_completed_task",
120
+ "name": "create_completed_task",
121
121
  "arguments": '{"description": "Test task", "completion_date": "2025-01-15"}',
122
122
  },
123
123
  "id": "test_id",
@@ -128,10 +128,10 @@ class TestToolErrorHandling:
128
128
  assert result["error"] is False
129
129
  assert "Created and completed task: Test task" in result["output"]
130
130
  assert result["tool_call_id"] == "test_id"
131
- assert result["name"] == "created_completed_task"
131
+ assert result["name"] == "create_completed_task"
132
132
 
133
133
  # Verify the method was called with correct arguments
134
- self.mock_todo_manager.created_completed_task.assert_called_once_with(
134
+ self.mock_todo_manager.create_completed_task.assert_called_once_with(
135
135
  description="Test task", completion_date="2025-01-15"
136
136
  )
137
137
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.3.2'
32
- __version_tuple__ = version_tuple = (0, 3, 2)
31
+ __version__ = version = '0.3.5'
32
+ __version_tuple__ = version_tuple = (0, 3, 5)
33
33
 
34
- __commit_id__ = commit_id = 'gaaa8652a7'
34
+ __commit_id__ = commit_id = 'g698d691a3'