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