todo-agent 0.3.2__tar.gz → 0.3.3__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.3}/PKG-INFO +1 -1
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_core/test_todo_manager.py +31 -11
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_inference.py +3 -1
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_ollama_client.py +11 -24
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_openrouter_client.py +18 -25
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_todo_shell.py +49 -6
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_interface/test_cli.py +12 -4
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/_version.py +3 -3
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/core/exceptions.py +6 -6
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/core/todo_manager.py +13 -8
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/inference.py +113 -52
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/llm_client.py +56 -22
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/ollama_client.py +23 -13
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/openrouter_client.py +20 -12
- todo_agent-0.3.3/todo_agent/infrastructure/prompts/system_prompt.txt +91 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/todo_shell.py +35 -11
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/interface/cli.py +51 -33
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/interface/formatters.py +7 -4
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/interface/progress.py +30 -19
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/interface/tools.py +25 -25
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent.egg-info/PKG-INFO +1 -1
- todo_agent-0.3.2/todo_agent/infrastructure/prompts/system_prompt.txt +0 -441
- {todo_agent-0.3.2 → todo_agent-0.3.3}/.gitignore +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/LICENSE +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/MANIFEST.in +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/Makefile +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/README.md +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/docs/publishing.md +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/pyproject.toml +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/requirements-dev.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/requirements.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/setup.cfg +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_calendar_utils.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_interface/test_formatters.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_interface/test_tools.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_linting.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_logger.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/tests/test_main.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/core/conversation_manager.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/calendar_utils.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent/main.py +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent.egg-info/SOURCES.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.3.2 → todo_agent-0.3.3}/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."""
|
@@ -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"
|
@@ -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.3'
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 3)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
34
|
+
__commit_id__ = commit_id = 'g4d82778ea'
|
@@ -35,7 +35,7 @@ class TodoShellError(TodoError):
|
|
35
35
|
|
36
36
|
class ProviderError(Exception):
|
37
37
|
"""Base exception for LLM provider errors."""
|
38
|
-
|
38
|
+
|
39
39
|
def __init__(self, message: str, error_type: str, provider: str):
|
40
40
|
super().__init__(message)
|
41
41
|
self.message = message
|
@@ -45,34 +45,34 @@ class ProviderError(Exception):
|
|
45
45
|
|
46
46
|
class MalformedResponseError(ProviderError):
|
47
47
|
"""Provider returned malformed or invalid response."""
|
48
|
-
|
48
|
+
|
49
49
|
def __init__(self, message: str, provider: str):
|
50
50
|
super().__init__(message, "malformed_response", provider)
|
51
51
|
|
52
52
|
|
53
53
|
class RateLimitError(ProviderError):
|
54
54
|
"""Provider rate limit exceeded."""
|
55
|
-
|
55
|
+
|
56
56
|
def __init__(self, message: str, provider: str):
|
57
57
|
super().__init__(message, "rate_limit", provider)
|
58
58
|
|
59
59
|
|
60
60
|
class AuthenticationError(ProviderError):
|
61
61
|
"""Provider authentication failed."""
|
62
|
-
|
62
|
+
|
63
63
|
def __init__(self, message: str, provider: str):
|
64
64
|
super().__init__(message, "auth_error", provider)
|
65
65
|
|
66
66
|
|
67
67
|
class TimeoutError(ProviderError):
|
68
68
|
"""Provider request timed out."""
|
69
|
-
|
69
|
+
|
70
70
|
def __init__(self, message: str, provider: str):
|
71
71
|
super().__init__(message, "timeout", provider)
|
72
72
|
|
73
73
|
|
74
74
|
class GeneralProviderError(ProviderError):
|
75
75
|
"""General provider error."""
|
76
|
-
|
76
|
+
|
77
77
|
def __init__(self, message: str, provider: str):
|
78
78
|
super().__init__(message, "general_error", provider)
|
@@ -102,9 +102,11 @@ class TodoManager:
|
|
102
102
|
self.todo_shell.add(full_description)
|
103
103
|
return f"Added task: {full_description}"
|
104
104
|
|
105
|
-
def list_tasks(
|
105
|
+
def list_tasks(
|
106
|
+
self, filter: Optional[str] = None, suppress_color: bool = True
|
107
|
+
) -> str:
|
106
108
|
"""List tasks with optional filtering."""
|
107
|
-
result = self.todo_shell.list_tasks(filter)
|
109
|
+
result = self.todo_shell.list_tasks(filter, suppress_color=suppress_color)
|
108
110
|
if not result.strip():
|
109
111
|
return "No tasks found."
|
110
112
|
|
@@ -258,16 +260,16 @@ class TodoManager:
|
|
258
260
|
operation_desc = ", ".join(operations)
|
259
261
|
return f"Updated projects for task {task_number} ({operation_desc}): {result}"
|
260
262
|
|
261
|
-
def list_projects(self, **kwargs: Any) -> str:
|
263
|
+
def list_projects(self, suppress_color: bool = True, **kwargs: Any) -> str:
|
262
264
|
"""List all available projects in todo.txt."""
|
263
|
-
result = self.todo_shell.list_projects()
|
265
|
+
result = self.todo_shell.list_projects(suppress_color=suppress_color)
|
264
266
|
if not result.strip():
|
265
267
|
return "No projects found."
|
266
268
|
return result
|
267
269
|
|
268
|
-
def list_contexts(self, **kwargs: Any) -> str:
|
270
|
+
def list_contexts(self, suppress_color: bool = True, **kwargs: Any) -> str:
|
269
271
|
"""List all available contexts in todo.txt."""
|
270
|
-
result = self.todo_shell.list_contexts()
|
272
|
+
result = self.todo_shell.list_contexts(suppress_color=suppress_color)
|
271
273
|
if not result.strip():
|
272
274
|
return "No contexts found."
|
273
275
|
return result
|
@@ -280,6 +282,7 @@ class TodoManager:
|
|
280
282
|
text_search: Optional[str] = None,
|
281
283
|
date_from: Optional[str] = None,
|
282
284
|
date_to: Optional[str] = None,
|
285
|
+
suppress_color: bool = True,
|
283
286
|
**kwargs: Any,
|
284
287
|
) -> str:
|
285
288
|
"""List completed tasks with optional filtering.
|
@@ -329,7 +332,9 @@ class TodoManager:
|
|
329
332
|
# Combine all filters
|
330
333
|
combined_filter = " ".join(filter_parts) if filter_parts else None
|
331
334
|
|
332
|
-
result = self.todo_shell.list_completed(
|
335
|
+
result = self.todo_shell.list_completed(
|
336
|
+
combined_filter, suppress_color=suppress_color
|
337
|
+
)
|
333
338
|
if not result.strip():
|
334
339
|
return "No completed tasks found matching the criteria."
|
335
340
|
return result
|
@@ -464,7 +469,7 @@ class TodoManager:
|
|
464
469
|
|
465
470
|
# Use the move command to restore the task from done.txt to todo.txt
|
466
471
|
result = self.todo_shell.move(task_number, "todo.txt", "done.txt")
|
467
|
-
|
472
|
+
|
468
473
|
# Extract the task description from the result for confirmation
|
469
474
|
# The result format is typically: "TODO: X moved from '.../done.txt' to '.../todo.txt'."
|
470
475
|
if "moved from" in result and "to" in result:
|