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.
- {todo_agent-0.3.2 → todo_agent-0.3.5}/PKG-INFO +1 -1
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/test_todo_manager.py +55 -32
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_inference.py +3 -1
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_ollama_client.py +11 -24
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_openrouter_client.py +18 -25
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_todo_shell.py +49 -6
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_cli.py +12 -4
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_tools.py +6 -6
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/_version.py +3 -3
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/exceptions.py +6 -6
- todo_agent-0.3.5/todo_agent/core/todo_manager.py +607 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/inference.py +120 -52
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/llm_client.py +56 -22
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/ollama_client.py +23 -13
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/openrouter_client.py +20 -12
- todo_agent-0.3.5/todo_agent/infrastructure/prompts/system_prompt.txt +193 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/todo_shell.py +94 -11
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/cli.py +51 -33
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/formatters.py +7 -4
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/progress.py +30 -19
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/tools.py +73 -30
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/main.py +17 -1
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/PKG-INFO +1 -1
- todo_agent-0.3.2/todo_agent/core/todo_manager.py +0 -474
- todo_agent-0.3.2/todo_agent/infrastructure/prompts/system_prompt.txt +0 -441
- {todo_agent-0.3.2 → todo_agent-0.3.5}/.gitignore +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/LICENSE +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/MANIFEST.in +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/Makefile +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/README.md +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/docs/publishing.md +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/pyproject.toml +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/requirements-dev.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/requirements.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/setup.cfg +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_calendar_utils.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_interface/test_formatters.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_linting.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_logger.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/tests/test_main.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/core/conversation_manager.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/calendar_utils.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/SOURCES.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.5}/todo_agent.egg-info/top_level.txt +0 -0
@@ -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 ==
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
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.
|
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
|
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.
|
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
|
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.
|
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.
|
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
|
603
|
-
self.todo_shell.
|
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
|
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.
|
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
|
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.
|
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
|
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.
|
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
|
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.
|
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
|
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.
|
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 =
|
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
|
-
|
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(
|
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(
|
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
|
-
|
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(
|
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(
|
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(
|
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(
|
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
|
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
|
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
|
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
|
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
|
112
|
-
"""Test that the
|
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.
|
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": "
|
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"] == "
|
131
|
+
assert result["name"] == "create_completed_task"
|
132
132
|
|
133
133
|
# Verify the method was called with correct arguments
|
134
|
-
self.mock_todo_manager.
|
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.
|
32
|
-
__version_tuple__ = version_tuple = (0, 3,
|
31
|
+
__version__ = version = '0.3.5'
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 5)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
34
|
+
__commit_id__ = commit_id = 'g698d691a3'
|