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.
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/PKG-INFO +1 -1
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/pyproject.toml +61 -56
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/models.py +3 -1
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/task_store.py +47 -7
- vector_task_mcp-1.2.1/tests/test_task_store.py +509 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/PKG-INFO +1 -1
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/SOURCES.txt +1 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/.mcp.json +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/.python-version +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/CLAUDE.md +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/LICENSE +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/MANIFEST.in +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/README.md +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/claude-desktop-config.example.json +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/main.py +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/requirements.txt +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/run-arm64.sh +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/setup.cfg +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/__init__.py +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/embeddings.py +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/src/security.py +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/dependency_links.txt +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/entry_points.txt +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/requires.txt +0 -0
- {vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/top_level.txt +0 -0
|
@@ -1,56 +1,61 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "vector-task-mcp"
|
|
3
|
-
version = "1.2.
|
|
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
|
|
244
|
-
conn.execute('UPDATE tasks SET status = ? WHERE id = ?', ('
|
|
245
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vector_task_mcp-1.2.0 → vector_task_mcp-1.2.1}/vector_task_mcp.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|