Files
LangBot/tests/unit_tests/plugin/test_handler_helpers.py
T
RockChinQandHyu 616981b0cf perf(llm): harden request policy and optimize connections, reasoning and prompt caching (#2661)
* fix(llm): centralize request policy and preserve stream diagnostics

* test(llm): dispose SQLite engine after migration regression

* perf(llm): reuse connections and unify reasoning and prompt cache policy

* fix(llm): preserve explicit native prompt cache options

* perf(llm): fix measured TLS setup and streaming reuse overhead

* fix(llm): resolve plugin request-chain regressions found by E2E

---------

Co-authored-by: Hyu <[email protected]>
2026-10-15 19:48:35 +08:00

148 lines
5.2 KiB
Python

"""Unit tests for plugin handler helper functions and methods.
Tests cover:
- _make_rag_error_response() helper function
- RuntimeConnectionHandler cleanup_plugin_data method
"""
from __future__ import annotations
import pytest
from unittest.mock import Mock, AsyncMock
from importlib import import_module
from types import SimpleNamespace
@pytest.mark.parametrize(
'call_args',
[
{'num_retries': 0},
{'max_retries': 0},
{'timeout': 3},
{'request_policy': {'stream_idle_timeout': 2}},
],
)
def test_llm_bridge_keeps_model_and_call_policy_layers_separate(call_args):
from langbot.pkg.provider.modelmgr.request_policy import RequestPolicy
model_args = {'temperature': 0.2, 'request_policy': {'retries': 2, 'read_timeout': 90}}
model = SimpleNamespace(model_entity=SimpleNamespace(extra_args=model_args))
merged = get_handler_module()._merge_model_extra_args(model, call_args, llm_policy=True)
assert RequestPolicy.resolve(model_args, merged) == RequestPolicy.resolve(model_args, call_args)
assert merged['temperature'] == 0.2
assert model_args['request_policy'] == {'retries': 2, 'read_timeout': 90}
def get_handler_module():
"""Lazy import to avoid circular import issues."""
return import_module('langbot.pkg.plugin.handler')
class TestMakeRagErrorResponse:
"""Tests for _make_rag_error_response helper function."""
def test_creates_error_response_with_exception(self):
"""Test basic error response creation."""
handler = get_handler_module()
error = ValueError('test error message')
result = handler._make_rag_error_response(error, 'TestError')
# ActionResponse.error() returns code=1 (error status)
assert result.code == 1
assert 'TestError' in result.message
assert 'ValueError' in result.message
assert 'test error message' in result.message
def test_includes_error_type_in_message(self):
"""Test that error type is included in message."""
handler = get_handler_module()
error = RuntimeError('something went wrong')
result = handler._make_rag_error_response(error, 'VectorStoreError')
assert '[VectorStoreError/RuntimeError]' in result.message
def test_includes_extra_context_in_message(self):
"""Test that extra context fields are included."""
handler = get_handler_module()
error = Exception('embedding failed')
result = handler._make_rag_error_response(
error,
'EmbeddingError',
embedding_model_uuid='test-uuid-123',
collection_id='collection-456',
)
assert 'embedding_model_uuid=test-uuid-123' in result.message
assert 'collection_id=collection-456' in result.message
def test_handles_exception_with_no_message(self):
"""Test handling exception with empty message."""
handler = get_handler_module()
error = Exception()
result = handler._make_rag_error_response(error, 'GenericError')
# ActionResponse.error() returns code=1 (error status)
assert result.code == 1
assert '[GenericError/Exception]' in result.message
def test_formats_context_with_multiple_fields(self):
"""Test multiple context fields are comma separated."""
handler = get_handler_module()
error = IOError('file not found')
result = handler._make_rag_error_response(
error,
'FileServiceError',
storage_path='/data/file.pdf',
kb_id='kb-001',
)
assert '[storage_path=/data/file.pdf, kb_id=kb-001]' in result.message
class TestCleanupPluginData:
"""Tests for cleanup_plugin_data method."""
@pytest.mark.asyncio
async def test_deletes_plugin_settings(self):
"""Test that plugin settings are deleted."""
handler_module = get_handler_module()
mock_app = Mock()
mock_app.persistence_mgr = AsyncMock()
mock_app.persistence_mgr.execute_async = AsyncMock()
# Mock the handler without connection (we only need ap)
handler_instance = Mock(spec=handler_module.RuntimeConnectionHandler)
handler_instance.ap = mock_app
# Call cleanup_plugin_data
await handler_module.RuntimeConnectionHandler.cleanup_plugin_data(
handler_instance, 'test-author', 'test-plugin'
)
# Verify plugin settings delete was called
calls = mock_app.persistence_mgr.execute_async.call_args_list
assert len(calls) >= 1
@pytest.mark.asyncio
async def test_deletes_binary_storage(self):
"""Test that binary storage is deleted."""
handler_module = get_handler_module()
mock_app = Mock()
mock_app.persistence_mgr = AsyncMock()
mock_app.persistence_mgr.execute_async = AsyncMock()
handler_instance = Mock(spec=handler_module.RuntimeConnectionHandler)
handler_instance.ap = mock_app
await handler_module.RuntimeConnectionHandler.cleanup_plugin_data(handler_instance, 'author', 'plugin-name')
# Should have at least 2 calls: one for settings, one for binary storage
assert mock_app.persistence_mgr.execute_async.call_count >= 2