mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Coverage baseline raised from 13.65% to 26% (+12.35%) Gate raised from 12% to 18% Tasks completed: - COV-001: Command system unit tests (100% coverage) - COV-002: API service unit tests batch 1 (user/apikey/model/provider) - COV-003: Provider model manager unit tests - COV-004: Pipeline remaining stage tests (aggregator/cntfilter/longtext/msgtrun) - COV-005: Storage and utils coverage pass - COV-006: Gate ratchet 12%→15% - COV-007: Gate ratchet 15%→18% - COV-008: API service batch 2 (bot/pipeline/webhook/space/maintenance/mcp) - COV-009: Blocked - API controller circular import issue documented - COV-010: Plugin runtime unit tests (+0.08%) - COV-011: RAG and vector unit tests (+0.68%) - COV-012: Core boot and migration unit tests - COV-013: Provider requester logic unit tests (+0.62%) Key additions: - tests/utils/import_isolation.py: sys.modules isolation for circular imports - Provider requester mock tests: proved HTTP-dependent code can be tested locally - Vector filter utilities: 100% coverage on pure functions - API services: fake persistence pattern for unit testing Blocked issue COV-009 documented in langbot-test-plan/1.5/issues/ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
"""Tests for VectorDatabase base class and SearchType enum."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock
|
|
import pytest
|
|
|
|
from langbot.pkg.vector.vdb import SearchType, VectorDatabase
|
|
|
|
|
|
class TestSearchType:
|
|
"""Tests for SearchType enum."""
|
|
|
|
def test_search_type_values(self):
|
|
"""Test SearchType enum values."""
|
|
assert SearchType.VECTOR.value == 'vector'
|
|
assert SearchType.FULL_TEXT.value == 'full_text'
|
|
assert SearchType.HYBRID.value == 'hybrid'
|
|
|
|
def test_search_type_is_string_enum(self):
|
|
"""SearchType is a string enum."""
|
|
assert isinstance(SearchType.VECTOR, str)
|
|
assert SearchType.VECTOR == 'vector'
|
|
|
|
def test_search_type_from_string(self):
|
|
"""Can create SearchType from string."""
|
|
assert SearchType('vector') == SearchType.VECTOR
|
|
assert SearchType('full_text') == SearchType.FULL_TEXT
|
|
assert SearchType('hybrid') == SearchType.HYBRID
|
|
|
|
|
|
class TestVectorDatabaseAbstractMethods:
|
|
"""Tests for VectorDatabase abstract methods."""
|
|
|
|
def test_vector_database_is_abstract(self):
|
|
"""VectorDatabase is abstract and cannot be instantiated directly."""
|
|
with pytest.raises(TypeError):
|
|
VectorDatabase()
|
|
|
|
def test_abstract_methods_required(self):
|
|
"""Subclass must implement all abstract methods."""
|
|
class IncompleteVectorDB(VectorDatabase):
|
|
pass
|
|
|
|
with pytest.raises(TypeError):
|
|
IncompleteVectorDB()
|
|
|
|
def test_supported_search_types_default(self):
|
|
"""Default supported_search_types returns [VECTOR]."""
|
|
class MinimalVectorDB(VectorDatabase):
|
|
async def add_embeddings(self, collection, ids, embeddings_list, metadatas, documents=None):
|
|
pass
|
|
|
|
async def search(self, collection, query_embedding, k=5, search_type='vector', query_text='', filter=None, vector_weight=None):
|
|
pass
|
|
|
|
async def delete_by_file_id(self, collection, file_id):
|
|
pass
|
|
|
|
async def delete_by_filter(self, collection, filter):
|
|
pass
|
|
|
|
async def get_or_create_collection(self, collection):
|
|
pass
|
|
|
|
async def delete_collection(self, collection):
|
|
pass
|
|
|
|
db = MinimalVectorDB()
|
|
assert db.supported_search_types() == [SearchType.VECTOR]
|
|
|
|
def test_list_by_filter_default_implementation(self):
|
|
"""list_by_filter has default implementation returning empty."""
|
|
class MinimalVectorDB(VectorDatabase):
|
|
async def add_embeddings(self, collection, ids, embeddings_list, metadatas, documents=None):
|
|
pass
|
|
|
|
async def search(self, collection, query_embedding, k=5, search_type='vector', query_text='', filter=None, vector_weight=None):
|
|
pass
|
|
|
|
async def delete_by_file_id(self, collection, file_id):
|
|
pass
|
|
|
|
async def delete_by_filter(self, collection, filter):
|
|
pass
|
|
|
|
async def get_or_create_collection(self, collection):
|
|
pass
|
|
|
|
async def delete_collection(self, collection):
|
|
pass
|
|
|
|
db = MinimalVectorDB()
|
|
# list_by_filter should return empty list and -1 for total
|
|
import asyncio
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
db.list_by_filter('test_collection')
|
|
)
|
|
assert result == ([], -1)
|
|
|
|
|
|
class TestVectorDatabaseInterface:
|
|
"""Tests for VectorDatabase interface contracts."""
|
|
|
|
@pytest.fixture
|
|
def mock_vector_db(self):
|
|
"""Create a minimal mock VectorDatabase for testing."""
|
|
class MockVectorDB(VectorDatabase):
|
|
def __init__(self):
|
|
self.add_embeddings = AsyncMock()
|
|
self.search = AsyncMock(return_value={
|
|
'ids': [['id1', 'id2']],
|
|
'distances': [[0.1, 0.2]],
|
|
'metadatas': [[{'key': 'val1'}, {'key': 'val2'}]]
|
|
})
|
|
self.delete_by_file_id = AsyncMock()
|
|
self.delete_by_filter = AsyncMock(return_value=5)
|
|
self.get_or_create_collection = AsyncMock()
|
|
self.delete_collection = AsyncMock()
|
|
|
|
async def add_embeddings(self, collection, ids, embeddings_list, metadatas, documents=None):
|
|
pass
|
|
|
|
async def search(self, collection, query_embedding, k=5, search_type='vector', query_text='', filter=None, vector_weight=None):
|
|
pass
|
|
|
|
async def delete_by_file_id(self, collection, file_id):
|
|
pass
|
|
|
|
async def delete_by_filter(self, collection, filter):
|
|
pass
|
|
|
|
async def get_or_create_collection(self, collection):
|
|
pass
|
|
|
|
async def delete_collection(self, collection):
|
|
pass
|
|
|
|
return MockVectorDB()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_embeddings_signature(self, mock_vector_db):
|
|
"""add_embeddings has expected signature."""
|
|
await mock_vector_db.add_embeddings(
|
|
collection='test',
|
|
ids=['id1', 'id2'],
|
|
embeddings_list=[[0.1, 0.2], [0.3, 0.4]],
|
|
metadatas=[{'a': 1}, {'b': 2}],
|
|
documents=['doc1', 'doc2']
|
|
)
|
|
mock_vector_db.add_embeddings.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_signature(self, mock_vector_db):
|
|
"""search has expected signature with all optional params."""
|
|
import numpy as np
|
|
|
|
await mock_vector_db.search(
|
|
collection='test',
|
|
query_embedding=np.array([0.1, 0.2]),
|
|
k=10,
|
|
search_type='hybrid',
|
|
query_text='search text',
|
|
filter={'file_id': 'abc'},
|
|
vector_weight=0.7
|
|
)
|
|
mock_vector_db.search.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_by_filter_returns_int(self, mock_vector_db):
|
|
"""delete_by_filter returns int count."""
|
|
result = await mock_vector_db.delete_by_filter('test', {'file_id': 'abc'})
|
|
assert isinstance(result, int) |