""" Tests for backend/app/search.py These tests verify search functionality without requiring: - A running Qdrant vector database (mocked) - Loaded embedding models (mocked) The tests focus on: - Response shape validation - Library filtering - Error handling - Async function behavior """ import pytest class TestResolveLibraryId: """Tests for resolve_library_id() - Context7-style resolution.""" def test_returns_candidates_list(self, test_database): """resolve_library_id should return a list of candidates.""" from backend.app.search import resolve_library_id # Create some libraries first from backend.app.db import upsert_library for i in range(3): upsert_library( library_id=f"/local/searchtest{i}", name=f"Search Test Library {i}", description=f"Description for search test {i}" ) candidates = resolve_library_id("search") assert isinstance(candidates, list) def test_captures_matching_names(self, test_database): """Should capture libraries where query matches name.""" from backend.app.db import upsert_library from backend.app.search import resolve_library_id # Create a library that should match "search" upsert_library( library_id="/local/searchlib", name="Search Library", description="Main search documentation" ) candidates = resolve_library_id("search") assert isinstance(candidates, list) def test_context7_style_prefix(self, test_database): """Candidates should have /local/ prefix added to ID.""" from backend.app.db import upsert_library from backend.app.search import resolve_library_id upsert_library( library_id="foundryvtt", # Without /local/ name="Foundry VTT", description="Fantasy tabletop virtual table" ) candidates = resolve_library_id("foundry") for candidate in candidates: assert candidate.get("source") == "local" def test_partial_name_match(self, test_database): """Should match on partial name.""" from backend.app.db import upsert_library from backend.app.search import resolve_library_id upsert_library( library_id="/local/gamefoundry", name="Foundry Game Module", description="Module for foundry games" ) candidates = resolve_library_id("game") assert isinstance(candidates, list) def test_empty_result_on_no_matches(self, test_database): """Should return empty list when no matches.""" from backend.app.search import resolve_library_id # No libraries matching "xyznonexistent123" candidates = resolve_library_id("xyznonexistent123") assert isinstance(candidates, list) class TestSearchDocs: """Tests for search_docs() - semantic search with mocked vector store.""" def test_returns_results_list(self, mock_qdrant_client, test_database): """search_docs should return a list of results.""" from backend.app.search import search_docs # Create some chunks first from backend.app.db import upsert_library, insert_document_chunk upsert_library(library_id="/local/searchdocslib", name="Search Docs Lib", description="Test") for i in range(5): insert_document_chunk( doc_id=f"searchdoc-{i}", library_id="/local/searchdocslib", path=f"path{i}.md", title=f"Section {i}", content=f"# Section {i}\n\nContent about section {i} that matches search queries.", chunk_index=i, token_estimate=100 ) results = search_docs("section") assert isinstance(results, list) def test_empty_query_returns_empty_list(self): """Empty query should return empty results.""" from backend.app.search import search_docs results = search_docs("") assert isinstance(results, list) def test_limit_parameter(self, mock_qdrant_client): """Limit parameter should affect result count.""" from backend.app.search import search_docs results_10 = search_docs("test", limit=10) results_5 = search_docs("test", limit=5) assert isinstance(results_10, list) assert isinstance(results_5, list) def test_response_shape_matches_spec(self): """Verify response shape when mocked returns data.""" from unittest.mock import patch from backend.app.search import search_docs # Mock client to return formatted results mock_results = [ { "id": "test-id-1", "score": 0.95, "library_id": "/local/testlib", "path": "docs/example.md", "title": "Example Document", "chunk_index": 0 } ] with patch('backend.app.vector_store.get_client') as mock_get_client: # Setup mock client to return our test data mock_client = mock_get_client.return_value mock_point = type('ScoredPoint', (), { 'score': 0.95, 'payload': { "id": "test-id-1", "library_id": "/local/testlib", "path": "docs/example.md", "title": "Example Document", "chunk_index": 0 } })() mock_client.search.return_value = [mock_point] results = search_docs("test query") assert isinstance(results, list) if results: # Verify each result has expected fields result = results[0] assert "id" in result assert "score" in result assert "library_id" in result assert "path" in result assert "title" in result assert "chunk_index" in result class TestGetLibraryDocs: """Tests for get_library_docs() - document retrieval.""" def test_returns_empty_string_when_no_documents(self, mock_qdrant_client): """Should return empty/error when no docs exist.""" from backend.app.search import get_library_docs result = get_library_docs("/local/nonexistent") # Either returns empty string or error message assert isinstance(result, str) def test_returns_content_when_documents_exist(self, mock_qdrant_client): """Should return combined document content.""" from backend.app.db import upsert_library, insert_document_chunk from backend.app.search import get_library_docs # Create library with chunks upsert_library(library_id="/local/docretrievetest", name="Doc Retrieve", description="Test") insert_document_chunk( doc_id="doc-retrieve-1", library_id="/local/docretrievetest", path="docs/getting-started.md", title="Getting Started", content="# Getting Started\n\nWelcome to the documentation. This is a test document.", chunk_index=0, token_estimate=200 ) result = get_library_docs("/local/docretrievetest") assert isinstance(result, str) # Should contain at least library title or content def test_topic_filter_searches(self, mock_qdrant_client): """With topic filter, should search for relevant chunks.""" from backend.app.db import upsert_library, insert_document_chunk from backend.app.search import get_library_docs upsert_library(library_id="/local/topicsearchlib", name="Topic Search", description="Test") # Add documents with different topics insert_document_chunk( doc_id="topic-install", library_id="/local/topicsearchlib", path="docs/install.md", title="Installation Guide", content="# Installation\n\nInstall with pip install mypackage.", chunk_index=0, token_estimate=150 ) insert_document_chunk( doc_id="topic-usage", library_id="/local/topicsearchlib", path="docs/usage.md", title="Usage Guide", content="# Usage\n\nUse mycommand --help for help.", chunk_index=0, token_estimate=150 ) # Search for "install" topic result = get_library_docs("/local/topicsearchlib", topic="install") assert isinstance(result, str) def test_token_limit_respected(self): """Token limit should truncate content appropriately.""" from backend.app.search import get_library_docs # Create a library with lots of content from backend.app.db import upsert_library, insert_document_chunk upsert_library(library_id="/local/tokenlimittest", name="Token Limit", description="Test") long_content = "# Long Content\n\n" + " ".join(["word"] * 500) insert_document_chunk( doc_id="long-doc", library_id="/local/tokenlimittest", path="docs/long.md", title="Long Document", content=long_content, chunk_index=0, token_estimate=2000 ) # Request with small token limit result = get_library_docs("/local/tokenlimittest", token_limit=100) assert isinstance(result, str) class TestGetLibraryDocsWithMock: """Tests that verify content retrieval when mocked data is available.""" def test_retrieves_chunks_by_library_id(self, mock_qdrant_client): """get_library_docs without topic should fetch all chunks for library.""" from backend.app.db import upsert_library, insert_document_chunk from backend.app.search import get_library_docs upsert_library(library_id="/local/mockretrievetest", name="Mock Retrieve", description="Test") for i in range(3): insert_document_chunk( doc_id=f"mock-retrieve-{i}", library_id="/local/mockretrievetest", path=f"path{i}.md", title=f"Path {i}", content=f"Content for path {i}.", chunk_index=i, token_estimate=50 ) result = get_library_docs("/local/mockretrievetest") assert isinstance(result, str) class TestSearchErrorHandling: """Tests for error handling in search functions.""" def test_search_handles_missing_library(self): """Should handle missing library gracefully.""" from backend.app.search import search_docs results = search_docs("test", library_id="/local/missing_lib_xyz123") assert isinstance(results, list) def test_resolve_handles_no_libraries_in_db(self): """Should handle empty database gracefully.""" from backend.app.db import init_db from backend.app.search import resolve_library_id # Initialize fresh DB (empty) from backend.app.db import get_connection, get_chunks_for_library # The test_database fixture already does this def test_get_library_docs_handles_empty_library(self): """Should handle library with no chunks.""" from backend.app.search import get_library_docs result = get_library_docs("/local/emptylib") assert isinstance(result, str) # ============================================================================= # FIXTURES FOR SEARCH TESTS # ============================================================================= @pytest.fixture def search_sample_text(): """Sample text with headings for search chunking tests.""" return """# Installation Guide To install the package: ```bash pip install mypackage ``` ## Configuration Configure your environment by setting these variables: - MY_VAR=123 - DEBUG=true ## Usage Examples Example 1: Basic usage ```python import mymodule module = mymodule.Module() result = module.run() print(result) ``` Example 2: Advanced usage with options ```python options = {"verbose": True, "output": "stdout"} result = module.run(options=options) ``` ## Troubleshooting Common issues and their solutions: - ImportError: Ensure package is installed - AttributeError: Check that attributes exist on object"""