mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-03 04:24:36 +00:00
397 lines
14 KiB
Python
397 lines
14 KiB
Python
"""Unit tests for API knowledge service.
|
|
|
|
Tests cover:
|
|
- Knowledge base CRUD operations
|
|
- Capability checking
|
|
- Knowledge engine discovery
|
|
- File operations
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, AsyncMock
|
|
from importlib import import_module
|
|
|
|
|
|
def get_knowledge_service_module():
|
|
"""Lazy import to avoid circular import issues."""
|
|
return import_module('langbot.pkg.api.http.service.knowledge')
|
|
|
|
|
|
def create_mock_app():
|
|
"""Create mock Application for testing."""
|
|
mock_app = Mock()
|
|
mock_app.logger = Mock()
|
|
mock_app.rag_mgr = AsyncMock()
|
|
mock_app.persistence_mgr = AsyncMock()
|
|
mock_app.persistence_mgr.execute_async = AsyncMock()
|
|
mock_app.persistence_mgr.serialize_model = Mock(return_value={})
|
|
mock_app.plugin_connector = AsyncMock()
|
|
mock_app.plugin_connector.is_enable_plugin = True
|
|
return mock_app
|
|
|
|
|
|
class TestKnowledgeServiceInit:
|
|
"""Tests for KnowledgeService initialization."""
|
|
|
|
def test_init_stores_app_reference(self):
|
|
"""Test that __init__ stores Application reference."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
assert service.ap is mock_app
|
|
|
|
|
|
class TestGetKnowledgeBases:
|
|
"""Tests for get_knowledge_bases method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_all_kb_details(self):
|
|
"""Test that it returns all knowledge base details."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_all_knowledge_base_details = AsyncMock(
|
|
return_value=[{'uuid': 'kb1', 'name': 'KB1'}]
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_knowledge_bases()
|
|
|
|
assert len(result) == 1
|
|
assert result[0]['uuid'] == 'kb1'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_list_when_no_kbs(self):
|
|
"""Test that it returns empty list when no knowledge bases."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_all_knowledge_base_details = AsyncMock(return_value=[])
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_knowledge_bases()
|
|
|
|
assert result == []
|
|
|
|
|
|
class TestGetKnowledgeBase:
|
|
"""Tests for get_knowledge_base method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_kb_details_by_uuid(self):
|
|
"""Test that it returns specific KB details."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(
|
|
return_value={'uuid': 'kb1', 'name': 'KB1'}
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_knowledge_base('kb1')
|
|
|
|
assert result['uuid'] == 'kb1'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_none_when_not_found(self):
|
|
"""Test that it returns None when KB not found."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(return_value=None)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_knowledge_base('nonexistent')
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestCreateKnowledgeBase:
|
|
"""Tests for create_knowledge_base method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creates_kb_with_required_fields(self):
|
|
"""Test creating KB with required plugin ID."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_kb = Mock()
|
|
mock_kb.uuid = 'new_kb_uuid'
|
|
mock_app.rag_mgr.create_knowledge_base = AsyncMock(return_value=mock_kb)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
kb_data = {
|
|
'name': 'Test KB',
|
|
'knowledge_engine_plugin_id': 'author/engine',
|
|
'description': 'Test description',
|
|
}
|
|
|
|
result = await service.create_knowledge_base(kb_data)
|
|
|
|
assert result == 'new_kb_uuid'
|
|
mock_app.rag_mgr.create_knowledge_base.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raises_when_missing_plugin_id(self):
|
|
"""Test that ValueError is raised when plugin ID missing."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
await service.create_knowledge_base({'name': 'Test'})
|
|
|
|
assert 'knowledge_engine_plugin_id is required' in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creates_with_default_name(self):
|
|
"""Test that KB is created with default name if not provided."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_kb = Mock()
|
|
mock_kb.uuid = 'new_kb_uuid'
|
|
mock_app.rag_mgr.create_knowledge_base = AsyncMock(return_value=mock_kb)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
await service.create_knowledge_base({
|
|
'knowledge_engine_plugin_id': 'author/engine'
|
|
})
|
|
|
|
# Check that default name 'Untitled' was used
|
|
call_args = mock_app.rag_mgr.create_knowledge_base.call_args
|
|
assert call_args.kwargs['name'] == 'Untitled'
|
|
|
|
|
|
class TestUpdateKnowledgeBase:
|
|
"""Tests for update_knowledge_base method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_updates_mutable_fields_only(self):
|
|
"""Test that only mutable fields are updated."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(
|
|
return_value={'uuid': 'kb1', 'name': 'Updated'}
|
|
)
|
|
mock_app.rag_mgr.remove_knowledge_base_from_runtime = AsyncMock()
|
|
mock_app.rag_mgr.load_knowledge_base = AsyncMock()
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
# Pass both mutable and immutable fields
|
|
await service.update_knowledge_base('kb1', {
|
|
'name': 'New Name',
|
|
'description': 'New desc',
|
|
'uuid': 'should_be_filtered', # immutable
|
|
})
|
|
|
|
# Check that only mutable fields were passed to update
|
|
call_args = mock_app.persistence_mgr.execute_async.call_args
|
|
assert call_args is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_early_when_no_mutable_fields(self):
|
|
"""Test that update returns early when no mutable fields provided."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
# Pass only immutable fields
|
|
await service.update_knowledge_base('kb1', {'uuid': 'should_be_filtered'})
|
|
|
|
# No DB update should be called
|
|
mock_app.persistence_mgr.execute_async.assert_not_called()
|
|
|
|
|
|
class TestCheckDocCapability:
|
|
"""Tests for _check_doc_capability method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_passes_when_capability_supported(self):
|
|
"""Test that check passes when doc_ingestion capability exists."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(
|
|
return_value={'knowledge_engine': {'capabilities': ['doc_ingestion']}}
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
await service._check_doc_capability('kb1', 'document upload')
|
|
|
|
# No exception raised means success
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raises_when_kb_not_found(self):
|
|
"""Test that Exception is raised when KB not found."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(return_value=None)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
await service._check_doc_capability('nonexistent', 'test operation')
|
|
|
|
assert 'Knowledge base not found' in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_raises_when_capability_not_supported(self):
|
|
"""Test that Exception is raised when doc_ingestion not in capabilities."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.rag_mgr.get_knowledge_base_details = AsyncMock(
|
|
return_value={'knowledge_engine': {'capabilities': ['other_capability']}}
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
await service._check_doc_capability('kb1', 'document upload')
|
|
|
|
assert 'does not support document upload' in str(exc_info.value)
|
|
|
|
|
|
class TestListKnowledgeEngines:
|
|
"""Tests for list_knowledge_engines method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_engines_from_plugin_connector(self):
|
|
"""Test that it returns knowledge engines from plugin connector."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.list_knowledge_engines = AsyncMock(
|
|
return_value=[{'id': 'engine1', 'name': 'Engine 1'}]
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_knowledge_engines()
|
|
|
|
assert len(result) == 1
|
|
assert result[0]['id'] == 'engine1'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_when_plugin_disabled(self):
|
|
"""Test that it returns empty list when plugin disabled."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.is_enable_plugin = False
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_knowledge_engines()
|
|
|
|
assert result == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_on_exception(self):
|
|
"""Test that it returns empty list and logs warning on exception."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.list_knowledge_engines = AsyncMock(
|
|
side_effect=Exception('Connection error')
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_knowledge_engines()
|
|
|
|
assert result == []
|
|
mock_app.logger.warning.assert_called_once()
|
|
|
|
|
|
class TestListParsers:
|
|
"""Tests for list_parsers method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_all_parsers(self):
|
|
"""Test that it returns all parsers when no MIME type filter."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.list_parsers = AsyncMock(
|
|
return_value=[
|
|
{'id': 'parser1', 'supported_mime_types': ['text/plain']},
|
|
{'id': 'parser2', 'supported_mime_types': ['application/pdf']},
|
|
]
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_parsers()
|
|
|
|
assert len(result) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filters_by_mime_type(self):
|
|
"""Test that it filters parsers by MIME type."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.list_parsers = AsyncMock(
|
|
return_value=[
|
|
{'id': 'parser1', 'supported_mime_types': ['text/plain']},
|
|
{'id': 'parser2', 'supported_mime_types': ['application/pdf']},
|
|
]
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_parsers(mime_type='application/pdf')
|
|
|
|
assert len(result) == 1
|
|
assert result[0]['id'] == 'parser2'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_when_plugin_disabled(self):
|
|
"""Test that it returns empty list when plugin disabled."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.is_enable_plugin = False
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.list_parsers()
|
|
|
|
assert result == []
|
|
|
|
|
|
class TestGetEngineSchemas:
|
|
"""Tests for get_engine_creation_schema and get_engine_retrieval_schema."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_creation_schema(self):
|
|
"""Test that it returns creation schema for engine."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.get_rag_creation_schema = AsyncMock(
|
|
return_value={'properties': {'name': {'type': 'string'}}}
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_engine_creation_schema('author/engine')
|
|
|
|
assert 'properties' in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_retrieval_schema(self):
|
|
"""Test that it returns retrieval schema for engine."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.get_rag_retrieval_schema = AsyncMock(
|
|
return_value={'properties': {'top_k': {'type': 'integer'}}}
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_engine_retrieval_schema('author/engine')
|
|
|
|
assert 'properties' in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_dict_on_exception(self):
|
|
"""Test that it returns empty dict and logs warning on exception."""
|
|
knowledge_module = get_knowledge_service_module()
|
|
mock_app = create_mock_app()
|
|
mock_app.plugin_connector.get_rag_creation_schema = AsyncMock(
|
|
side_effect=Exception('Plugin error')
|
|
)
|
|
|
|
service = knowledge_module.KnowledgeService(mock_app)
|
|
result = await service.get_engine_creation_schema('author/engine')
|
|
|
|
assert result == {}
|
|
mock_app.logger.warning.assert_called_once() |