Files
LangBot/tests/unit_tests/plugin/test_connector_methods.py
2026-05-16 10:30:17 +08:00

505 lines
17 KiB
Python

"""Unit tests for plugin connector methods.
Tests cover:
- list_plugins() with filtering and sorting
- list_knowledge_engines() and list_parsers()
- RAG methods (ingest, retrieve, schema)
- Disabled plugin early returns
"""
from __future__ import annotations
import pytest
from unittest.mock import Mock, AsyncMock
from importlib import import_module
def get_connector_module():
"""Lazy import to avoid circular import issues."""
return import_module('langbot.pkg.plugin.connector')
def create_mock_app():
"""Create mock Application for testing."""
mock_app = Mock()
mock_app.logger = Mock()
mock_app.instance_config = Mock()
mock_app.instance_config.data = {'plugin': {'enable': True}}
mock_app.persistence_mgr = AsyncMock()
mock_app.persistence_mgr.execute_async = AsyncMock()
return mock_app
def create_mock_connector():
"""Create mock PluginRuntimeConnector instance for testing."""
connector = get_connector_module()
async def mock_disconnect_callback(conn):
pass
return connector.PluginRuntimeConnector(create_mock_app(), mock_disconnect_callback)
class TestListPlugins:
"""Tests for list_plugins method."""
@pytest.mark.asyncio
async def test_returns_empty_when_plugin_disabled(self):
"""Test returns empty list when plugin system disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.list_plugins()
assert result == []
@pytest.mark.asyncio
async def test_calls_handler_list_plugins(self):
"""Test that handler.list_plugins is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.list_plugins = AsyncMock(
return_value=[{'manifest': {'manifest': {'metadata': {'author': 'test', 'name': 'plugin'}}}}]
)
result = await connector.list_plugins()
connector.handler.list_plugins.assert_called_once()
assert result == [{'manifest': {'manifest': {'metadata': {'author': 'test', 'name': 'plugin'}}}}]
@pytest.mark.asyncio
async def test_filters_by_component_kinds(self):
"""Test that plugins are filtered by component kinds."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.list_plugins = AsyncMock(
return_value=[
{
'manifest': {'manifest': {'metadata': {'author': 'a', 'name': 'p1'}}},
'components': [
{'manifest': {'manifest': {'kind': 'Command'}}}
],
'debug': False,
},
{
'manifest': {'manifest': {'metadata': {'author': 'b', 'name': 'p2'}}},
'components': [
{'manifest': {'manifest': {'kind': 'Tool'}}}
],
'debug': False,
},
]
)
result = await connector.list_plugins(component_kinds=['Command'])
assert len(result) == 1
assert result[0]['manifest']['manifest']['metadata']['name'] == 'p1'
@pytest.mark.asyncio
async def test_sorts_debug_plugins_first(self):
"""Test that debug plugins are sorted first."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.list_plugins = AsyncMock(
return_value=[
{
'manifest': {'manifest': {'metadata': {'author': 'a', 'name': 'normal'}}},
'components': [],
'debug': False,
},
{
'manifest': {'manifest': {'metadata': {'author': 'b', 'name': 'debug'}}},
'components': [],
'debug': True,
},
]
)
connector.ap.persistence_mgr.execute_async = AsyncMock(
return_value=Mock(__iter__=lambda self: iter([]))
)
result = await connector.list_plugins()
# Debug plugin should be first
assert result[0]['debug'] is True
class TestListKnowledgeEngines:
"""Tests for list_knowledge_engines method."""
@pytest.mark.asyncio
async def test_returns_empty_when_plugin_disabled(self):
"""Test returns empty list when plugin system disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.list_knowledge_engines()
assert result == []
@pytest.mark.asyncio
async def test_calls_handler_list_knowledge_engines(self):
"""Test that handler method is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.list_knowledge_engines = AsyncMock(
return_value=[{'plugin_id': 'author/engine', 'name': 'Engine'}]
)
result = await connector.list_knowledge_engines()
connector.handler.list_knowledge_engines.assert_called_once()
assert result == [{'plugin_id': 'author/engine', 'name': 'Engine'}]
class TestListParsers:
"""Tests for list_parsers method."""
@pytest.mark.asyncio
async def test_returns_empty_when_plugin_disabled(self):
"""Test returns empty list when plugin system disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.list_parsers()
assert result == []
@pytest.mark.asyncio
async def test_calls_handler_list_parsers(self):
"""Test that handler method is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.list_parsers = AsyncMock(
return_value=[{'plugin_id': 'author/parser', 'supported_mime_types': ['text/plain']}]
)
result = await connector.list_parsers()
connector.handler.list_parsers.assert_called_once()
assert result == [{'plugin_id': 'author/parser', 'supported_mime_types': ['text/plain']}]
class TestCallParser:
"""Tests for call_parser method."""
@pytest.mark.asyncio
async def test_calls_handler_parse_document(self):
"""Test that handler.parse_document is called with correct args."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.parse_document = AsyncMock(return_value={'content': 'parsed'})
result = await connector.call_parser(
'author/parser',
{'mime_type': 'text/plain', 'filename': 'test.txt'},
b'file content',
)
connector.handler.parse_document.assert_called_once_with(
'author', 'parser',
{'mime_type': 'text/plain', 'filename': 'test.txt'},
b'file content',
)
assert result['content'] == 'parsed'
class TestRAGMethods:
"""Tests for RAG-related methods."""
@pytest.mark.asyncio
async def test_call_rag_ingest(self):
"""Test call_rag_ingest calls handler with parsed plugin ID."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.rag_ingest_document = AsyncMock(return_value={'status': 'success'})
result = await connector.call_rag_ingest('author/engine', {'file': 'test.pdf'})
connector.handler.rag_ingest_document.assert_called_once_with(
'author', 'engine', {'file': 'test.pdf'}
)
assert result['status'] == 'success'
@pytest.mark.asyncio
async def test_call_rag_retrieve(self):
"""Test call_rag_retrieve calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.retrieve_knowledge = AsyncMock(
return_value={'results': [{'id': 'doc1', 'content': [{'type': 'text', 'text': 'test'}], 'metadata': {}, 'distance': 0.1}]}
)
result = await connector.call_rag_retrieve('author/engine', {'query': 'test'})
connector.handler.retrieve_knowledge.assert_called_once_with(
'author', 'engine', '', {'query': 'test'}
)
assert result == {
'results': [
{
'id': 'doc1',
'content': [{'type': 'text', 'text': 'test'}],
'metadata': {},
'distance': 0.1,
}
]
}
@pytest.mark.asyncio
async def test_get_rag_creation_schema(self):
"""Test get_rag_creation_schema calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.get_rag_creation_schema = AsyncMock(
return_value={'properties': {'name': {'type': 'string'}}}
)
result = await connector.get_rag_creation_schema('author/engine')
connector.handler.get_rag_creation_schema.assert_called_once_with('author', 'engine')
assert result == {'properties': {'name': {'type': 'string'}}}
@pytest.mark.asyncio
async def test_get_rag_retrieval_schema(self):
"""Test get_rag_retrieval_schema calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.get_rag_retrieval_schema = AsyncMock(
return_value={'properties': {'top_k': {'type': 'integer'}}}
)
result = await connector.get_rag_retrieval_schema('author/engine')
connector.handler.get_rag_retrieval_schema.assert_called_once_with('author', 'engine')
assert result == {'properties': {'top_k': {'type': 'integer'}}}
@pytest.mark.asyncio
async def test_rag_on_kb_create(self):
"""Test rag_on_kb_create calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.rag_on_kb_create = AsyncMock(return_value={'status': 'ok'})
await connector.rag_on_kb_create('author/engine', 'kb-uuid', {'model': 'test'})
connector.handler.rag_on_kb_create.assert_called_once_with(
'author', 'engine', 'kb-uuid', {'model': 'test'}
)
@pytest.mark.asyncio
async def test_rag_on_kb_delete(self):
"""Test rag_on_kb_delete calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.rag_on_kb_delete = AsyncMock(return_value={'status': 'ok'})
await connector.rag_on_kb_delete('author/engine', 'kb-uuid')
connector.handler.rag_on_kb_delete.assert_called_once_with('author', 'engine', 'kb-uuid')
@pytest.mark.asyncio
async def test_call_rag_delete_document(self):
"""Test call_rag_delete_document calls handler."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.rag_delete_document = AsyncMock(return_value=True)
result = await connector.call_rag_delete_document('author/engine', 'doc-uuid', 'kb-uuid')
connector.handler.rag_delete_document.assert_called_once_with(
'author', 'engine', 'doc-uuid', 'kb-uuid'
)
assert result is True
class TestRetrieveKnowledge:
"""Tests for retrieve_knowledge method."""
@pytest.mark.asyncio
async def test_returns_empty_results_when_plugin_disabled(self):
"""Test returns empty when plugin disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.retrieve_knowledge('author', 'engine', 'retriever', {})
assert result == {'results': []}
class TestDisabledPluginEarlyReturns:
"""Tests for early returns when plugin system is disabled."""
@pytest.mark.asyncio
async def test_list_tools_returns_empty(self):
"""Test list_tools returns empty when disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.list_tools()
assert result == []
@pytest.mark.asyncio
async def test_list_commands_returns_empty(self):
"""Test list_commands returns empty when disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.list_commands()
assert result == []
@pytest.mark.asyncio
async def test_get_debug_info_returns_empty(self):
"""Test get_debug_info returns empty dict when disabled."""
connector_module = get_connector_module()
async def mock_disconnect(conn):
pass
mock_app = create_mock_app()
mock_app.instance_config.data = {'plugin': {'enable': False}}
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
result = await connector.get_debug_info()
assert result == {}
class TestGetPluginInfo:
"""Tests for get_plugin_info method."""
@pytest.mark.asyncio
async def test_calls_handler_get_plugin_info(self):
"""Test that handler.get_plugin_info is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.get_plugin_info = AsyncMock(
return_value={'manifest': {'metadata': {'name': 'plugin'}}}
)
result = await connector.get_plugin_info('author', 'plugin')
connector.handler.get_plugin_info.assert_called_once_with('author', 'plugin')
assert result == {'manifest': {'metadata': {'name': 'plugin'}}}
class TestSetPluginConfig:
"""Tests for set_plugin_config method."""
@pytest.mark.asyncio
async def test_calls_handler_set_plugin_config(self):
"""Test that handler.set_plugin_config is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.set_plugin_config = AsyncMock(return_value={'status': 'ok'})
await connector.set_plugin_config('author', 'plugin', {'setting': 'value'})
connector.handler.set_plugin_config.assert_called_once_with(
'author', 'plugin', {'setting': 'value'}
)
class TestPingPluginRuntime:
"""Tests for ping_plugin_runtime method."""
@pytest.mark.asyncio
async def test_raises_when_handler_not_set(self):
"""Test that exception is raised when handler not initialized."""
get_connector_module()
connector = create_mock_connector()
# handler is not set
with pytest.raises(Exception, match='Plugin runtime is not connected') as exc_info:
await connector.ping_plugin_runtime()
assert 'not connected' in str(exc_info.value)
@pytest.mark.asyncio
async def test_calls_handler_ping(self):
"""Test that handler.ping is called."""
get_connector_module()
connector = create_mock_connector()
connector.handler = AsyncMock()
connector.handler.ping = AsyncMock(return_value={'status': 'ok'})
await connector.ping_plugin_runtime()
connector.handler.ping.assert_called_once()