todo-agent 0.3.1__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.1 → todo_agent-0.3.3}/PKG-INFO +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.3}/README.md +2 -2
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_core/test_conversation_manager.py +2 -2
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_core/test_todo_manager.py +138 -71
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_calendar_utils.py +2 -2
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_inference.py +6 -4
- todo_agent-0.3.3/tests/test_infrastructure/test_ollama_client.py +198 -0
- todo_agent-0.3.3/tests/test_infrastructure/test_openrouter_client.py +266 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_todo_shell.py +66 -19
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_interface/test_cli.py +23 -7
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_interface/test_tools.py +27 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/_version.py +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/core/conversation_manager.py +1 -1
- todo_agent-0.3.3/todo_agent/core/exceptions.py +78 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/core/todo_manager.py +127 -56
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/calendar_utils.py +2 -4
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/inference.py +158 -52
- todo_agent-0.3.3/todo_agent/infrastructure/llm_client.py +319 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/ollama_client.py +77 -76
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/openrouter_client.py +77 -72
- todo_agent-0.3.3/todo_agent/infrastructure/prompts/system_prompt.txt +91 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/todo_shell.py +37 -27
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/interface/cli.py +129 -19
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/interface/formatters.py +25 -0
- todo_agent-0.3.3/todo_agent/interface/progress.py +69 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/interface/tools.py +142 -23
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent.egg-info/PKG-INFO +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent.egg-info/SOURCES.txt +1 -0
- todo_agent-0.3.1/tests/test_infrastructure/test_ollama_client.py +0 -139
- todo_agent-0.3.1/tests/test_infrastructure/test_openrouter_client.py +0 -292
- todo_agent-0.3.1/todo_agent/core/exceptions.py +0 -27
- todo_agent-0.3.1/todo_agent/infrastructure/llm_client.py +0 -62
- todo_agent-0.3.1/todo_agent/infrastructure/prompts/system_prompt.txt +0 -399
- {todo_agent-0.3.1 → todo_agent-0.3.3}/.gitignore +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/LICENSE +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/MANIFEST.in +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/Makefile +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/docs/publishing.md +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/pyproject.toml +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/requirements-dev.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/requirements.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/setup.cfg +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_interface/test_formatters.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_linting.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_logger.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/tests/test_main.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent/main.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.3}/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.
|
3
|
+
Version: 0.3.3
|
4
4
|
Summary: A natural language interface for todo.sh task management
|
5
5
|
Author: codeprimate
|
6
6
|
Maintainer: codeprimate
|
@@ -68,7 +68,7 @@ todo-agent "show my work tasks"
|
|
68
68
|
|
69
69
|
**Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
|
70
70
|
|
71
|
-
**Work smarter** with automatic duplicate detection
|
71
|
+
**Work smarter** with automatic duplicate detection and calendar-aware scheduling.
|
72
72
|
|
73
73
|
**Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
|
74
74
|
|
@@ -163,7 +163,7 @@ todo-agent "what tasks are blocking other work?"
|
|
163
163
|
### Natural Language Intelligence
|
164
164
|
```bash
|
165
165
|
todo-agent "add dentist appointment next Monday"
|
166
|
-
|
166
|
+
|
167
167
|
todo-agent "move all completed tasks to archive"
|
168
168
|
todo-agent "show me tasks I can do from home"
|
169
169
|
```
|
@@ -21,7 +21,7 @@ todo-agent "show my work tasks"
|
|
21
21
|
|
22
22
|
**Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
|
23
23
|
|
24
|
-
**Work smarter** with automatic duplicate detection
|
24
|
+
**Work smarter** with automatic duplicate detection and calendar-aware scheduling.
|
25
25
|
|
26
26
|
**Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
|
27
27
|
|
@@ -116,7 +116,7 @@ todo-agent "what tasks are blocking other work?"
|
|
116
116
|
### Natural Language Intelligence
|
117
117
|
```bash
|
118
118
|
todo-agent "add dentist appointment next Monday"
|
119
|
-
|
119
|
+
|
120
120
|
todo-agent "move all completed tasks to archive"
|
121
121
|
todo-agent "show me tasks I can do from home"
|
122
122
|
```
|
@@ -14,8 +14,8 @@ class TestConversationManager:
|
|
14
14
|
"""Test ConversationManager initialization."""
|
15
15
|
manager = ConversationManager()
|
16
16
|
assert len(manager.history) == 0
|
17
|
-
assert manager.max_tokens ==
|
18
|
-
assert manager.max_messages ==
|
17
|
+
assert manager.max_tokens == 64000
|
18
|
+
assert manager.max_messages == 100
|
19
19
|
assert manager.system_prompt is None
|
20
20
|
|
21
21
|
def test_add_message(self):
|
@@ -148,44 +148,6 @@ class TestTodoManager(unittest.TestCase):
|
|
148
148
|
self.todo_manager.set_due_date(1, "2025/1/5")
|
149
149
|
self.assertIn("Invalid due date format", str(context.exception))
|
150
150
|
|
151
|
-
def test_add_task_with_valid_recurring_daily(self):
|
152
|
-
"""Test adding a task with valid daily recurring format."""
|
153
|
-
self.todo_shell.add.return_value = "Test task"
|
154
|
-
result = self.todo_manager.add_task("Test task", recurring="rec:daily")
|
155
|
-
self.assertEqual(result, "Added task: Test task rec:daily")
|
156
|
-
self.todo_shell.add.assert_called_once_with("Test task rec:daily")
|
157
|
-
|
158
|
-
def test_add_task_with_valid_recurring_weekly_interval(self):
|
159
|
-
"""Test adding a task with valid weekly recurring format with interval."""
|
160
|
-
self.todo_shell.add.return_value = "Test task"
|
161
|
-
result = self.todo_manager.add_task("Test task", recurring="rec:weekly:2")
|
162
|
-
self.assertEqual(result, "Added task: Test task rec:weekly:2")
|
163
|
-
self.todo_shell.add.assert_called_once_with("Test task rec:weekly:2")
|
164
|
-
|
165
|
-
def test_add_task_with_invalid_recurring_format(self):
|
166
|
-
"""Test adding a task with invalid recurring format raises ValueError."""
|
167
|
-
with self.assertRaises(ValueError) as context:
|
168
|
-
self.todo_manager.add_task("Test task", recurring="invalid")
|
169
|
-
self.assertIn("Invalid recurring format", str(context.exception))
|
170
|
-
|
171
|
-
def test_add_task_with_invalid_recurring_frequency(self):
|
172
|
-
"""Test adding a task with invalid recurring frequency raises ValueError."""
|
173
|
-
with self.assertRaises(ValueError) as context:
|
174
|
-
self.todo_manager.add_task("Test task", recurring="rec:invalid")
|
175
|
-
self.assertIn("Invalid frequency", str(context.exception))
|
176
|
-
|
177
|
-
def test_add_task_with_invalid_recurring_interval(self):
|
178
|
-
"""Test adding a task with invalid recurring interval raises ValueError."""
|
179
|
-
with self.assertRaises(ValueError) as context:
|
180
|
-
self.todo_manager.add_task("Test task", recurring="rec:weekly:invalid")
|
181
|
-
self.assertIn("Invalid interval", str(context.exception))
|
182
|
-
|
183
|
-
def test_add_task_with_zero_recurring_interval(self):
|
184
|
-
"""Test adding a task with zero recurring interval raises ValueError."""
|
185
|
-
with self.assertRaises(ValueError) as context:
|
186
|
-
self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
|
187
|
-
self.assertIn("Must be a positive integer", str(context.exception))
|
188
|
-
|
189
151
|
def test_add_task_with_invalid_duration_format(self):
|
190
152
|
"""Test adding a task with invalid duration format raises ValueError."""
|
191
153
|
with self.assertRaises(ValueError) as context:
|
@@ -216,24 +178,6 @@ class TestTodoManager(unittest.TestCase):
|
|
216
178
|
self.todo_manager.add_task("Test task", duration="")
|
217
179
|
self.assertIn("Duration must be a non-empty string", str(context.exception))
|
218
180
|
|
219
|
-
def test_add_task_with_all_parameters_including_recurring(self):
|
220
|
-
"""Test adding a task with all parameters including recurring."""
|
221
|
-
self.todo_shell.add.return_value = "Test task"
|
222
|
-
result = self.todo_manager.add_task(
|
223
|
-
"Test task",
|
224
|
-
priority="A",
|
225
|
-
project="work",
|
226
|
-
context="office",
|
227
|
-
due="2024-01-15",
|
228
|
-
recurring="rec:daily",
|
229
|
-
)
|
230
|
-
self.assertEqual(
|
231
|
-
result, "Added task: (A) Test task +work @office due:2024-01-15 rec:daily"
|
232
|
-
)
|
233
|
-
self.todo_shell.add.assert_called_once_with(
|
234
|
-
"(A) Test task +work @office due:2024-01-15 rec:daily"
|
235
|
-
)
|
236
|
-
|
237
181
|
def test_add_task_with_duration(self):
|
238
182
|
"""Test adding a task with duration parameter."""
|
239
183
|
self.todo_shell.add.return_value = "Test task"
|
@@ -253,15 +197,14 @@ class TestTodoManager(unittest.TestCase):
|
|
253
197
|
project="work",
|
254
198
|
context="office",
|
255
199
|
due="2024-01-15",
|
256
|
-
recurring="rec:daily",
|
257
200
|
duration="2h",
|
258
201
|
)
|
259
202
|
self.assertEqual(
|
260
203
|
result,
|
261
|
-
"Added task: (A) Test task +work @office due:2024-01-15
|
204
|
+
"Added task: (A) Test task +work @office due:2024-01-15 duration:2h",
|
262
205
|
)
|
263
206
|
self.todo_shell.add.assert_called_once_with(
|
264
|
-
"(A) Test task +work @office due:2024-01-15
|
207
|
+
"(A) Test task +work @office due:2024-01-15 duration:2h"
|
265
208
|
)
|
266
209
|
|
267
210
|
def test_list_tasks(self):
|
@@ -269,7 +212,7 @@ class TestTodoManager(unittest.TestCase):
|
|
269
212
|
self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
|
270
213
|
result = self.todo_manager.list_tasks()
|
271
214
|
self.assertEqual(result, "1. Task 1\n2. Task 2")
|
272
|
-
self.todo_shell.list_tasks.assert_called_once_with(None)
|
215
|
+
self.todo_shell.list_tasks.assert_called_once_with(None, suppress_color=True)
|
273
216
|
|
274
217
|
def test_complete_task(self):
|
275
218
|
"""Test completing a task."""
|
@@ -285,28 +228,36 @@ class TestTodoManager(unittest.TestCase):
|
|
285
228
|
)
|
286
229
|
result = self.todo_manager.list_completed_tasks()
|
287
230
|
self.assertEqual(result, "1. Completed task 1\n2. Completed task 2")
|
288
|
-
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
|
+
)
|
289
234
|
|
290
235
|
def test_list_completed_tasks_with_project_filter(self):
|
291
236
|
"""Test listing completed tasks with project filter."""
|
292
237
|
self.todo_shell.list_completed.return_value = "1. Work task completed"
|
293
238
|
result = self.todo_manager.list_completed_tasks(project="work")
|
294
239
|
self.assertEqual(result, "1. Work task completed")
|
295
|
-
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
|
+
)
|
296
243
|
|
297
244
|
def test_list_completed_tasks_with_context_filter(self):
|
298
245
|
"""Test listing completed tasks with context filter."""
|
299
246
|
self.todo_shell.list_completed.return_value = "1. Office task completed"
|
300
247
|
result = self.todo_manager.list_completed_tasks(context="office")
|
301
248
|
self.assertEqual(result, "1. Office task completed")
|
302
|
-
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
|
+
)
|
303
252
|
|
304
253
|
def test_list_completed_tasks_with_text_search(self):
|
305
254
|
"""Test listing completed tasks with text search."""
|
306
255
|
self.todo_shell.list_completed.return_value = "1. Review task completed"
|
307
256
|
result = self.todo_manager.list_completed_tasks(text_search="review")
|
308
257
|
self.assertEqual(result, "1. Review task completed")
|
309
|
-
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
|
+
)
|
310
261
|
|
311
262
|
def test_list_completed_tasks_with_date_filters(self):
|
312
263
|
"""Test listing completed tasks with date filters."""
|
@@ -316,14 +267,18 @@ class TestTodoManager(unittest.TestCase):
|
|
316
267
|
)
|
317
268
|
self.assertEqual(result, "1. Task completed in date range")
|
318
269
|
# With both date_from and date_to, we use the year-month pattern from date_from
|
319
|
-
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
|
+
)
|
320
273
|
|
321
274
|
def test_list_completed_tasks_with_date_from_only(self):
|
322
275
|
"""Test listing completed tasks with only date_from filter."""
|
323
276
|
self.todo_shell.list_completed.return_value = "1. Task completed from date"
|
324
277
|
result = self.todo_manager.list_completed_tasks(date_from="2025-08-01")
|
325
278
|
self.assertEqual(result, "1. Task completed from date")
|
326
|
-
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
|
+
)
|
327
282
|
|
328
283
|
def test_list_completed_tasks_with_date_to_only(self):
|
329
284
|
"""Test listing completed tasks with only date_to filter."""
|
@@ -331,7 +286,9 @@ class TestTodoManager(unittest.TestCase):
|
|
331
286
|
result = self.todo_manager.list_completed_tasks(date_to="2025-08-31")
|
332
287
|
self.assertEqual(result, "1. Task completed in month")
|
333
288
|
# With only date_to, we use the year-month pattern
|
334
|
-
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
|
+
)
|
335
292
|
|
336
293
|
def test_list_completed_tasks_with_multiple_filters(self):
|
337
294
|
"""Test listing completed tasks with multiple filters."""
|
@@ -342,21 +299,27 @@ class TestTodoManager(unittest.TestCase):
|
|
342
299
|
project="work", context="office", text_search="review"
|
343
300
|
)
|
344
301
|
self.assertEqual(result, "1. Work task from office completed")
|
345
|
-
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
|
+
)
|
346
305
|
|
347
306
|
def test_list_completed_tasks_with_raw_filter(self):
|
348
307
|
"""Test listing completed tasks with raw filter string."""
|
349
308
|
self.todo_shell.list_completed.return_value = "1. Custom filtered task"
|
350
309
|
result = self.todo_manager.list_completed_tasks(filter="+urgent @home")
|
351
310
|
self.assertEqual(result, "1. Custom filtered task")
|
352
|
-
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
|
+
)
|
353
314
|
|
354
315
|
def test_list_completed_tasks_no_results(self):
|
355
316
|
"""Test listing completed tasks when no results found."""
|
356
317
|
self.todo_shell.list_completed.return_value = ""
|
357
318
|
result = self.todo_manager.list_completed_tasks(project="nonexistent")
|
358
319
|
self.assertEqual(result, "No completed tasks found matching the criteria.")
|
359
|
-
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
|
+
)
|
360
323
|
|
361
324
|
def test_add_task_sanitizes_inputs(self):
|
362
325
|
"""Test that add_task sanitizes inputs to prevent duplicates."""
|
@@ -421,7 +384,6 @@ class TestTodoManager(unittest.TestCase):
|
|
421
384
|
"projects": ["+project1", "+project1", "+project2"], # Duplicate project1
|
422
385
|
"contexts": ["@context1", "@context1", "@context2"], # Duplicate context1
|
423
386
|
"due": "2024-01-01",
|
424
|
-
"recurring": None,
|
425
387
|
"other_tags": [],
|
426
388
|
}
|
427
389
|
|
@@ -599,6 +561,111 @@ class TestTodoManager(unittest.TestCase):
|
|
599
561
|
assert components["contexts"].count("@context1") == 1
|
600
562
|
assert components["contexts"].count("@context2") == 1
|
601
563
|
|
564
|
+
def test_get_current_datetime(self):
|
565
|
+
"""Test getting current date and time."""
|
566
|
+
with patch("todo_agent.core.todo_manager.datetime") as mock_datetime:
|
567
|
+
mock_now = Mock()
|
568
|
+
mock_now.strftime.side_effect = lambda fmt: {
|
569
|
+
"%Y-%m-%d %H:%M:%S": "2025-01-15 10:30:00",
|
570
|
+
"%A, %B %d, %Y at %I:%M %p": "Wednesday, January 15, 2025 at 10:30 AM",
|
571
|
+
}[fmt]
|
572
|
+
mock_now.isocalendar.return_value = (2025, 3, 3)
|
573
|
+
mock_now.astimezone.return_value.tzinfo = Mock()
|
574
|
+
mock_datetime.now.return_value = mock_now
|
575
|
+
|
576
|
+
result = self.todo_manager.get_current_datetime()
|
577
|
+
self.assertIn("2025-01-15 10:30:00", result)
|
578
|
+
self.assertIn("Week 3", result)
|
579
|
+
|
580
|
+
def test_created_completed_task_basic(self):
|
581
|
+
"""Test creating and immediately completing a task."""
|
582
|
+
# Mock the todo_shell methods
|
583
|
+
self.todo_shell.add.return_value = "Task added"
|
584
|
+
self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
|
585
|
+
self.todo_shell.complete.return_value = "Task completed"
|
586
|
+
|
587
|
+
result = self.todo_manager.created_completed_task("Test task")
|
588
|
+
|
589
|
+
# Verify the task was added
|
590
|
+
self.todo_shell.add.assert_called_once_with("Test task")
|
591
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
592
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
593
|
+
# Verify the result message
|
594
|
+
self.assertIn("Created and completed task: Test task", result)
|
595
|
+
|
596
|
+
def test_created_completed_task_with_project_and_context(self):
|
597
|
+
"""Test creating and completing a task with project and context."""
|
598
|
+
self.todo_shell.add.return_value = "Task added"
|
599
|
+
self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
|
600
|
+
self.todo_shell.complete.return_value = "Task completed"
|
601
|
+
|
602
|
+
result = self.todo_manager.created_completed_task(
|
603
|
+
"Test task", project="work", context="office"
|
604
|
+
)
|
605
|
+
|
606
|
+
# Verify the task was added with project and context
|
607
|
+
self.todo_shell.add.assert_called_once_with("Test task +work @office")
|
608
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
609
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
610
|
+
self.assertIn("+work @office", result)
|
611
|
+
|
612
|
+
def test_created_completed_task_with_custom_date(self):
|
613
|
+
"""Test creating and completing a task with a custom completion date."""
|
614
|
+
self.todo_shell.add.return_value = "Task added"
|
615
|
+
self.todo_shell.list_tasks.return_value = "1 Test task"
|
616
|
+
self.todo_shell.complete.return_value = "Task completed"
|
617
|
+
|
618
|
+
result = self.todo_manager.created_completed_task(
|
619
|
+
"Test task", completion_date="2025-01-10"
|
620
|
+
)
|
621
|
+
|
622
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
623
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
624
|
+
self.assertIn("completed on 2025-01-10", result)
|
625
|
+
|
626
|
+
def test_created_completed_task_with_invalid_date(self):
|
627
|
+
"""Test that invalid completion date raises ValueError."""
|
628
|
+
with self.assertRaises(ValueError) as context:
|
629
|
+
self.todo_manager.created_completed_task(
|
630
|
+
"Test task", completion_date="invalid-date"
|
631
|
+
)
|
632
|
+
self.assertIn("Invalid completion date format", str(context.exception))
|
633
|
+
|
634
|
+
def test_created_completed_task_sanitizes_project_and_context(self):
|
635
|
+
"""Test that project and context with existing symbols are properly sanitized."""
|
636
|
+
self.todo_shell.add.return_value = "Task added"
|
637
|
+
self.todo_shell.list_tasks.return_value = "1 Test task"
|
638
|
+
self.todo_shell.complete.return_value = "Task completed"
|
639
|
+
|
640
|
+
result = self.todo_manager.created_completed_task(
|
641
|
+
"Test task", project="+work", context="@office"
|
642
|
+
)
|
643
|
+
|
644
|
+
# Verify the task was added with properly sanitized project and context
|
645
|
+
self.todo_shell.add.assert_called_once_with("Test task +work @office")
|
646
|
+
self.assertIn("+work @office", result)
|
647
|
+
|
648
|
+
def test_created_completed_task_with_empty_project_after_sanitization(self):
|
649
|
+
"""Test that empty project after sanitization raises ValueError."""
|
650
|
+
with self.assertRaises(ValueError) as context:
|
651
|
+
self.todo_manager.created_completed_task("Test task", project="+")
|
652
|
+
self.assertIn("Project name cannot be empty", str(context.exception))
|
653
|
+
|
654
|
+
def test_created_completed_task_with_empty_context_after_sanitization(self):
|
655
|
+
"""Test that empty context after sanitization raises ValueError."""
|
656
|
+
with self.assertRaises(ValueError) as context:
|
657
|
+
self.todo_manager.created_completed_task("Test task", context="@")
|
658
|
+
self.assertIn("Context name cannot be empty", str(context.exception))
|
659
|
+
|
660
|
+
def test_created_completed_task_no_tasks_after_addition(self):
|
661
|
+
"""Test that RuntimeError is raised when no tasks are found after addition."""
|
662
|
+
self.todo_shell.add.return_value = "Task added"
|
663
|
+
self.todo_shell.list_tasks.return_value = "" # No tasks found
|
664
|
+
|
665
|
+
with self.assertRaises(RuntimeError) as context:
|
666
|
+
self.todo_manager.created_completed_task("Test task")
|
667
|
+
self.assertIn("Failed to add task", str(context.exception))
|
668
|
+
|
602
669
|
|
603
670
|
if __name__ == "__main__":
|
604
671
|
unittest.main()
|
@@ -17,7 +17,7 @@ class TestCalendarUtils:
|
|
17
17
|
|
18
18
|
def test_get_calendar_output_success(self):
|
19
19
|
"""Test successful calendar output generation."""
|
20
|
-
mock_output = "Calendar output from cal
|
20
|
+
mock_output = "Calendar output from cal"
|
21
21
|
|
22
22
|
with patch("subprocess.run") as mock_run:
|
23
23
|
mock_result = Mock()
|
@@ -29,7 +29,7 @@ class TestCalendarUtils:
|
|
29
29
|
assert result == mock_output
|
30
30
|
assert len(result) > 0
|
31
31
|
mock_run.assert_called_once_with(
|
32
|
-
["cal"
|
32
|
+
["cal"], capture_output=True, text=True, check=True
|
33
33
|
)
|
34
34
|
|
35
35
|
def test_get_calendar_output_fallback(self):
|
@@ -104,7 +104,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
|
@@ -122,9 +124,9 @@ class TestInference:
|
|
122
124
|
"System prompt should contain properly formatted datetime"
|
123
125
|
)
|
124
126
|
|
125
|
-
# Verify that
|
126
|
-
assert "{
|
127
|
-
assert "
|
127
|
+
# Verify that calendar_output is also interpolated
|
128
|
+
assert "{calendar_output}" not in result
|
129
|
+
assert "September" in result # Should contain calendar month information
|
128
130
|
|
129
131
|
def test_process_request_with_tool_calls(self):
|
130
132
|
"""Test request processing with tool calls."""
|
@@ -0,0 +1,198 @@
|
|
1
|
+
"""
|
2
|
+
Tests for OllamaClient.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from unittest.mock import Mock, mock_open, patch
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
|
10
|
+
from todo_agent.infrastructure.config import Config
|
11
|
+
from todo_agent.infrastructure.ollama_client import OllamaClient
|
12
|
+
|
13
|
+
|
14
|
+
class TestOllamaClient:
|
15
|
+
"""Test OllamaClient functionality."""
|
16
|
+
|
17
|
+
def setup_method(self):
|
18
|
+
"""Set up test fixtures."""
|
19
|
+
self.config = Config()
|
20
|
+
self.config.provider = "ollama"
|
21
|
+
self.config.ollama_base_url = "http://localhost:11434"
|
22
|
+
self.config.ollama_model = "llama3.2"
|
23
|
+
|
24
|
+
# Patch the dependencies that are now initialized in the parent class
|
25
|
+
with patch("todo_agent.infrastructure.llm_client.Logger") as mock_logger, patch(
|
26
|
+
"todo_agent.infrastructure.llm_client.get_token_counter"
|
27
|
+
) as mock_token_counter:
|
28
|
+
mock_logger.return_value = Mock()
|
29
|
+
mock_token_counter.return_value = Mock()
|
30
|
+
|
31
|
+
self.client = OllamaClient(self.config)
|
32
|
+
|
33
|
+
def test_initialization(self):
|
34
|
+
"""Test client initialization."""
|
35
|
+
assert self.client.config == self.config
|
36
|
+
assert self.client.base_url == "http://localhost:11434"
|
37
|
+
assert self.client.model == "llama3.2"
|
38
|
+
assert self.client.logger is not None
|
39
|
+
assert self.client.token_counter is not None
|
40
|
+
|
41
|
+
def test_get_model_name(self):
|
42
|
+
"""Test getting model name."""
|
43
|
+
assert self.client.get_model_name() == "llama3.2"
|
44
|
+
|
45
|
+
def test_get_provider_name(self):
|
46
|
+
"""Test getting provider name."""
|
47
|
+
assert self.client.get_provider_name() == "ollama"
|
48
|
+
|
49
|
+
def test_get_request_timeout(self):
|
50
|
+
"""Test getting request timeout."""
|
51
|
+
assert self.client.get_request_timeout() == 120 # 2 minutes for Ollama
|
52
|
+
|
53
|
+
def test_get_request_headers(self):
|
54
|
+
"""Test request headers generation."""
|
55
|
+
headers = self.client._get_request_headers()
|
56
|
+
assert headers == {"Content-Type": "application/json"}
|
57
|
+
|
58
|
+
def test_get_request_payload(self):
|
59
|
+
"""Test request payload generation."""
|
60
|
+
messages = [{"role": "user", "content": "Hello"}]
|
61
|
+
tools = [{"name": "test_tool", "description": "A test tool"}]
|
62
|
+
|
63
|
+
payload = self.client._get_request_payload(messages, tools)
|
64
|
+
expected = {
|
65
|
+
"model": "llama3.2",
|
66
|
+
"messages": messages,
|
67
|
+
"tools": tools,
|
68
|
+
"stream": False,
|
69
|
+
}
|
70
|
+
assert payload == expected
|
71
|
+
|
72
|
+
def test_get_api_endpoint(self):
|
73
|
+
"""Test API endpoint generation."""
|
74
|
+
endpoint = self.client._get_api_endpoint()
|
75
|
+
assert endpoint == "http://localhost:11434/api/chat"
|
76
|
+
|
77
|
+
@patch("todo_agent.infrastructure.ollama_client.OllamaClient._make_http_request")
|
78
|
+
def test_chat_with_tools_success(self, mock_make_request):
|
79
|
+
"""Test successful chat with tools."""
|
80
|
+
# Mock the common HTTP request method
|
81
|
+
expected_response = {
|
82
|
+
"message": {"content": "Here are your tasks", "tool_calls": []}
|
83
|
+
}
|
84
|
+
mock_make_request.return_value = expected_response
|
85
|
+
|
86
|
+
messages = [{"role": "user", "content": "List my tasks"}]
|
87
|
+
tools = [{"function": {"name": "list_tasks", "description": "List tasks"}}]
|
88
|
+
|
89
|
+
response = self.client.chat_with_tools(messages, tools)
|
90
|
+
|
91
|
+
assert response == expected_response
|
92
|
+
mock_make_request.assert_called_once_with(messages, tools)
|
93
|
+
|
94
|
+
@patch("todo_agent.infrastructure.ollama_client.OllamaClient._make_http_request")
|
95
|
+
def test_chat_with_tools_api_error(self, mock_make_request):
|
96
|
+
"""Test chat with tools when API returns error."""
|
97
|
+
# Mock error response from the common method
|
98
|
+
error_response = {
|
99
|
+
"error": True,
|
100
|
+
"error_type": "general_error",
|
101
|
+
"provider": "ollama",
|
102
|
+
"status_code": 500,
|
103
|
+
"raw_error": "Internal Server Error",
|
104
|
+
}
|
105
|
+
mock_make_request.return_value = error_response
|
106
|
+
|
107
|
+
messages = [{"role": "user", "content": "List my tasks"}]
|
108
|
+
tools = [{"function": {"name": "list_tasks", "description": "List tasks"}}]
|
109
|
+
|
110
|
+
response = self.client.chat_with_tools(messages, tools)
|
111
|
+
assert response == error_response
|
112
|
+
|
113
|
+
def test_extract_tool_calls_with_tools(self):
|
114
|
+
"""Test extracting tool calls from response with tools."""
|
115
|
+
response = {
|
116
|
+
"message": {
|
117
|
+
"content": "I'll help you with that",
|
118
|
+
"tool_calls": [
|
119
|
+
{
|
120
|
+
"id": "call_1",
|
121
|
+
"function": {"name": "list_tasks", "arguments": "{}"},
|
122
|
+
}
|
123
|
+
],
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
tool_calls = self.client.extract_tool_calls(response)
|
128
|
+
assert len(tool_calls) == 1
|
129
|
+
assert tool_calls[0]["id"] == "call_1"
|
130
|
+
assert tool_calls[0]["function"]["name"] == "list_tasks"
|
131
|
+
|
132
|
+
def test_extract_tool_calls_without_tools(self):
|
133
|
+
"""Test extracting tool calls from response without tools."""
|
134
|
+
response = {
|
135
|
+
"message": {
|
136
|
+
"content": "I'll help you with that",
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
tool_calls = self.client.extract_tool_calls(response)
|
141
|
+
assert len(tool_calls) == 0
|
142
|
+
|
143
|
+
def test_extract_tool_calls_with_error_response(self):
|
144
|
+
"""Test extracting tool calls from error response."""
|
145
|
+
error_response = {"error": True, "error_type": "timeout", "provider": "ollama"}
|
146
|
+
|
147
|
+
tool_calls = self.client.extract_tool_calls(error_response)
|
148
|
+
assert len(tool_calls) == 0
|
149
|
+
|
150
|
+
def test_extract_content_with_content(self):
|
151
|
+
"""Test extracting content from response with content."""
|
152
|
+
response = {
|
153
|
+
"message": {
|
154
|
+
"content": "Here are your tasks",
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
content = self.client.extract_content(response)
|
159
|
+
assert content == "Here are your tasks"
|
160
|
+
|
161
|
+
def test_extract_content_without_content(self):
|
162
|
+
"""Test extracting content from response without content."""
|
163
|
+
response = {"message": {}}
|
164
|
+
|
165
|
+
content = self.client.extract_content(response)
|
166
|
+
assert content == ""
|
167
|
+
|
168
|
+
def test_extract_content_empty_response(self):
|
169
|
+
"""Test extracting content from empty response."""
|
170
|
+
response = {}
|
171
|
+
|
172
|
+
content = self.client.extract_content(response)
|
173
|
+
assert content == ""
|
174
|
+
|
175
|
+
def test_extract_content_with_error_response(self):
|
176
|
+
"""Test extracting content from error response."""
|
177
|
+
error_response = {"error": True, "error_type": "timeout", "provider": "ollama"}
|
178
|
+
|
179
|
+
content = self.client.extract_content(error_response)
|
180
|
+
assert content == ""
|
181
|
+
|
182
|
+
def test_process_response(self):
|
183
|
+
"""Test response processing and logging."""
|
184
|
+
response_data = {
|
185
|
+
"message": {
|
186
|
+
"content": "Hello",
|
187
|
+
"tool_calls": [{"id": "call_1", "function": {"name": "test_tool"}}],
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
with patch.object(self.client.logger, "info") as mock_info, patch.object(
|
192
|
+
self.client.logger, "debug"
|
193
|
+
) as mock_debug:
|
194
|
+
self.client._process_response(response_data, 0.0)
|
195
|
+
|
196
|
+
# Should log response details
|
197
|
+
assert mock_info.call_count >= 1
|
198
|
+
assert mock_debug.call_count >= 1
|