Files
DocsMCP/tests/test_search.py
T
2026-06-06 13:01:52 +01:00

403 lines
14 KiB
Python

"""
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
def test_uses_modern_qdrant_query_points_api(self):
"""New qdrant-client versions expose query_points instead of search."""
from unittest.mock import patch
from backend.app.search import search_docs
point = type("ScoredPoint", (), {
"score": 0.91,
"payload": {
"id": "modern-result",
"library_id": "documentation",
"path": "docs/index.md",
"title": "Index",
"chunk_index": 0,
},
})()
class Response:
points = [point]
class ModernClient:
def query_points(self, **kwargs):
assert kwargs["query"] == [0.1, 0.2]
assert kwargs["limit"] == 3
return Response()
with (
patch("backend.app.search.embed_text", return_value=[0.1, 0.2]),
patch("backend.app.search.get_client", return_value=ModernClient()),
):
results = search_docs("world generation", limit=3)
assert len(results) == 1
assert results[0]["id"] == "modern-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"""