Files
FabledScribe/tests/test_mcp_tool_snippets.py
T
bvandeusen 1942913366
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 20s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 1m7s
feat(scribe): add snippet recall — note_type='snippet' service + MCP tools
Record reusable functions/components once so they surface via the existing
semantic search + title-first auto-inject, instead of re-solving as one-offs.

A snippet is a Note with note_type='snippet' (no schema change): note_type is
free-text, and semantic_search_notes never filters by type, so snippets join
the recall/auto-inject pool the moment they're embedded. Structured fields
(name/language/signature/location/when_to_use/code) are stored via a body
convention — title = "name — when to use" (what auto-inject surfaces), language
+ "snippet" as tags, templated markdown body — keeping storage swappable later
without changing the tool/UI contract.

- services/snippets.py: compose/parse helpers + create/get/list/update wrappers
  over notes_svc (dedup + System association reused).
- mcp/tools/snippets.py: list_snippets / create_snippet / get_snippet /
  update_snippet, registered in tools/__init__.py.
- unit tests for the serialize/parse round-trip and the MCP tool surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pa2EsuB54BuWQ8GfJq9c7t
2026-07-25 11:00:33 -04:00

101 lines
3.3 KiB
Python

"""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")
def test_register_attaches_four_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",
}