Files
LangBot/tests/unit_tests/plugin/test_handler.py
T
2026-07-12 20:36:32 +08:00

293 lines
10 KiB
Python

"""Tests for RuntimeConnectionHandler helper functions.
Tests handler helper methods that don't require full handler setup.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, Mock
import pytest
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
def make_handler(app):
"""Create a RuntimeConnectionHandler with mocked external connection."""
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
return RuntimeConnectionHandler(Mock(), AsyncMock(return_value=True), app)
class TestHandlerQueryVariables:
"""Tests for handler query variable logic."""
@pytest.fixture
def mock_app(self):
"""Create mock app with query pool."""
app = SimpleNamespace()
app.query_pool = SimpleNamespace()
app.query_pool.cached_queries = {}
app.logger = SimpleNamespace()
app.logger.debug = MagicMock()
app.logger.warning = MagicMock()
return app
@pytest.mark.asyncio
async def test_set_query_var_query_not_found(self, mock_app):
"""Test set_query_var returns error when query not found."""
runtime_handler = make_handler(mock_app)
response = await runtime_handler.actions[PluginToRuntimeAction.SET_QUERY_VAR.value](
{
'query_id': 'nonexistent-query',
'key': 'test_var',
'value': 'test_value',
}
)
assert response.code != 0
assert 'nonexistent-query' in response.message
@pytest.mark.asyncio
async def test_set_query_var_success(self, mock_app):
"""Test set_query_var sets variable on existing query."""
runtime_handler = make_handler(mock_app)
mock_query = SimpleNamespace()
mock_query.variables = {}
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.SET_QUERY_VAR.value](
{
'query_id': 'test-query',
'key': 'test_var',
'value': 'test_value',
}
)
assert response.code == 0
assert mock_query.variables['test_var'] == 'test_value'
@pytest.mark.asyncio
@pytest.mark.parametrize(
'key',
[
'_host_box_scope',
'_host_tool_source_refs',
'_pipeline_bound_plugins',
'_pipeline_bound_mcp_servers',
'_pipeline_bound_skills',
'_pipeline_mcp_resource_attachments',
'_pipeline_mcp_resource_agent_read_enabled',
'_activated_skills',
'_fallback_model_uuids',
'_monitoring_message_id',
'_sandbox_outbound_collected',
'_authorized_models',
'_permission_tools',
'_routed_by_rule',
],
)
async def test_set_query_var_rejects_host_reserved_keys(self, mock_app, key):
runtime_handler = make_handler(mock_app)
original_variables = {key: 'host-owned'}
mock_query = SimpleNamespace(variables=original_variables.copy())
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.SET_QUERY_VAR.value](
{
'query_id': 'test-query',
'key': key,
'value': 'plugin-overwrite',
}
)
assert response.code != 0
assert response.message == f'Query variable {key!r} is reserved for LangBot Host'
assert mock_query.variables == original_variables
mock_app.logger.warning.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize(
'key',
[
'business_context',
'_ltm_context',
'_knowledge_base_uuids',
'_skill_authoring_post_response_candidate',
],
)
async def test_set_query_var_keeps_plugin_business_variables_writable(self, mock_app, key):
runtime_handler = make_handler(mock_app)
mock_query = SimpleNamespace(variables={})
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.SET_QUERY_VAR.value](
{
'query_id': 'test-query',
'key': key,
'value': {'plugin': 'value'},
}
)
assert response.code == 0
assert mock_query.variables[key] == {'plugin': 'value'}
@pytest.mark.asyncio
@pytest.mark.parametrize('key', ['', None, 7])
async def test_set_query_var_rejects_invalid_key_shapes(self, mock_app, key):
runtime_handler = make_handler(mock_app)
mock_query = SimpleNamespace(variables={})
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.SET_QUERY_VAR.value](
{
'query_id': 'test-query',
'key': key,
'value': 'value',
}
)
assert response.code != 0
assert response.message == 'Query variable key must be a non-empty string'
assert mock_query.variables == {}
@pytest.mark.asyncio
async def test_get_query_var_success(self, mock_app):
"""Test get_query_var retrieves variable from query."""
runtime_handler = make_handler(mock_app)
mock_query = SimpleNamespace()
mock_query.variables = {'existing_var': 'existing_value'}
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.GET_QUERY_VAR.value](
{
'query_id': 'test-query',
'key': 'existing_var',
}
)
assert response.code == 0
assert response.data == {'value': 'existing_value'}
@pytest.mark.asyncio
async def test_get_query_vars_multiple(self, mock_app):
"""Test get_query_vars returns the query's variable mapping."""
runtime_handler = make_handler(mock_app)
mock_query = SimpleNamespace()
mock_query.variables = {'var1': 'val1', 'var2': 'val2', 'var3': 'val3'}
mock_app.query_pool.cached_queries['test-query'] = mock_query
response = await runtime_handler.actions[PluginToRuntimeAction.GET_QUERY_VARS.value](
{
'query_id': 'test-query',
}
)
assert response.code == 0
assert response.data == {'vars': mock_query.variables}
class TestHandlerRagErrorResponse:
"""Tests for _make_rag_error_response helper."""
def test_make_rag_error_response_basic(self):
"""Test basic error response creation."""
from langbot.pkg.plugin.handler import _make_rag_error_response
error = Exception('test error')
response = _make_rag_error_response(error, 'TestError')
# ActionResponse is a pydantic model, check message field
assert 'TestError' in response.message
assert 'test error' in response.message
assert 'Exception' in response.message
def test_make_rag_error_response_with_context(self):
"""Test error response with extra context."""
from langbot.pkg.plugin.handler import _make_rag_error_response
error = ValueError('invalid input')
response = _make_rag_error_response(error, 'ValidationError', field='name', value='test')
assert 'ValidationError' in response.message
assert 'field=name' in response.message
assert 'value=test' in response.message
assert 'ValueError' in response.message
def test_make_rag_error_response_exception_type(self):
"""Test error response includes exception type."""
from langbot.pkg.plugin.handler import _make_rag_error_response
error = RuntimeError('connection failed')
response = _make_rag_error_response(error, 'ConnectionError')
assert 'RuntimeError' in response.message
assert 'ConnectionError' in response.message
assert 'connection failed' in response.message
def test_make_rag_error_response_empty_context(self):
"""Test error response with no extra context."""
from langbot.pkg.plugin.handler import _make_rag_error_response
error = KeyError('missing_key')
response = _make_rag_error_response(error, 'LookupError')
# No context parts means no brackets
assert '[' in response.message # Still has error type bracket
assert 'KeyError' in response.message
class TestHandlerPluginDiagnostic:
@pytest.mark.asyncio
async def test_notify_plugin_diagnostic_falls_back_to_raw_protocol_action(self):
"""Diagnostic forwarding works before the SDK enum exists."""
app = SimpleNamespace()
app.logger = SimpleNamespace(debug=MagicMock())
runtime_handler = make_handler(app)
runtime_handler.call_action = AsyncMock(return_value={})
payload = {'code': 'response_delivery_failed'}
await runtime_handler.notify_plugin_diagnostic(payload)
action = runtime_handler.call_action.await_args.args[0]
assert action.value == 'plugin_diagnostic'
assert runtime_handler.call_action.await_args.args[1] is payload
assert runtime_handler.call_action.await_args.kwargs['timeout'] == 5
def test_langbot_to_runtime_action_uses_enum_when_available(self):
"""The compatibility helper should prefer SDK enums once available."""
from langbot.pkg.plugin import handler as plugin_handler
sentinel = object()
original = plugin_handler.LangBotToRuntimeAction
plugin_handler.LangBotToRuntimeAction = SimpleNamespace(PLUGIN_DIAGNOSTIC=sentinel)
try:
assert plugin_handler._langbot_to_runtime_action('PLUGIN_DIAGNOSTIC', 'plugin_diagnostic') is sentinel
finally:
plugin_handler.LangBotToRuntimeAction = original
class TestConstantsSemanticVersion:
"""Tests for version constant access."""
def test_semantic_version_exists(self):
"""Test semantic_version is defined."""
from langbot.pkg.utils import constants
assert hasattr(constants, 'semantic_version')
assert constants.semantic_version.startswith('v')
def test_edition_exists(self):
"""Test edition constant is defined."""
from langbot.pkg.utils import constants
assert hasattr(constants, 'edition')
assert constants.edition == 'community'