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>
296 lines
9.7 KiB
Python
296 lines
9.7 KiB
Python
"""
|
|
Test fixtures for provider/modelmgr tests.
|
|
|
|
Provides fake persistence, mock requester registry, and test utilities
|
|
without calling real LLM APIs or network requests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, Mock
|
|
from types import SimpleNamespace
|
|
|
|
from langbot.pkg.provider.modelmgr import requester
|
|
from langbot.pkg.provider.modelmgr import token
|
|
from langbot.pkg.provider.modelmgr.modelmgr import ModelManager
|
|
from langbot.pkg.entity.persistence import model as persistence_model
|
|
from langbot.pkg.discover import engine as discover_engine
|
|
|
|
|
|
class FakeProviderAPIRequester(requester.ProviderAPIRequester):
|
|
"""Fake requester for testing that does not make real API calls."""
|
|
|
|
name = 'fake-requester'
|
|
|
|
default_config = {'base_url': 'https://fake-api.example.com', 'timeout': 30}
|
|
|
|
def __init__(self, ap, config: dict):
|
|
super().__init__(ap, config)
|
|
self._invoke_count = 0
|
|
self._last_messages = None
|
|
self._last_model = None
|
|
|
|
async def invoke_llm(
|
|
self,
|
|
query,
|
|
model: requester.RuntimeLLMModel,
|
|
messages: list,
|
|
funcs=None,
|
|
extra_args={},
|
|
remove_think=False,
|
|
):
|
|
"""Return a fake message response."""
|
|
self._invoke_count += 1
|
|
self._last_messages = messages
|
|
self._last_model = model
|
|
|
|
# Import the message entity for response
|
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
|
|
return provider_message.Message(
|
|
role='assistant',
|
|
content=[provider_message.ContentElement(type='text', text='Fake LLM response')],
|
|
)
|
|
|
|
async def invoke_llm_stream(
|
|
self,
|
|
query,
|
|
model: requester.RuntimeLLMModel,
|
|
messages: list,
|
|
funcs=None,
|
|
extra_args={},
|
|
remove_think=False,
|
|
):
|
|
"""Yield fake message chunks."""
|
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
|
|
yield provider_message.MessageChunk(
|
|
role='assistant',
|
|
content=[provider_message.ContentElement(type='text', text='Fake stream chunk')],
|
|
)
|
|
|
|
async def invoke_embedding(self, model, input_text: list, extra_args={}):
|
|
"""Return fake embedding vectors."""
|
|
return [[0.1, 0.2, 0.3] for _ in input_text]
|
|
|
|
async def invoke_rerank(self, model, query: str, documents: list, extra_args={}):
|
|
"""Return fake rerank results."""
|
|
return [{'index': i, 'relevance_score': 0.9 - i * 0.1} for i in range(len(documents))]
|
|
|
|
|
|
class AnotherFakeRequester(requester.ProviderAPIRequester):
|
|
"""Another fake requester for multi-requester tests."""
|
|
|
|
name = 'another-fake-requester'
|
|
|
|
default_config = {'base_url': 'https://another-fake.example.com'}
|
|
|
|
async def invoke_llm(self, query, model, messages, funcs=None, extra_args={}, remove_think=False):
|
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
return provider_message.Message(role='assistant', content=[provider_message.ContentElement(type='text', text='Another response')])
|
|
|
|
async def invoke_rerank(self, model, query: str, documents: list, extra_args={}):
|
|
"""Return fake rerank results."""
|
|
return [{'index': i, 'relevance_score': 0.9 - i * 0.1} for i in range(len(documents))]
|
|
|
|
|
|
def _create_fake_component(name: str, requester_class: type) -> Mock:
|
|
"""Create a fake Component mock for a requester."""
|
|
# Use Mock to allow overriding get_python_component_class
|
|
component = Mock(spec=discover_engine.Component)
|
|
component.metadata = Mock()
|
|
component.metadata.name = name
|
|
component.get_python_component_class = Mock(return_value=requester_class)
|
|
return component
|
|
|
|
|
|
def _make_mock_result(items: list = None, first_item=None):
|
|
"""Create a mock result object for persistence queries."""
|
|
result = Mock()
|
|
result.all = Mock(return_value=items or [])
|
|
result.first = Mock(return_value=first_item)
|
|
return result
|
|
|
|
|
|
def _make_row_mock(entity):
|
|
"""Create a mock Row-like object that can be unpacked via _mapping.
|
|
|
|
Note: This function returns the actual entity directly since Mock objects
|
|
don't pass isinstance(provider_info, sqlalchemy.Row) checks. The code
|
|
in modelmgr.load_provider handles this via the else branch.
|
|
"""
|
|
return entity
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_app_for_modelmgr():
|
|
"""Provides a mock Application for ModelManager tests."""
|
|
app = SimpleNamespace()
|
|
app.logger = Mock()
|
|
app.logger.debug = Mock()
|
|
app.logger.info = Mock()
|
|
app.logger.warning = Mock()
|
|
app.logger.error = Mock()
|
|
|
|
# Fake persistence manager - returns empty results by default
|
|
app.persistence_mgr = SimpleNamespace()
|
|
async def default_execute(query):
|
|
return _make_mock_result([])
|
|
app.persistence_mgr.execute_async = AsyncMock(side_effect=default_execute)
|
|
|
|
# Fake discover engine
|
|
app.discover = SimpleNamespace()
|
|
app.discover.get_components_by_kind = Mock(return_value=[])
|
|
|
|
# Fake instance config
|
|
app.instance_config = SimpleNamespace()
|
|
app.instance_config.data = {'space': {'disable_models_service': True}}
|
|
|
|
# Other services (not used in basic tests)
|
|
app.space_service = AsyncMock()
|
|
app.llm_model_service = AsyncMock()
|
|
app.embedding_models_service = AsyncMock()
|
|
app.monitoring_service = AsyncMock()
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_requester_registry(mock_app_for_modelmgr):
|
|
"""Provides a ModelManager with fake requester registry."""
|
|
app = mock_app_for_modelmgr
|
|
|
|
# Create fake components
|
|
fake_component = _create_fake_component('fake-requester', FakeProviderAPIRequester)
|
|
another_component = _create_fake_component('another-fake-requester', AnotherFakeRequester)
|
|
|
|
app.discover.get_components_by_kind = Mock(
|
|
return_value=[fake_component, another_component]
|
|
)
|
|
|
|
model_mgr = ModelManager(app)
|
|
return model_mgr
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_persistence_data():
|
|
"""Provides fake persistence data for models and providers."""
|
|
provider_uuid = 'test-provider-uuid'
|
|
provider_uuid2 = 'test-provider-uuid-2'
|
|
|
|
providers = [
|
|
persistence_model.ModelProvider(
|
|
uuid=provider_uuid,
|
|
name='Test Provider',
|
|
requester='fake-requester',
|
|
base_url='https://test.example.com',
|
|
api_keys=['test-api-key-1', 'test-api-key-2'],
|
|
),
|
|
persistence_model.ModelProvider(
|
|
uuid=provider_uuid2,
|
|
name='Test Provider 2',
|
|
requester='another-fake-requester',
|
|
base_url='https://test2.example.com',
|
|
api_keys=['key-3'],
|
|
),
|
|
]
|
|
|
|
llm_models = [
|
|
persistence_model.LLMModel(
|
|
uuid='test-llm-uuid-1',
|
|
name='TestLLM-1',
|
|
provider_uuid=provider_uuid,
|
|
abilities=['func_call'],
|
|
extra_args={'temperature': 0.7},
|
|
),
|
|
persistence_model.LLMModel(
|
|
uuid='test-llm-uuid-2',
|
|
name='TestLLM-2',
|
|
provider_uuid=provider_uuid,
|
|
abilities=['vision'],
|
|
extra_args={},
|
|
),
|
|
]
|
|
|
|
embedding_models = [
|
|
persistence_model.EmbeddingModel(
|
|
uuid='test-embedding-uuid-1',
|
|
name='TestEmbedding-1',
|
|
provider_uuid=provider_uuid,
|
|
extra_args={'dimensions': 768},
|
|
),
|
|
]
|
|
|
|
rerank_models = [
|
|
persistence_model.RerankModel(
|
|
uuid='test-rerank-uuid-1',
|
|
name='TestRerank-1',
|
|
provider_uuid=provider_uuid2,
|
|
extra_args={},
|
|
),
|
|
]
|
|
|
|
return {
|
|
'providers': providers,
|
|
'llm_models': llm_models,
|
|
'embedding_models': embedding_models,
|
|
'rerank_models': rerank_models,
|
|
'provider_uuid': provider_uuid,
|
|
'provider_uuid2': provider_uuid2,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def runtime_provider(fake_persistence_data, mock_app_for_modelmgr):
|
|
"""Provides a RuntimeProvider instance for testing."""
|
|
provider_entity = fake_persistence_data['providers'][0]
|
|
token_mgr = token.TokenManager(name=provider_entity.uuid, tokens=provider_entity.api_keys or [])
|
|
requester_inst = FakeProviderAPIRequester(mock_app_for_modelmgr, {'base_url': provider_entity.base_url})
|
|
|
|
return requester.RuntimeProvider(
|
|
provider_entity=provider_entity,
|
|
token_mgr=token_mgr,
|
|
requester=requester_inst,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def runtime_llm_model(fake_persistence_data, runtime_provider):
|
|
"""Provides a RuntimeLLMModel instance for testing."""
|
|
model_entity = fake_persistence_data['llm_models'][0]
|
|
return requester.RuntimeLLMModel(
|
|
model_entity=model_entity,
|
|
provider=runtime_provider,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def runtime_embedding_model(fake_persistence_data, runtime_provider):
|
|
"""Provides a RuntimeEmbeddingModel instance for testing."""
|
|
model_entity = fake_persistence_data['embedding_models'][0]
|
|
return requester.RuntimeEmbeddingModel(
|
|
model_entity=model_entity,
|
|
provider=runtime_provider,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def runtime_rerank_model(fake_persistence_data, mock_app_for_modelmgr):
|
|
"""Provides a RuntimeRerankModel instance for testing."""
|
|
provider_entity = fake_persistence_data['providers'][1]
|
|
token_mgr = token.TokenManager(name=provider_entity.uuid, tokens=provider_entity.api_keys or [])
|
|
requester_inst = AnotherFakeRequester(mock_app_for_modelmgr, {'base_url': provider_entity.base_url})
|
|
|
|
provider = requester.RuntimeProvider(
|
|
provider_entity=provider_entity,
|
|
token_mgr=token_mgr,
|
|
requester=requester_inst,
|
|
)
|
|
|
|
model_entity = fake_persistence_data['rerank_models'][0]
|
|
return requester.RuntimeRerankModel(
|
|
model_entity=model_entity,
|
|
provider=provider,
|
|
)
|