"""Tests for MCP snippet tools — patches the service layer.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from scribe.mcp._context import _user_id_ctx @pytest.fixture(autouse=True) def _bind_user(): token = _user_id_ctx.set(7) yield _user_id_ctx.reset(token) def _fake_snippet(): n = MagicMock() n.id = 1 n.title = "debounce — rate-limit a callback" n.body = "```js\nreturn 1\n```\n" n.tags = ["js", "snippet"] n.note_type = "snippet" n.to_dict.return_value = { "id": 1, "title": n.title, "note_type": "snippet", "tags": n.tags, } return n @pytest.mark.asyncio async def test_create_snippet_requires_name_and_code(): from scribe.mcp.tools.snippets import create_snippet with pytest.raises(ValueError): await create_snippet(name="", code="x") with pytest.raises(ValueError): await create_snippet(name="x", code=" ") @pytest.mark.asyncio async def test_create_snippet_records_and_returns_parsed(): created = _fake_snippet() with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \ patch("scribe.services.snippets.create_snippet", AsyncMock(return_value=created)) as mock_create: from scribe.mcp.tools.snippets import create_snippet out = await create_snippet( name="debounce", code="return 1", language="js", when_to_use="rate-limit a callback", ) assert out["note_type"] == "snippet" assert out["snippet"]["name"] == "debounce" assert out["snippet"]["language"] == "js" assert mock_create.await_args.kwargs["name"] == "debounce" @pytest.mark.asyncio async def test_create_snippet_dedup_blocks_and_labels_snippet(): dup = MagicMock(id=99) with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=dup)), \ patch("scribe.services.dedup.duplicate_response", MagicMock(return_value={"duplicate": True, "existing_id": 99})) as mock_resp, \ patch("scribe.services.snippets.create_snippet", AsyncMock()) as mock_create: from scribe.mcp.tools.snippets import create_snippet out = await create_snippet(name="debounce", code="return 1") assert out["duplicate"] is True mock_create.assert_not_awaited() assert mock_resp.call_args.args[1] == "snippet" @pytest.mark.asyncio async def test_get_snippet_not_found_raises(): with patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=None)): from scribe.mcp.tools.snippets import get_snippet with pytest.raises(ValueError): await get_snippet(123) @pytest.mark.asyncio async def test_update_snippet_missing_raises(): with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=None)): from scribe.mcp.tools.snippets import update_snippet with pytest.raises(ValueError): await update_snippet(123, name="x") @pytest.mark.asyncio async def test_update_snippet_empty_string_clears_a_field(): # An omitted field must stay None ("leave alone"), but an explicit empty # string has to reach the service as "" so a stale field can be removed. updated = _fake_snippet() with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=updated)) as mock_update: from scribe.mcp.tools.snippets import update_snippet await update_snippet(1, signature="") kwargs = mock_update.await_args.kwargs assert kwargs["signature"] == "" # cleared assert kwargs["name"] is None # untouched @pytest.mark.asyncio async def test_update_snippet_project_id_conventions(): from scribe.services import snippets as snippets_svc updated = _fake_snippet() cases = {0: snippets_svc.UNSET, -1: None, 5: 5} for given, expected in cases.items(): with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=updated)) as mock_update: from scribe.mcp.tools.snippets import update_snippet await update_snippet(1, project_id=given) assert mock_update.await_args.kwargs["project_id"] is expected @pytest.mark.asyncio async def test_create_and_update_pass_locations_through(): locs = [{"repo": "a", "path": "a.py", "symbol": "f"}, {"repo": "b", "path": "b.py", "symbol": "g"}] created = _fake_snippet() with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \ patch("scribe.services.snippets.create_snippet", AsyncMock(return_value=created)) as mock_create: from scribe.mcp.tools.snippets import create_snippet await create_snippet(name="f", code="x", locations=locs) assert mock_create.await_args.kwargs["locations"] == locs with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=created)) as mock_update: from scribe.mcp.tools.snippets import update_snippet await update_snippet(1, locations=locs) assert mock_update.await_args.kwargs["locations"] == locs @pytest.mark.asyncio async def test_delete_snippet_retires_or_raises(): from scribe.mcp.tools.snippets import delete_snippet with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=True)): assert await delete_snippet(1) == {"deleted": True, "id": 1} with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=False)): with pytest.raises(ValueError): await delete_snippet(404) @pytest.mark.asyncio async def test_list_snippets_defaults_to_every_project(): with patch("scribe.services.snippets.list_snippets", AsyncMock(return_value=([], 0))) as mock_list: from scribe.mcp.tools.snippets import list_snippets await list_snippets(q="debounce") assert mock_list.await_args.kwargs["project_id"] is None await list_snippets(q="debounce", project_id=3) assert mock_list.await_args.kwargs["project_id"] == 3 @pytest.mark.asyncio async def test_merge_snippets_requires_a_source(): from scribe.mcp.tools.snippets import merge_snippets with pytest.raises(ValueError): await merge_snippets(target_id=1, source_ids=[]) with pytest.raises(ValueError): await merge_snippets(target_id=1, source_ids=[1]) # only self → nothing to merge @pytest.mark.asyncio async def test_merge_snippets_returns_survivor_and_merged_ids(): survivor = _fake_snippet() with patch("scribe.services.snippets.merge_snippets", AsyncMock(return_value=(survivor, [2, 3]))) as mock_merge: from scribe.mcp.tools.snippets import merge_snippets out = await merge_snippets(target_id=1, source_ids=[2, 3, 1]) assert out["merged_ids"] == [2, 3] assert out["snippet"]["name"] == "debounce" # target_id passed through; self-id filtered out of the source list. assert mock_merge.await_args.args[1] == 1 assert mock_merge.await_args.args[2] == [2, 3] @pytest.mark.asyncio async def test_merge_snippets_not_found_raises(): with patch("scribe.services.snippets.merge_snippets", AsyncMock(return_value=None)): from scribe.mcp.tools.snippets import merge_snippets with pytest.raises(ValueError): await merge_snippets(target_id=1, source_ids=[2]) def test_register_attaches_all_tools(): from scribe.mcp.tools import snippets names: list[str] = [] class FakeMcp: def tool(self, name): names.append(name) def deco(fn): return fn return deco snippets.register(FakeMcp()) assert set(names) == { "list_snippets", "create_snippet", "get_snippet", "update_snippet", "delete_snippet", "merge_snippets", }