mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
35884ef3a7
FastMCP library changes now require calling decorated functions via the .fn attribute: - Tools: @mcp.tool() functions return FunctionTool, call with tool.fn() - Prompts: @mcp.prompt() functions return FunctionPrompt, call with prompt.fn() - Resources: @mcp.resource() functions return FunctionResource, call with resource.fn() Updated core files: - view_note.py: read_note() → read_note.fn() - read_note.py: search_notes() → search_notes.fn() (2 locations) - tool.py: 6 MCP tool calls updated to use .fn - recent_activity.py: recent_activity() → recent_activity.fn() - project.py: project_info() → project_info.fn() with type ignore Updated 100+ test files systematically to use .fn attribute and fixed mock targets. All 869 tests now pass. Fixes view_note tool error in Claude Desktop. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
249 lines
8.3 KiB
Python
249 lines
8.3 KiB
Python
"""Tests for search MCP tools."""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
from basic_memory.mcp.tools import write_note
|
|
from basic_memory.mcp.tools.search import search_notes, _format_search_error_response
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_text(client):
|
|
"""Test basic search functionality."""
|
|
# Create a test note
|
|
result = await write_note.fn(
|
|
title="Test Search Note",
|
|
folder="test",
|
|
content="# Test\nThis is a searchable test note",
|
|
tags=["test", "search"],
|
|
)
|
|
assert result
|
|
|
|
# Search for it
|
|
response = await search_notes.fn(query="searchable")
|
|
|
|
# Verify results
|
|
assert len(response.results) > 0
|
|
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_title(client):
|
|
"""Test basic search functionality."""
|
|
# Create a test note
|
|
result = await write_note.fn(
|
|
title="Test Search Note",
|
|
folder="test",
|
|
content="# Test\nThis is a searchable test note",
|
|
tags=["test", "search"],
|
|
)
|
|
assert result
|
|
|
|
# Search for it
|
|
response = await search_notes.fn(query="Search Note", search_type="title")
|
|
|
|
# Verify results
|
|
assert len(response.results) > 0
|
|
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_permalink(client):
|
|
"""Test basic search functionality."""
|
|
# Create a test note
|
|
result = await write_note.fn(
|
|
title="Test Search Note",
|
|
folder="test",
|
|
content="# Test\nThis is a searchable test note",
|
|
tags=["test", "search"],
|
|
)
|
|
assert result
|
|
|
|
# Search for it
|
|
response = await search_notes.fn(query="test/test-search-note", search_type="permalink")
|
|
|
|
# Verify results
|
|
assert len(response.results) > 0
|
|
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_permalink_match(client):
|
|
"""Test basic search functionality."""
|
|
# Create a test note
|
|
result = await write_note.fn(
|
|
title="Test Search Note",
|
|
folder="test",
|
|
content="# Test\nThis is a searchable test note",
|
|
tags=["test", "search"],
|
|
)
|
|
assert result
|
|
|
|
# Search for it
|
|
response = await search_notes.fn(query="test/test-search-*", search_type="permalink")
|
|
|
|
# Verify results
|
|
assert len(response.results) > 0
|
|
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_pagination(client):
|
|
"""Test basic search functionality."""
|
|
# Create a test note
|
|
result = await write_note.fn(
|
|
title="Test Search Note",
|
|
folder="test",
|
|
content="# Test\nThis is a searchable test note",
|
|
tags=["test", "search"],
|
|
)
|
|
assert result
|
|
|
|
# Search for it
|
|
response = await search_notes.fn(query="searchable", page=1, page_size=1)
|
|
|
|
# Verify results
|
|
assert len(response.results) == 1
|
|
assert any(r.permalink == "test/test-search-note" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_with_type_filter(client):
|
|
"""Test search with entity type filter."""
|
|
# Create test content
|
|
await write_note.fn(
|
|
title="Entity Type Test",
|
|
folder="test",
|
|
content="# Test\nFiltered by type",
|
|
)
|
|
|
|
# Search with type filter
|
|
response = await search_notes.fn(query="type", types=["note"])
|
|
|
|
# Verify all results are entities
|
|
assert all(r.type == "entity" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_with_entity_type_filter(client):
|
|
"""Test search with entity type filter."""
|
|
# Create test content
|
|
await write_note.fn(
|
|
title="Entity Type Test",
|
|
folder="test",
|
|
content="# Test\nFiltered by type",
|
|
)
|
|
|
|
# Search with entity type filter
|
|
response = await search_notes.fn(query="type", entity_types=["entity"])
|
|
|
|
# Verify all results are entities
|
|
assert all(r.type == "entity" for r in response.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_with_date_filter(client):
|
|
"""Test search with date filter."""
|
|
# Create test content
|
|
await write_note.fn(
|
|
title="Recent Note",
|
|
folder="test",
|
|
content="# Test\nRecent content",
|
|
)
|
|
|
|
# Search with date filter
|
|
one_hour_ago = datetime.now() - timedelta(hours=1)
|
|
response = await search_notes.fn(query="recent", after_date=one_hour_ago.isoformat())
|
|
|
|
# Verify we get results within timeframe
|
|
assert len(response.results) > 0
|
|
|
|
|
|
class TestSearchErrorFormatting:
|
|
"""Test search error formatting for better user experience."""
|
|
|
|
def test_format_search_error_fts5_syntax(self):
|
|
"""Test formatting for FTS5 syntax errors."""
|
|
result = _format_search_error_response("syntax error in FTS5", "test query(")
|
|
|
|
assert "# Search Failed - Invalid Syntax" in result
|
|
assert "The search query 'test query(' contains invalid syntax" in result
|
|
assert "Special characters" in result
|
|
assert "test query" in result # Clean query without special chars
|
|
|
|
def test_format_search_error_no_results(self):
|
|
"""Test formatting for no results found."""
|
|
result = _format_search_error_response("no results found", "very specific query")
|
|
|
|
assert "# Search Complete - No Results Found" in result
|
|
assert "No content found matching 'very specific query'" in result
|
|
assert "Broaden your search" in result
|
|
assert "very" in result # Simplified query
|
|
|
|
def test_format_search_error_server_error(self):
|
|
"""Test formatting for server errors."""
|
|
result = _format_search_error_response("internal server error", "test query")
|
|
|
|
assert "# Search Failed - Server Error" in result
|
|
assert "The search service encountered an error while processing 'test query'" in result
|
|
assert "Try again" in result
|
|
assert "Check project status" in result
|
|
|
|
def test_format_search_error_permission_denied(self):
|
|
"""Test formatting for permission errors."""
|
|
result = _format_search_error_response("permission denied", "test query")
|
|
|
|
assert "# Search Failed - Access Error" in result
|
|
assert "You don't have permission to search" in result
|
|
assert "Check your project access" in result
|
|
|
|
def test_format_search_error_project_not_found(self):
|
|
"""Test formatting for project not found errors."""
|
|
result = _format_search_error_response("current project not found", "test query")
|
|
|
|
assert "# Search Failed - Project Not Found" in result
|
|
assert "The current project is not accessible" in result
|
|
assert "Check available projects" in result
|
|
|
|
def test_format_search_error_generic(self):
|
|
"""Test formatting for generic errors."""
|
|
result = _format_search_error_response("unknown error", "test query")
|
|
|
|
assert "# Search Failed" in result
|
|
assert "Error searching for 'test query': unknown error" in result
|
|
assert "General troubleshooting" in result
|
|
|
|
|
|
class TestSearchToolErrorHandling:
|
|
"""Test search tool exception handling."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_notes_exception_handling(self):
|
|
"""Test exception handling in search_notes."""
|
|
with patch("basic_memory.mcp.tools.search.get_active_project") as mock_get_project:
|
|
mock_get_project.return_value.project_url = "http://test"
|
|
|
|
with patch(
|
|
"basic_memory.mcp.tools.search.call_post", side_effect=Exception("syntax error")
|
|
):
|
|
result = await search_notes.fn("test query")
|
|
|
|
assert isinstance(result, str)
|
|
assert "# Search Failed - Invalid Syntax" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_notes_permission_error(self):
|
|
"""Test search_notes with permission error."""
|
|
with patch("basic_memory.mcp.tools.search.get_active_project") as mock_get_project:
|
|
mock_get_project.return_value.project_url = "http://test"
|
|
|
|
with patch(
|
|
"basic_memory.mcp.tools.search.call_post",
|
|
side_effect=Exception("permission denied"),
|
|
):
|
|
result = await search_notes.fn("test query")
|
|
|
|
assert isinstance(result, str)
|
|
assert "# Search Failed - Access Error" in result
|