vector-task-mcp 1.2.0__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/PKG-INFO +1 -1
  2. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/pyproject.toml +61 -56
  3. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/models.py +3 -1
  4. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/task_store.py +47 -7
  5. vector_task_mcp-1.2.1/tests/test_task_store.py +509 -0
  6. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/PKG-INFO +1 -1
  7. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/SOURCES.txt +1 -0
  8. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/.mcp.json +0 -0
  9. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/.python-version +0 -0
  10. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/CLAUDE.md +0 -0
  11. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/LICENSE +0 -0
  12. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/MANIFEST.in +0 -0
  13. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/README.md +0 -0
  14. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/claude-desktop-config.example.json +0 -0
  15. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/main.py +0 -0
  16. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/requirements.txt +0 -0
  17. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/run-arm64.sh +0 -0
  18. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/setup.cfg +0 -0
  19. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/__init__.py +0 -0
  20. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/embeddings.py +0 -0
  21. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/security.py +0 -0
  22. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/dependency_links.txt +0 -0
  23. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/entry_points.txt +0 -0
  24. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/requires.txt +0 -0
  25. {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vector-task-mcp
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers
5
5
  Author-email: Xsaven <xsaven@gmail.com>
6
6
  License: MIT
@@ -1,56 +1,61 @@
1
- [project]
2
- name = "vector-task-mcp"
3
- version = "1.2.0"
4
- description = "A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- license = { text = "MIT" }
8
- authors = [
9
- { name = "Xsaven", email = "xsaven@gmail.com" }
10
- ]
11
- keywords = [
12
- "mcp",
13
- "model-context-protocol",
14
- "task-management",
15
- "sqlite",
16
- "embeddings",
17
- "semantic-search",
18
- "claude",
19
- "ai-tasks"
20
- ]
21
- classifiers = [
22
- "Development Status :: 4 - Beta",
23
- "Intended Audience :: Developers",
24
- "License :: OSI Approved :: MIT License",
25
- "Programming Language :: Python :: 3",
26
- "Programming Language :: Python :: 3.10",
27
- "Programming Language :: Python :: 3.11",
28
- "Programming Language :: Python :: 3.12",
29
- "Topic :: Software Development :: Libraries :: Python Modules",
30
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
- ]
32
-
33
- dependencies = [
34
- "mcp>=0.3.0",
35
- "sqlite-vec>=0.1.6",
36
- "sentence-transformers>=2.2.2",
37
- ]
38
-
39
- [project.urls]
40
- Homepage = "https://github.com/xsaven/vector-task-mcp"
41
- Repository = "https://github.com/xsaven/vector-task-mcp"
42
- Issues = "https://github.com/xsaven/vector-task-mcp/issues"
43
-
44
- [project.scripts]
45
- vector-task-mcp = "main:main"
46
-
47
- [build-system]
48
- requires = ["setuptools>=45", "wheel"]
49
- build-backend = "setuptools.build_meta"
50
-
51
- [tool.setuptools]
52
- packages = ["src"]
53
- py-modules = ["main"]
54
-
55
- [tool.setuptools.package-data]
56
- "*" = ["*.md"]
1
+ [project]
2
+ name = "vector-task-mcp"
3
+ version = "1.2.1"
4
+ description = "A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [
9
+ { name = "Xsaven", email = "xsaven@gmail.com" }
10
+ ]
11
+ keywords = [
12
+ "mcp",
13
+ "model-context-protocol",
14
+ "task-management",
15
+ "sqlite",
16
+ "embeddings",
17
+ "semantic-search",
18
+ "claude",
19
+ "ai-tasks"
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
+ ]
32
+
33
+ dependencies = [
34
+ "mcp>=0.3.0",
35
+ "sqlite-vec>=0.1.6",
36
+ "sentence-transformers>=2.2.2",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/xsaven/vector-task-mcp"
41
+ Repository = "https://github.com/xsaven/vector-task-mcp"
42
+ Issues = "https://github.com/xsaven/vector-task-mcp/issues"
43
+
44
+ [project.scripts]
45
+ vector-task-mcp = "main:main"
46
+
47
+ [build-system]
48
+ requires = ["setuptools>=45", "wheel"]
49
+ build-backend = "setuptools.build_meta"
50
+
51
+ [tool.setuptools]
52
+ packages = ["src"]
53
+ py-modules = ["main"]
54
+
55
+ [tool.setuptools.package-data]
56
+ "*" = ["*.md"]
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "pytest>=9.0.2",
61
+ ]
@@ -192,6 +192,7 @@ class Task:
192
192
  estimate: Optional[float] = None
193
193
  order: Optional[int] = None
194
194
  time_spent: float = 0.0
195
+ status_history: Optional[List[Dict[str, Any]]] = None
195
196
 
196
197
  def __post_init__(self):
197
198
  """Initialize default values"""
@@ -217,7 +218,8 @@ class Task:
217
218
  "tags": self.tags,
218
219
  "estimate": self.estimate,
219
220
  "order": self.order,
220
- "time_spent": self.time_spent
221
+ "time_spent": self.time_spent,
222
+ "status_history": self.status_history
221
223
  }
222
224
 
223
225
  @classmethod
@@ -240,9 +240,11 @@ class TaskStore:
240
240
  conn.execute('UPDATE tasks SET status = ? WHERE id = ?', (parent_status, parent_id))
241
241
  self._propagate_status_to_parents(conn, parent_id, parent_status)
242
242
  else:
243
- # Not all finished → parent should be pending (waiting for others)
244
- conn.execute('UPDATE tasks SET status = ? WHERE id = ?', ('pending', parent_id))
245
- # Don't propagate pending up - parent was already in correct state
243
+ # Not all finished → parent stays in_progress (active work in subtree)
244
+ conn.execute('UPDATE tasks SET status = ? WHERE id = ?', ('in_progress', parent_id))
245
+ # CRITICAL: Continue recursion to grandparent even when siblings not finished
246
+ # This ensures upper hierarchy is notified of activity in deep subtrees
247
+ self._propagate_status_to_parents(conn, parent_id, 'in_progress')
246
248
 
247
249
  elif new_status == 'in_progress':
248
250
  # Any child working → parent in_progress
@@ -407,6 +409,42 @@ class TaskStore:
407
409
  # Recursively propagate session finish to parent with same time_spent and finish_status
408
410
  self._finish_time_session(conn, parent_id, time_spent, finish_status)
409
411
 
412
+ def _get_status_history(self, conn: sqlite3.Connection, task_id: int, limit: int = 5) -> List[Dict[str, Any]]:
413
+ """
414
+ Get recent status transition history from task_time_log.
415
+ Returns completed sessions only (finish_at IS NOT NULL).
416
+
417
+ Args:
418
+ conn: Database connection
419
+ task_id: Task ID to get history for
420
+ limit: Maximum records to return (default 5)
421
+
422
+ Returns:
423
+ List of dicts with keys: from, to, at, spent
424
+ Ordered by finish_at DESC (most recent first)
425
+ """
426
+ cursor = conn.execute(
427
+ """
428
+ SELECT start_status, finish_status, finish_at, time_spent
429
+ FROM task_time_log
430
+ WHERE task_id = ? AND finish_at IS NOT NULL
431
+ ORDER BY finish_at DESC
432
+ LIMIT ?
433
+ """,
434
+ (task_id, limit)
435
+ )
436
+
437
+ history = []
438
+ for row in cursor.fetchall():
439
+ history.append({
440
+ "from": row[0],
441
+ "to": row[1],
442
+ "at": row[2],
443
+ "spent": row[3]
444
+ })
445
+
446
+ return history
447
+
410
448
  def create_task(self, title: str, content: str, parent_id: Optional[int] = None, comment: Optional[str] = None, priority: Optional[str] = None, tags: Optional[List[str]] = None, estimate: Optional[float] = None, order: Optional[int] = None) -> Dict[str, Any]:
411
449
  """
412
450
  Create a new task with vector embedding.
@@ -937,10 +975,9 @@ class TaskStore:
937
975
  # Start session when entering in_progress
938
976
  if new_status == 'in_progress' and old_status != 'in_progress':
939
977
  self._start_time_session(conn, task_id, old_status)
940
- # Finish session when exiting in_progress
978
+ # Finish session when exiting in_progress (always close, even with 0 time)
941
979
  elif old_status == 'in_progress' and new_status != 'in_progress':
942
- if time_delta > 0:
943
- self._finish_time_session(conn, task_id, time_delta, new_status)
980
+ self._finish_time_session(conn, task_id, time_delta, new_status)
944
981
 
945
982
  conn.commit()
946
983
 
@@ -1121,7 +1158,10 @@ class TaskStore:
1121
1158
  """, (task_id,)).fetchone()
1122
1159
 
1123
1160
  if result:
1124
- return Task.from_db_row(result)
1161
+ task = Task.from_db_row(result)
1162
+ # Attach status history (last 5 transitions)
1163
+ task.status_history = self._get_status_history(conn, task_id)
1164
+ return task
1125
1165
  return None
1126
1166
 
1127
1167
  except Exception as e:
@@ -0,0 +1,509 @@
1
+ """
2
+ TaskStore Unit Tests
3
+ ====================
4
+
5
+ Tests for src/task_store.py including:
6
+ - Task CRUD operations
7
+ - Status propagation in hierarchies
8
+ - Deep hierarchy status propagation (4 levels)
9
+ - Parent-child relationship management
10
+ """
11
+
12
+ import pytest
13
+ from unittest.mock import patch
14
+ from pathlib import Path
15
+
16
+
17
+ class TestDeepHierarchyStatusPropagation:
18
+ """
19
+ Test suite for deep hierarchy status propagation.
20
+
21
+ Verifies that status changes in deeply nested tasks correctly
22
+ propagate up through the entire parent chain to the root.
23
+
24
+ Bug context (memory #72):
25
+ - When deep nested subtasks complete (3+ levels deep), parent tasks
26
+ at certain depths remained `in_progress` instead of updating correctly.
27
+ - Fix: Continue recursion to grandparent even when siblings not finished.
28
+ """
29
+
30
+ def test_deep_hierarchy_status_propagation_to_root(self, task_store, deep_hierarchy):
31
+ """
32
+ Test that status propagation works correctly through 4-level deep hierarchy.
33
+
34
+ Scenario:
35
+ 1. Create 4-level hierarchy: Root -> Level1 -> Level2 -> Level3
36
+ 2. Set Level3 (deepest) to in_progress, then completed
37
+ 3. Assert that Level2, Level1, and Root ALL update their status correctly
38
+ 4. Specifically verify Root does NOT stay stuck at in_progress
39
+
40
+ Expected behavior:
41
+ - When Level3 starts (in_progress): All parents should become in_progress
42
+ - When Level3 completes: All parents should become completed
43
+ (since each level has only one child)
44
+ """
45
+ level0_id = deep_hierarchy['level0_id']
46
+ level1_id = deep_hierarchy['level1_id']
47
+ level2_id = deep_hierarchy['level2_id']
48
+ level3_id = deep_hierarchy['level3_id']
49
+
50
+ # Verify initial state - all tasks should be pending
51
+ for task_id in [level0_id, level1_id, level2_id, level3_id]:
52
+ task = task_store.get_task_by_id(task_id)
53
+ assert task.status == 'pending', f"Task {task_id} should start as pending"
54
+
55
+ # Step 1: Set Level3 (deepest leaf) to in_progress
56
+ task_store.update_task(level3_id, status='in_progress')
57
+
58
+ # Verify in_progress propagated to all parents
59
+ for task_id, level_name in [
60
+ (level2_id, "Level2"),
61
+ (level1_id, "Level1"),
62
+ (level0_id, "Root")
63
+ ]:
64
+ task = task_store.get_task_by_id(task_id)
65
+ assert task.status == 'in_progress', (
66
+ f"{level_name} (id={task_id}) should be in_progress when child is working. "
67
+ f"Actual status: {task.status}"
68
+ )
69
+
70
+ # Step 2: Complete Level3 (deepest leaf)
71
+ task_store.update_task(level3_id, status='completed')
72
+
73
+ # Verify Level3 is completed
74
+ level3_task = task_store.get_task_by_id(level3_id)
75
+ assert level3_task.status == 'completed', "Level3 should be completed"
76
+
77
+ # Verify completion propagated to all parents
78
+ # Since each level has only one child, ALL parents should be completed
79
+ for task_id, level_name in [
80
+ (level2_id, "Level2"),
81
+ (level1_id, "Level1"),
82
+ (level0_id, "Root")
83
+ ]:
84
+ task = task_store.get_task_by_id(task_id)
85
+ assert task.status == 'completed', (
86
+ f"{level_name} (id={task_id}) should be completed when all children are done. "
87
+ f"Actual status: {task.status}. "
88
+ f"This indicates status propagation did not reach the root level."
89
+ )
90
+
91
+ def test_deep_hierarchy_partial_completion(self, task_store):
92
+ """
93
+ Test status propagation when not all siblings at a level are complete.
94
+
95
+ Scenario:
96
+ - Root has Child1 and Child2
97
+ - Child1 has Grandchild1A and Grandchild1B
98
+ - Completing Grandchild1A should NOT complete Child1 (sibling Grandchild1B still pending)
99
+ - Root should stay in_progress (has incomplete subtree)
100
+ """
101
+ # Create hierarchy
102
+ root = task_store.create_task(
103
+ title="Root",
104
+ content="Root with multiple children"
105
+ )
106
+ root_id = root['task_id']
107
+
108
+ child1 = task_store.create_task(
109
+ title="Child 1",
110
+ content="First child with grandchildren",
111
+ parent_id=root_id
112
+ )
113
+ child1_id = child1['task_id']
114
+
115
+ child2 = task_store.create_task(
116
+ title="Child 2",
117
+ content="Second child",
118
+ parent_id=root_id
119
+ )
120
+ child2_id = child2['task_id']
121
+
122
+ gc1a = task_store.create_task(
123
+ title="Grandchild 1A",
124
+ content="First grandchild",
125
+ parent_id=child1_id
126
+ )
127
+ gc1a_id = gc1a['task_id']
128
+
129
+ gc1b = task_store.create_task(
130
+ title="Grandchild 1B",
131
+ content="Second grandchild",
132
+ parent_id=child1_id
133
+ )
134
+ gc1b_id = gc1b['task_id']
135
+
136
+ # Start work on Grandchild1A
137
+ task_store.update_task(gc1a_id, status='in_progress')
138
+
139
+ # Verify in_progress propagated correctly
140
+ child1_task = task_store.get_task_by_id(child1_id)
141
+ root_task = task_store.get_task_by_id(root_id)
142
+ assert child1_task.status == 'in_progress', "Child1 should be in_progress"
143
+ assert root_task.status == 'in_progress', "Root should be in_progress"
144
+
145
+ # Complete Grandchild1A
146
+ task_store.update_task(gc1a_id, status='completed')
147
+
148
+ # Verify Child1 stays in_progress (Grandchild1B still pending)
149
+ child1_task = task_store.get_task_by_id(child1_id)
150
+ assert child1_task.status == 'in_progress', (
151
+ "Child1 should stay in_progress because Grandchild1B is still pending"
152
+ )
153
+
154
+ # Verify Root stays in_progress
155
+ root_task = task_store.get_task_by_id(root_id)
156
+ assert root_task.status == 'in_progress', (
157
+ "Root should stay in_progress because its subtree is not fully complete"
158
+ )
159
+
160
+ # Now complete Grandchild1B
161
+ task_store.update_task(gc1b_id, status='in_progress')
162
+ task_store.update_task(gc1b_id, status='completed')
163
+
164
+ # Now Child1 should be completed (all grandchildren done)
165
+ child1_task = task_store.get_task_by_id(child1_id)
166
+ assert child1_task.status == 'completed', (
167
+ "Child1 should be completed when all grandchildren are done"
168
+ )
169
+
170
+ # Root should still be in_progress (Child2 still pending)
171
+ root_task = task_store.get_task_by_id(root_id)
172
+ assert root_task.status == 'in_progress', (
173
+ "Root should stay in_progress because Child2 is still pending"
174
+ )
175
+
176
+ # Complete Child2 (no grandchildren, so direct completion works)
177
+ task_store.update_task(child2_id, status='in_progress')
178
+ task_store.update_task(child2_id, status='completed')
179
+
180
+ # Now Root should be completed
181
+ root_task = task_store.get_task_by_id(root_id)
182
+ assert root_task.status == 'completed', (
183
+ "Root should be completed when all children and their subtrees are done"
184
+ )
185
+
186
+ def test_deep_hierarchy_tested_status_propagation(self, task_store, deep_hierarchy):
187
+ """
188
+ Test that 'tested' status propagates correctly and parents get 'completed'.
189
+
190
+ Expected behavior:
191
+ - Child with 'tested' status counts as finished
192
+ - Parent should get 'completed' (not 'tested') when all children finish
193
+ - This tests the fix for tested/validated status propagation
194
+ """
195
+ level0_id = deep_hierarchy['level0_id']
196
+ level1_id = deep_hierarchy['level1_id']
197
+ level2_id = deep_hierarchy['level2_id']
198
+ level3_id = deep_hierarchy['level3_id']
199
+
200
+ # Start and complete with 'tested' status at leaf
201
+ task_store.update_task(level3_id, status='in_progress')
202
+ task_store.update_task(level3_id, status='tested')
203
+
204
+ # Verify all parents got 'completed' (not 'tested')
205
+ for task_id, level_name in [
206
+ (level2_id, "Level2"),
207
+ (level1_id, "Level1"),
208
+ (level0_id, "Root")
209
+ ]:
210
+ task = task_store.get_task_by_id(task_id)
211
+ assert task.status == 'completed', (
212
+ f"{level_name} (id={task_id}) should be 'completed' (not 'tested'). "
213
+ f"Parent tasks always get 'completed' when children finish. "
214
+ f"Actual status: {task.status}"
215
+ )
216
+
217
+ def test_deep_hierarchy_validated_status_propagation(self, task_store, deep_hierarchy):
218
+ """
219
+ Test that 'validated' status propagates correctly and parents get 'completed'.
220
+
221
+ Same as tested, but with 'validated' status.
222
+ """
223
+ level0_id = deep_hierarchy['level0_id']
224
+ level1_id = deep_hierarchy['level1_id']
225
+ level2_id = deep_hierarchy['level2_id']
226
+ level3_id = deep_hierarchy['level3_id']
227
+
228
+ # Start and complete with 'validated' status at leaf
229
+ task_store.update_task(level3_id, status='in_progress')
230
+ task_store.update_task(level3_id, status='validated')
231
+
232
+ # Verify all parents got 'completed' (not 'validated')
233
+ for task_id, level_name in [
234
+ (level2_id, "Level2"),
235
+ (level1_id, "Level1"),
236
+ (level0_id, "Root")
237
+ ]:
238
+ task = task_store.get_task_by_id(task_id)
239
+ assert task.status == 'completed', (
240
+ f"{level_name} should be 'completed' not 'validated'. "
241
+ f"Actual: {task.status}"
242
+ )
243
+
244
+
245
+ class TestStatusPropagationEdgeCases:
246
+ """
247
+ Edge case tests for status propagation.
248
+ """
249
+
250
+ def test_root_task_no_parent_propagation(self, task_store):
251
+ """
252
+ Test that root tasks (no parent) don't cause errors on status change.
253
+ """
254
+ root = task_store.create_task(
255
+ title="Root Only",
256
+ content="A standalone root task"
257
+ )
258
+ root_id = root['task_id']
259
+
260
+ # Should not raise any errors
261
+ task_store.update_task(root_id, status='in_progress')
262
+ task = task_store.get_task_by_id(root_id)
263
+ assert task.status == 'in_progress'
264
+
265
+ task_store.update_task(root_id, status='completed')
266
+ task = task_store.get_task_by_id(root_id)
267
+ assert task.status == 'completed'
268
+
269
+ def test_stopped_status_does_not_complete_parent(self, task_store, deep_hierarchy):
270
+ """
271
+ Test that 'stopped' status keeps parent as in_progress, not pending.
272
+ """
273
+ level1_id = deep_hierarchy['level1_id']
274
+ level2_id = deep_hierarchy['level2_id']
275
+ level3_id = deep_hierarchy['level3_id']
276
+
277
+ # Start work on Level3
278
+ task_store.update_task(level3_id, status='in_progress')
279
+
280
+ # Verify parent is in_progress
281
+ level2_task = task_store.get_task_by_id(level2_id)
282
+ assert level2_task.status == 'in_progress'
283
+
284
+ # Stop Level3 (blocking, not completed)
285
+ task_store.update_task(level3_id, status='stopped')
286
+
287
+ # Parent should remain pending (no active work, not completed)
288
+ level2_task = task_store.get_task_by_id(level2_id)
289
+ assert level2_task.status == 'pending', (
290
+ "Level2 should be pending when child is stopped (no active work)"
291
+ )
292
+
293
+ def test_mixed_finish_statuses_complete_parent(self, task_store):
294
+ """
295
+ Test that parent completes when children have mixed finish statuses.
296
+
297
+ Child1: completed
298
+ Child2: tested
299
+ Child3: validated
300
+
301
+ Parent should become 'completed' when all are finished.
302
+ """
303
+ root = task_store.create_task(title="Root", content="Root task")
304
+ root_id = root['task_id']
305
+
306
+ child1 = task_store.create_task(
307
+ title="Child 1", content="First", parent_id=root_id
308
+ )
309
+ child2 = task_store.create_task(
310
+ title="Child 2", content="Second", parent_id=root_id
311
+ )
312
+ child3 = task_store.create_task(
313
+ title="Child 3", content="Third", parent_id=root_id
314
+ )
315
+
316
+ # Complete children with different statuses
317
+ task_store.update_task(child1['task_id'], status='in_progress')
318
+ task_store.update_task(child1['task_id'], status='completed')
319
+
320
+ task_store.update_task(child2['task_id'], status='in_progress')
321
+ task_store.update_task(child2['task_id'], status='tested')
322
+
323
+ task_store.update_task(child3['task_id'], status='in_progress')
324
+ task_store.update_task(child3['task_id'], status='validated')
325
+
326
+ # Root should be completed
327
+ root_task = task_store.get_task_by_id(root_id)
328
+ assert root_task.status == 'completed', (
329
+ "Root should be 'completed' when all children have finish statuses. "
330
+ f"Actual: {root_task.status}"
331
+ )
332
+
333
+
334
+ class TestTaskCRUD:
335
+ """
336
+ Basic CRUD operation tests for TaskStore.
337
+ """
338
+
339
+ def test_create_task(self, task_store):
340
+ """Test basic task creation."""
341
+ result = task_store.create_task(
342
+ title="Test Task",
343
+ content="Test content"
344
+ )
345
+
346
+ assert result['success'] is True
347
+ assert result['task_id'] > 0
348
+ assert result['status'] == 'pending'
349
+
350
+ def test_create_task_with_parent(self, task_store):
351
+ """Test creating child task with parent_id."""
352
+ parent = task_store.create_task(title="Parent", content="Parent content")
353
+ child = task_store.create_task(
354
+ title="Child",
355
+ content="Child content",
356
+ parent_id=parent['task_id']
357
+ )
358
+
359
+ assert child['success'] is True
360
+
361
+ # Verify relationship
362
+ child_task = task_store.get_task_by_id(child['task_id'])
363
+ assert child_task.parent_id == parent['task_id']
364
+
365
+ def test_update_task_status(self, task_store):
366
+ """Test updating task status."""
367
+ result = task_store.create_task(title="Task", content="Content")
368
+ task_id = result['task_id']
369
+
370
+ # Update to in_progress
371
+ update_result = task_store.update_task(task_id, status='in_progress')
372
+ assert update_result['success'] is True
373
+ assert update_result['task']['status'] == 'in_progress'
374
+
375
+ def test_delete_task(self, task_store):
376
+ """Test task deletion."""
377
+ result = task_store.create_task(title="To Delete", content="Will be deleted")
378
+ task_id = result['task_id']
379
+
380
+ deleted = task_store.delete_task(task_id)
381
+ assert deleted is True
382
+
383
+ # Verify task no longer exists
384
+ task = task_store.get_task_by_id(task_id)
385
+ assert task is None
386
+
387
+ def test_get_task_by_id(self, task_store):
388
+ """Test retrieving task by ID."""
389
+ result = task_store.create_task(
390
+ title="Retrieve Me",
391
+ content="Content to retrieve",
392
+ priority="high"
393
+ )
394
+ task_id = result['task_id']
395
+
396
+ task = task_store.get_task_by_id(task_id)
397
+ assert task is not None
398
+ assert task.id == task_id
399
+ assert task.title == "Retrieve Me"
400
+ assert task.priority == "high"
401
+
402
+ def test_get_nonexistent_task(self, task_store):
403
+ """Test retrieving non-existent task returns None."""
404
+ task = task_store.get_task_by_id(99999)
405
+ assert task is None
406
+
407
+
408
+ class TestTaskSearch:
409
+ """
410
+ Tests for task search and filtering functionality.
411
+ """
412
+
413
+ def test_search_by_status(self, task_store, sample_task_data):
414
+ """Test filtering tasks by status."""
415
+ # Create tasks
416
+ for data in sample_task_data:
417
+ task_store.create_task(**data)
418
+
419
+ # Get first task and set to in_progress
420
+ tasks, _ = task_store.search_tasks(limit=10)
421
+ if tasks:
422
+ task_store.update_task(tasks[0].id, status='in_progress')
423
+
424
+ # Search by status
425
+ in_progress_tasks, count = task_store.search_tasks(status='in_progress')
426
+ assert count >= 1
427
+ for task in in_progress_tasks:
428
+ assert task.status == 'in_progress'
429
+
430
+ def test_search_by_parent_id(self, task_store, simple_hierarchy):
431
+ """Test filtering tasks by parent_id."""
432
+ root_id = simple_hierarchy['root_id']
433
+
434
+ children, count = task_store.search_tasks(parent_id=root_id)
435
+ assert count == 1
436
+ assert children[0].parent_id == root_id
437
+
438
+ def test_search_with_query(self, task_store, sample_task_data):
439
+ """Test semantic search with query."""
440
+ # Create tasks
441
+ for data in sample_task_data:
442
+ task_store.create_task(**data)
443
+
444
+ # Search with query
445
+ results, count = task_store.search_tasks(query="authentication security")
446
+ assert count > 0
447
+
448
+
449
+ class TestTaskStats:
450
+ """
451
+ Tests for task statistics functionality.
452
+ """
453
+
454
+ def test_get_stats_empty_db(self, task_store):
455
+ """Test stats on empty database."""
456
+ stats = task_store.get_stats()
457
+ assert stats.total_tasks == 0
458
+ assert stats.pending_count == 0
459
+ assert stats.in_progress_count == 0
460
+
461
+ def test_get_stats_with_tasks(self, task_store, sample_task_data):
462
+ """Test stats with existing tasks."""
463
+ # Create tasks
464
+ for data in sample_task_data:
465
+ task_store.create_task(**data)
466
+
467
+ stats = task_store.get_stats()
468
+ assert stats.total_tasks == len(sample_task_data)
469
+ assert stats.pending_count == len(sample_task_data) # All start as pending
470
+
471
+
472
+ class TestBulkOperations:
473
+ """
474
+ Tests for bulk task operations.
475
+ """
476
+
477
+ def test_create_tasks_bulk(self, task_store):
478
+ """Test bulk task creation."""
479
+ tasks_to_create = [
480
+ {"title": "Bulk Task 1", "content": "Content 1"},
481
+ {"title": "Bulk Task 2", "content": "Content 2"},
482
+ {"title": "Bulk Task 3", "content": "Content 3"},
483
+ ]
484
+
485
+ result = task_store.create_tasks_bulk(tasks_to_create)
486
+ assert result['success'] is True
487
+ assert result['count'] == 3
488
+ assert len(result['created_task_ids']) == 3
489
+
490
+ def test_delete_tasks_bulk(self, task_store):
491
+ """Test bulk task deletion."""
492
+ # Create tasks first
493
+ task_ids = []
494
+ for i in range(3):
495
+ result = task_store.create_task(
496
+ title=f"Delete Task {i}",
497
+ content=f"Content {i}"
498
+ )
499
+ task_ids.append(result['task_id'])
500
+
501
+ # Delete in bulk
502
+ result = task_store.delete_tasks_bulk(task_ids)
503
+ assert result['success'] is True
504
+ assert result['deleted_count'] == 3
505
+
506
+ # Verify all deleted
507
+ for task_id in task_ids:
508
+ task = task_store.get_task_by_id(task_id)
509
+ assert task is None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vector-task-mcp
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers
5
5
  Author-email: Xsaven <xsaven@gmail.com>
6
6
  License: MIT
@@ -14,6 +14,7 @@ src/embeddings.py
14
14
  src/models.py
15
15
  src/security.py
16
16
  src/task_store.py
17
+ tests/test_task_store.py
17
18
  vector_task_mcp.egg-info/PKG-INFO
18
19
  vector_task_mcp.egg-info/SOURCES.txt
19
20
  vector_task_mcp.egg-info/dependency_links.txt
File without changes
File without changes