forked from cardosofelipe/fast-next-template
Implements RAG capabilities with pgvector for semantic search: - Intelligent chunking strategies (code-aware, markdown-aware, text) - Semantic search with vector similarity (HNSW index) - Keyword search with PostgreSQL full-text search - Hybrid search using Reciprocal Rank Fusion (RRF) - Redis caching for embeddings - Collection management (ingest, search, delete, stats) - FastMCP tools: search_knowledge, ingest_content, delete_content, list_collections, get_collection_stats, update_document Testing: - 128 comprehensive tests covering all components - 58% code coverage (database integration tests use mocks) - Passes ruff linting and mypy type checking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
308 lines
9.8 KiB
Python
308 lines
9.8 KiB
Python
"""Tests for exception classes."""
|
|
|
|
|
|
|
|
class TestErrorCode:
|
|
"""Tests for ErrorCode enum."""
|
|
|
|
def test_error_code_values(self):
|
|
"""Test error code values."""
|
|
from exceptions import ErrorCode
|
|
|
|
assert ErrorCode.UNKNOWN_ERROR.value == "KB_UNKNOWN_ERROR"
|
|
assert ErrorCode.DATABASE_CONNECTION_ERROR.value == "KB_DATABASE_CONNECTION_ERROR"
|
|
assert ErrorCode.EMBEDDING_GENERATION_ERROR.value == "KB_EMBEDDING_GENERATION_ERROR"
|
|
assert ErrorCode.CHUNKING_ERROR.value == "KB_CHUNKING_ERROR"
|
|
assert ErrorCode.SEARCH_ERROR.value == "KB_SEARCH_ERROR"
|
|
assert ErrorCode.COLLECTION_NOT_FOUND.value == "KB_COLLECTION_NOT_FOUND"
|
|
assert ErrorCode.DOCUMENT_NOT_FOUND.value == "KB_DOCUMENT_NOT_FOUND"
|
|
|
|
|
|
class TestKnowledgeBaseError:
|
|
"""Tests for base exception class."""
|
|
|
|
def test_basic_error(self):
|
|
"""Test basic error creation."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
error = KnowledgeBaseError(
|
|
message="Something went wrong",
|
|
code=ErrorCode.UNKNOWN_ERROR,
|
|
)
|
|
|
|
assert error.message == "Something went wrong"
|
|
assert error.code == ErrorCode.UNKNOWN_ERROR
|
|
assert error.details == {}
|
|
assert error.cause is None
|
|
|
|
def test_error_with_details(self):
|
|
"""Test error with details."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
error = KnowledgeBaseError(
|
|
message="Query failed",
|
|
code=ErrorCode.DATABASE_QUERY_ERROR,
|
|
details={"query": "SELECT * FROM table", "error_code": 42},
|
|
)
|
|
|
|
assert error.details["query"] == "SELECT * FROM table"
|
|
assert error.details["error_code"] == 42
|
|
|
|
def test_error_with_cause(self):
|
|
"""Test error with underlying cause."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
original = ValueError("Original error")
|
|
error = KnowledgeBaseError(
|
|
message="Wrapped error",
|
|
code=ErrorCode.INTERNAL_ERROR,
|
|
cause=original,
|
|
)
|
|
|
|
assert error.cause is original
|
|
assert isinstance(error.cause, ValueError)
|
|
|
|
def test_to_dict(self):
|
|
"""Test to_dict method."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
error = KnowledgeBaseError(
|
|
message="Test error",
|
|
code=ErrorCode.INVALID_REQUEST,
|
|
details={"field": "value"},
|
|
)
|
|
|
|
result = error.to_dict()
|
|
|
|
assert result["error"] == "KB_INVALID_REQUEST"
|
|
assert result["message"] == "Test error"
|
|
assert result["details"]["field"] == "value"
|
|
|
|
def test_str_representation(self):
|
|
"""Test string representation."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
error = KnowledgeBaseError(
|
|
message="Test error",
|
|
code=ErrorCode.INVALID_REQUEST,
|
|
)
|
|
|
|
assert str(error) == "[KB_INVALID_REQUEST] Test error"
|
|
|
|
def test_repr_representation(self):
|
|
"""Test repr representation."""
|
|
from exceptions import ErrorCode, KnowledgeBaseError
|
|
|
|
error = KnowledgeBaseError(
|
|
message="Test error",
|
|
code=ErrorCode.INVALID_REQUEST,
|
|
details={"key": "value"},
|
|
)
|
|
|
|
repr_str = repr(error)
|
|
assert "KnowledgeBaseError" in repr_str
|
|
assert "Test error" in repr_str
|
|
assert "KB_INVALID_REQUEST" in repr_str
|
|
|
|
|
|
class TestDatabaseErrors:
|
|
"""Tests for database-related exceptions."""
|
|
|
|
def test_database_connection_error(self):
|
|
"""Test database connection error."""
|
|
from exceptions import DatabaseConnectionError, ErrorCode
|
|
|
|
error = DatabaseConnectionError(
|
|
message="Cannot connect to database",
|
|
details={"host": "localhost", "port": 5432},
|
|
)
|
|
|
|
assert error.code == ErrorCode.DATABASE_CONNECTION_ERROR
|
|
assert error.details["host"] == "localhost"
|
|
|
|
def test_database_connection_error_default_message(self):
|
|
"""Test database connection error with default message."""
|
|
from exceptions import DatabaseConnectionError
|
|
|
|
error = DatabaseConnectionError()
|
|
|
|
assert error.message == "Failed to connect to database"
|
|
|
|
def test_database_query_error(self):
|
|
"""Test database query error."""
|
|
from exceptions import DatabaseQueryError, ErrorCode
|
|
|
|
error = DatabaseQueryError(
|
|
message="Query failed",
|
|
query="SELECT * FROM missing_table",
|
|
)
|
|
|
|
assert error.code == ErrorCode.DATABASE_QUERY_ERROR
|
|
assert error.details["query"] == "SELECT * FROM missing_table"
|
|
|
|
|
|
class TestEmbeddingErrors:
|
|
"""Tests for embedding-related exceptions."""
|
|
|
|
def test_embedding_generation_error(self):
|
|
"""Test embedding generation error."""
|
|
from exceptions import EmbeddingGenerationError, ErrorCode
|
|
|
|
error = EmbeddingGenerationError(
|
|
message="Failed to generate",
|
|
texts_count=10,
|
|
)
|
|
|
|
assert error.code == ErrorCode.EMBEDDING_GENERATION_ERROR
|
|
assert error.details["texts_count"] == 10
|
|
|
|
def test_embedding_dimension_mismatch(self):
|
|
"""Test embedding dimension mismatch error."""
|
|
from exceptions import EmbeddingDimensionMismatchError, ErrorCode
|
|
|
|
error = EmbeddingDimensionMismatchError(
|
|
expected=1536,
|
|
actual=768,
|
|
)
|
|
|
|
assert error.code == ErrorCode.EMBEDDING_DIMENSION_MISMATCH
|
|
assert "expected 1536" in error.message
|
|
assert "got 768" in error.message
|
|
assert error.details["expected_dimension"] == 1536
|
|
assert error.details["actual_dimension"] == 768
|
|
|
|
|
|
class TestChunkingErrors:
|
|
"""Tests for chunking-related exceptions."""
|
|
|
|
def test_unsupported_file_type_error(self):
|
|
"""Test unsupported file type error."""
|
|
from exceptions import ErrorCode, UnsupportedFileTypeError
|
|
|
|
error = UnsupportedFileTypeError(
|
|
file_type=".xyz",
|
|
supported_types=[".py", ".js", ".md"],
|
|
)
|
|
|
|
assert error.code == ErrorCode.UNSUPPORTED_FILE_TYPE
|
|
assert error.details["file_type"] == ".xyz"
|
|
assert len(error.details["supported_types"]) == 3
|
|
|
|
def test_file_too_large_error(self):
|
|
"""Test file too large error."""
|
|
from exceptions import ErrorCode, FileTooLargeError
|
|
|
|
error = FileTooLargeError(
|
|
file_size=10_000_000,
|
|
max_size=1_000_000,
|
|
)
|
|
|
|
assert error.code == ErrorCode.FILE_TOO_LARGE
|
|
assert error.details["file_size"] == 10_000_000
|
|
assert error.details["max_size"] == 1_000_000
|
|
|
|
def test_encoding_error(self):
|
|
"""Test encoding error."""
|
|
from exceptions import EncodingError, ErrorCode
|
|
|
|
error = EncodingError(
|
|
message="Cannot decode file",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert error.code == ErrorCode.ENCODING_ERROR
|
|
assert error.details["encoding"] == "utf-8"
|
|
|
|
|
|
class TestSearchErrors:
|
|
"""Tests for search-related exceptions."""
|
|
|
|
def test_invalid_search_type_error(self):
|
|
"""Test invalid search type error."""
|
|
from exceptions import ErrorCode, InvalidSearchTypeError
|
|
|
|
error = InvalidSearchTypeError(
|
|
search_type="invalid",
|
|
valid_types=["semantic", "keyword", "hybrid"],
|
|
)
|
|
|
|
assert error.code == ErrorCode.INVALID_SEARCH_TYPE
|
|
assert error.details["search_type"] == "invalid"
|
|
assert len(error.details["valid_types"]) == 3
|
|
|
|
def test_search_timeout_error(self):
|
|
"""Test search timeout error."""
|
|
from exceptions import ErrorCode, SearchTimeoutError
|
|
|
|
error = SearchTimeoutError(timeout=30.0)
|
|
|
|
assert error.code == ErrorCode.SEARCH_TIMEOUT
|
|
assert error.details["timeout"] == 30.0
|
|
assert "30" in error.message
|
|
|
|
|
|
class TestCollectionErrors:
|
|
"""Tests for collection-related exceptions."""
|
|
|
|
def test_collection_not_found_error(self):
|
|
"""Test collection not found error."""
|
|
from exceptions import CollectionNotFoundError, ErrorCode
|
|
|
|
error = CollectionNotFoundError(
|
|
collection="missing-collection",
|
|
project_id="proj-123",
|
|
)
|
|
|
|
assert error.code == ErrorCode.COLLECTION_NOT_FOUND
|
|
assert error.details["collection"] == "missing-collection"
|
|
assert error.details["project_id"] == "proj-123"
|
|
|
|
|
|
class TestDocumentErrors:
|
|
"""Tests for document-related exceptions."""
|
|
|
|
def test_document_not_found_error(self):
|
|
"""Test document not found error."""
|
|
from exceptions import DocumentNotFoundError, ErrorCode
|
|
|
|
error = DocumentNotFoundError(
|
|
source_path="/path/to/file.py",
|
|
project_id="proj-123",
|
|
)
|
|
|
|
assert error.code == ErrorCode.DOCUMENT_NOT_FOUND
|
|
assert error.details["source_path"] == "/path/to/file.py"
|
|
|
|
def test_invalid_document_error(self):
|
|
"""Test invalid document error."""
|
|
from exceptions import ErrorCode, InvalidDocumentError
|
|
|
|
error = InvalidDocumentError(
|
|
message="Empty content",
|
|
details={"reason": "no content"},
|
|
)
|
|
|
|
assert error.code == ErrorCode.INVALID_DOCUMENT
|
|
|
|
|
|
class TestProjectErrors:
|
|
"""Tests for project-related exceptions."""
|
|
|
|
def test_project_not_found_error(self):
|
|
"""Test project not found error."""
|
|
from exceptions import ErrorCode, ProjectNotFoundError
|
|
|
|
error = ProjectNotFoundError(project_id="missing-proj")
|
|
|
|
assert error.code == ErrorCode.PROJECT_NOT_FOUND
|
|
assert error.details["project_id"] == "missing-proj"
|
|
|
|
def test_project_access_denied_error(self):
|
|
"""Test project access denied error."""
|
|
from exceptions import ErrorCode, ProjectAccessDeniedError
|
|
|
|
error = ProjectAccessDeniedError(project_id="restricted-proj")
|
|
|
|
assert error.code == ErrorCode.PROJECT_ACCESS_DENIED
|
|
assert "restricted-proj" in error.message
|