""" Integration tests for search_notes MCP tool. Comprehensive tests covering search functionality using the complete MCP client-server flow with real databases. """ import pytest from fastmcp import Client @pytest.mark.asyncio async def test_search_basic_text_search(mcp_server, app, test_project): """Test basic text search functionality.""" async with Client(mcp_server) as client: # Create test notes for searching await client.call_tool( "write_note", { "project": test_project.name, "title": "Python Programming Guide", "directory": "docs", "content": "# Python Programming Guide\n\nThis guide covers Python basics and advanced topics.", "tags": "python,programming", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Flask Web Development", "directory": "docs", "content": "# Flask Web Development\n\nBuilding web applications with Python Flask framework.", "tags": "python,flask,web", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "JavaScript Basics", "directory": "docs", "content": "# JavaScript Basics\n\nIntroduction to JavaScript programming language.", "tags": "javascript,programming", }, ) # Search for Python-related content search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "Python", }, ) assert len(search_result.content) == 1 assert search_result.content[0].type == "text" # Parse the response (it should be a SearchResponse) result_text = search_result.content[0].text assert "Python Programming Guide" in result_text assert "Flask Web Development" in result_text # JavaScript note may appear due to shared "programming" tag — just verify Python notes rank first @pytest.mark.asyncio async def test_search_boolean_operators(mcp_server, app, test_project): """Test boolean search operators (AND, OR, NOT).""" async with Client(mcp_server) as client: # Create test notes await client.call_tool( "write_note", { "project": test_project.name, "title": "Python Flask Tutorial", "directory": "tutorials", "content": "# Python Flask Tutorial\n\nLearn Python web development with Flask.", "tags": "python,flask,tutorial", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Python Django Guide", "directory": "tutorials", "content": "# Python Django Guide\n\nBuilding web apps with Python Django framework.", "tags": "python,django,web", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "React JavaScript", "directory": "tutorials", "content": "# React JavaScript\n\nBuilding frontend applications with React.", "tags": "javascript,react,frontend", }, ) # Test AND operator search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "Python AND Flask", }, ) result_text = search_result.content[0].text assert "Python Flask Tutorial" in result_text # FTS may match broadly on shared terms — verify target note is present # Test OR operator search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "Flask OR Django", }, ) result_text = search_result.content[0].text assert "Python Flask Tutorial" in result_text assert "Python Django Guide" in result_text # Test NOT operator search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "Python NOT Django", }, ) result_text = search_result.content[0].text assert "Python Flask Tutorial" in result_text @pytest.mark.asyncio async def test_search_title_only(mcp_server, app, test_project): """Test searching in titles only.""" async with Client(mcp_server) as client: # Create test notes await client.call_tool( "write_note", { "project": test_project.name, "title": "Database Design", "directory": "docs", "content": "# Database Design\n\nThis covers SQL and database concepts.", "tags": "database,sql", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Web Development", "directory": "docs", "content": "# Web Development\n\nDatabase integration in web applications.", "tags": "web,development", }, ) # Search for "database" in titles only search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "Database", "search_type": "title", }, ) result_text = search_result.content[0].text assert "Database Design" in result_text assert "Web Development" not in result_text # Has "database" in content but not title @pytest.mark.asyncio async def test_search_permalink_exact(mcp_server, app, test_project): """Test exact permalink search.""" async with Client(mcp_server) as client: # Create test notes await client.call_tool( "write_note", { "project": test_project.name, "title": "API Documentation", "directory": "api", "content": "# API Documentation\n\nComplete API reference guide.", "tags": "api,docs", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "API Testing", "directory": "testing", "content": "# API Testing\n\nHow to test REST APIs.", "tags": "api,testing", }, ) # Search for exact permalink search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": f"{test_project.name}/api/api-documentation", "search_type": "permalink", }, ) result_text = search_result.content[0].text assert "API Documentation" in result_text assert "API Testing" not in result_text @pytest.mark.asyncio async def test_search_permalink_pattern(mcp_server, app, test_project): """Test permalink pattern search with wildcards.""" async with Client(mcp_server) as client: # Create test notes in different folders await client.call_tool( "write_note", { "project": test_project.name, "title": "Meeting Notes January", "directory": "meetings", "content": "# Meeting Notes January\n\nJanuary team meeting notes.", "tags": "meetings,january", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Meeting Notes February", "directory": "meetings", "content": "# Meeting Notes February\n\nFebruary team meeting notes.", "tags": "meetings,february", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Project Notes", "directory": "projects", "content": "# Project Notes\n\nGeneral project documentation.", "tags": "projects,notes", }, ) # Search for all meeting notes using pattern search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": f"{test_project.name}/meetings/*", "search_type": "permalink", }, ) result_text = search_result.content[0].text assert "Meeting Notes January" in result_text assert "Meeting Notes February" in result_text assert "Project Notes" not in result_text @pytest.mark.asyncio async def test_search_entity_type_filter(mcp_server, app, test_project): """Test filtering search results by entity type.""" async with Client(mcp_server) as client: # Create a note with observations and relations content_with_observations = """# Development Process This describes our development workflow. ## Observations - [process] We use Git for version control - [tool] We use VS Code as our editor ## Relations - uses [[Git]] - part_of [[Development Workflow]] Regular content about development practices.""" await client.call_tool( "write_note", { "project": test_project.name, "title": "Development Process", "directory": "processes", "content": content_with_observations, "tags": "development,process", }, ) # Search for "development" in entities only search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "development", "entity_types": ["entity"], }, ) result_text = search_result.content[0].text # Should find the main entity but filter out observations/relations assert "Development Process" in result_text @pytest.mark.asyncio async def test_search_pagination(mcp_server, app, test_project): """Test search result pagination.""" async with Client(mcp_server) as client: # Create multiple notes to test pagination for i in range(15): await client.call_tool( "write_note", { "project": test_project.name, "title": f"Test Note {i + 1:02d}", "directory": "test", "content": f"# Test Note {i + 1:02d}\n\nThis is test content for pagination testing.", "tags": "test,pagination", }, ) # Search with pagination (page 1, page_size 5) search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "test", "page": 1, "page_size": 5, }, ) result_text = search_result.content[0].text # Text format includes pagination info in footer assert "page 1" in result_text assert "page_size 5" in result_text # Search page 2 search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "test", "page": 2, "page_size": 5, }, ) result_text = search_result.content[0].text assert "page 2" in result_text @pytest.mark.asyncio async def test_search_no_results(mcp_server, app, test_project): """Test search with no matching results.""" async with Client(mcp_server) as client: # Create a test note await client.call_tool( "write_note", { "project": test_project.name, "title": "Sample Note", "directory": "test", "content": "# Sample Note\n\nThis is a sample note for testing.", "tags": "sample,test", }, ) # Search for something that doesn't exist — use a unique nonsense string search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "xyzzy99nonexistent", }, ) # Default text format returns "No results found" when empty result_text = search_result.content[0].text assert "No results found" in result_text @pytest.mark.asyncio async def test_search_complex_boolean_query(mcp_server, app, test_project): """Test complex boolean queries with grouping.""" async with Client(mcp_server) as client: # Create test notes await client.call_tool( "write_note", { "project": test_project.name, "title": "Python Web Development", "directory": "tutorials", "content": "# Python Web Development\n\nLearn Python for web development using Flask and Django.", "tags": "python,web,development", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "Python Data Science", "directory": "tutorials", "content": "# Python Data Science\n\nData analysis and machine learning with Python.", "tags": "python,data,science", }, ) await client.call_tool( "write_note", { "project": test_project.name, "title": "JavaScript Web Development", "directory": "tutorials", "content": "# JavaScript Web Development\n\nBuilding web applications with JavaScript and React.", "tags": "javascript,web,development", }, ) # Complex boolean query: (Python OR JavaScript) AND web search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": "(Python OR JavaScript) AND web", }, ) result_text = search_result.content[0].text assert "Python Web Development" in result_text assert "JavaScript Web Development" in result_text # "Python Data Science" may appear due to broad FTS matching on "Python" @pytest.mark.asyncio async def test_search_case_insensitive(mcp_server, app, test_project): """Test that search is case insensitive.""" async with Client(mcp_server) as client: # Create test note await client.call_tool( "write_note", { "project": test_project.name, "title": "Machine Learning Guide", "directory": "guides", "content": "# Machine Learning Guide\n\nIntroduction to MACHINE LEARNING concepts.", "tags": "ML,AI", }, ) # Search with different cases search_cases = ["machine", "MACHINE", "Machine", "learning", "LEARNING"] for search_term in search_cases: search_result = await client.call_tool( "search_notes", { "project": test_project.name, "query": search_term, }, ) result_text = search_result.content[0].text assert "Machine Learning Guide" in result_text, f"Failed for search term: {search_term}"