feat(issues): S2 MCP tools — system CRUD + issue/system wiring
Second slice of Issues + Systems (spec #825). New mcp/tools/systems.py: create_system, list_systems, get_system (records split into issues/tasks/notes), update_system (incl. archive via status), list_system_records (kind/open_only filters), delete_system. Registered in register_all; read tools (get_system, list_systems, list_system_records) added to the read-only-key allowlist (write tools default-deny). create_task/update_task: kind now accepts 'issue'; new system_ids (set-semantics associations) and arose_from_id (provenance, 0=unchanged/-1=clear) args. create_note/update_note: new system_ids arg (notes associate with systems too). services/notes.create_note: arose_from_id passthrough (update_note already handles it via setattr). Tests: MCP system tools + create_task issue-wiring (kind/provenance/systems), service layer mocked. Refs plan 825 (S2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""MCP system tools + task/note issue wiring (service layer mocked)."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _fake_system(sid=1, name="Reader", project_id=5):
|
||||
s = MagicMock()
|
||||
s.to_dict.return_value = {"id": sid, "name": name, "project_id": project_id}
|
||||
s.project_id = project_id
|
||||
return s
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_returns_dict():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.create_system = AsyncMock(return_value=_fake_system(name="Reader"))
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
result = await create_system(project_id=5, name="Reader", description="pdf reader")
|
||||
assert result["name"] == "Reader"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_no_access_raises():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.create_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
with pytest.raises(ValueError):
|
||||
await create_system(project_id=5, name="Reader")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_system_splits_records_by_kind():
|
||||
issue = MagicMock(); issue.to_dict.return_value = {"id": 10}; issue.task_kind = "issue"; issue.status = "todo"
|
||||
work = MagicMock(); work.to_dict.return_value = {"id": 11}; work.task_kind = "work"; work.status = "todo"
|
||||
note = MagicMock(); note.to_dict.return_value = {"id": 12}; note.task_kind = "work"; note.status = None
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.get_system = AsyncMock(return_value=_fake_system(sid=3))
|
||||
svc.list_records_for_system = AsyncMock(return_value=[issue, work, note])
|
||||
from scribe.mcp.tools.systems import get_system
|
||||
result = await get_system(system_id=3)
|
||||
assert [r["id"] for r in result["issues"]] == [10]
|
||||
assert [r["id"] for r in result["tasks"]] == [11]
|
||||
assert [r["id"] for r in result["notes"]] == [12]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_system_not_found_raises():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.update_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.systems import update_system
|
||||
with pytest.raises(ValueError):
|
||||
await update_system(system_id=99, name="x")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_issue_sets_kind_provenance_and_systems():
|
||||
note = MagicMock(); note.id = 50; note.to_dict.return_value = {"id": 50, "task_kind": "issue"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.systems_svc") as systems_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
systems_svc.set_record_systems = AsyncMock(return_value=[2, 3])
|
||||
systems_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
result = await create_task(title="bug", kind="issue", system_ids=[2, 3], arose_from_id=9)
|
||||
_, kwargs = notes_svc.create_note.call_args
|
||||
assert kwargs["task_kind"] == "issue"
|
||||
assert kwargs["arose_from_id"] == 9
|
||||
systems_svc.set_record_systems.assert_awaited_once_with(1, 50, [2, 3])
|
||||
assert result["id"] == 50
|
||||
Reference in New Issue
Block a user