feat(mcp): fable_list_tags + fable_get_recent
Two cross-type bootstrap tools: - fable_list_tags: tag vocabulary with usage counts, top-N by count. Aggregation in Python (not SQL UNNEST) — trivial perf cost at personal scale, much easier to test. - fable_get_recent: most-recently-touched items across notes, tasks, projects, events. Useful for Claude to ask 'what was I working on recently' at the start of a conversation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Tests for fable_list_tags.
|
||||
|
||||
The aggregation helper is unit-tested directly; the full tool is exercised
|
||||
with a mocked async_session to keep tests DB-free.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.tags import fable_list_tags, _aggregate_tag_counts
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _bind_user():
|
||||
token = _user_id_ctx.set(7)
|
||||
yield
|
||||
_user_id_ctx.reset(token)
|
||||
|
||||
|
||||
def test_aggregate_tag_counts_basic():
|
||||
assert _aggregate_tag_counts([["ops", "kafka"], ["ops"]]) == {"ops": 2, "kafka": 1}
|
||||
|
||||
|
||||
def test_aggregate_tag_counts_handles_none_rows():
|
||||
"""A note with NULL tags must not blow up the aggregator."""
|
||||
assert _aggregate_tag_counts([["a"], None, ["a", "b"]]) == {"a": 2, "b": 1}
|
||||
|
||||
|
||||
def test_aggregate_tag_counts_empty_input():
|
||||
assert _aggregate_tag_counts([]) == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fable_list_tags_returns_sorted_by_count_desc():
|
||||
"""End-to-end: query returns three rows, top tag wins."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.all.return_value = [(["a"],), (["a", "b"],), (["a"],)]
|
||||
mock_session = AsyncMock()
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx):
|
||||
out = await fable_list_tags()
|
||||
assert out["tags"][0] == {"tag": "a", "count": 3}
|
||||
assert out["tags"][1] == {"tag": "b", "count": 1}
|
||||
assert out["total"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fable_list_tags_clamps_limit():
|
||||
mock_result = MagicMock()
|
||||
mock_result.all.return_value = []
|
||||
mock_session = AsyncMock()
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx):
|
||||
# No exception — just exercises the clamping branch
|
||||
out = await fable_list_tags(limit=99999)
|
||||
assert out["total"] == 0
|
||||
Reference in New Issue
Block a user