"""Tests for fable_*_project tools.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.projects import ( fable_list_projects, fable_get_project, fable_create_project, fable_update_project, ) @pytest.fixture(autouse=True) def _bind_user(): token = _user_id_ctx.set(7) yield _user_id_ctx.reset(token) def _fake_project(**overrides) -> MagicMock: p = MagicMock() base = {"id": 1, "title": "P", "description": "", "goal": "", "status": "active", "color": None, "auto_summary": None} base.update(overrides) p.to_dict.return_value = base return p @pytest.mark.asyncio async def test_list_projects_wraps_in_dict(): rows = [_fake_project(id=1), _fake_project(id=2)] with patch( "fabledassistant.mcp.tools.projects.projects_svc.list_projects", AsyncMock(return_value=rows), ): out = await fable_list_projects() assert len(out["projects"]) == 2 @pytest.mark.asyncio async def test_get_project_enriches_with_milestone_summary(): p = _fake_project(id=5, title="found") milestone_summary = [{"id": 10, "title": "MS", "task_count": 3}] with patch( "fabledassistant.mcp.tools.projects.projects_svc.get_project", AsyncMock(return_value=p), ), patch( "fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary", AsyncMock(return_value=milestone_summary), ): out = await fable_get_project(project_id=5) assert out["id"] == 5 assert out["milestone_summary"] == milestone_summary @pytest.mark.asyncio async def test_get_project_raises_when_not_found(): with patch( "fabledassistant.mcp.tools.projects.projects_svc.get_project", AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="project 999 not found"): await fable_get_project(project_id=999) @pytest.mark.asyncio async def test_create_project_passes_color_empty_as_none(): p = _fake_project() mock = AsyncMock(return_value=p) with patch("fabledassistant.mcp.tools.projects.projects_svc.create_project", mock): await fable_create_project(title="P", color="") assert mock.call_args.kwargs["color"] is None @pytest.mark.asyncio async def test_update_project_only_sends_non_default_fields(): p = _fake_project() mock = AsyncMock(return_value=p) with patch("fabledassistant.mcp.tools.projects.projects_svc.update_project", mock): await fable_update_project(project_id=1, status="archived") args, kwargs = mock.call_args assert args == (7, 1) assert kwargs == {"status": "archived"} @pytest.mark.asyncio async def test_update_project_raises_when_not_found(): with patch( "fabledassistant.mcp.tools.projects.projects_svc.update_project", AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="project 999 not found"): await fable_update_project(project_id=999, title="x")