mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-23 00:45:25 +08:00
176 lines
6.6 KiB
Python
176 lines
6.6 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.fixture
|
|
async def cleanup(self, plugin_db):
|
|
from langbot.pkg.entity.persistence.plugin import PluginSetting
|
|
from langbot.pkg.entity.persistence.bstorage import BinaryStorage
|
|
from langbot_plugin.entities.io.context import InstallationBinding
|
|
|
|
plugin_db.add_all(
|
|
PluginSetting(workspace_uuid=workspace, plugin_author=author, plugin_name=name)
|
|
for workspace, author, name in [
|
|
('workspace-a', 'test-author', 'test-plugin'),
|
|
('workspace-b', 'test-author', 'test-plugin'),
|
|
('workspace-a', 'other-author', 'test-plugin'),
|
|
('workspace-a', 'test-author', 'other-plugin'),
|
|
]
|
|
)
|
|
plugin_db.add_all(
|
|
BinaryStorage(
|
|
workspace_uuid=workspace, unique_key=key, key=key, owner_type=owner_type, owner=owner, value=b'data'
|
|
)
|
|
for workspace, key, owner_type, owner in [
|
|
('workspace-a', 'target', 'plugin', 'test-author/test-plugin'),
|
|
('workspace-b', 'other-workspace', 'plugin', 'test-author/test-plugin'),
|
|
('workspace-a', 'other-owner', 'plugin', 'other-author/test-plugin'),
|
|
('workspace-a', 'other-type', 'skill', 'test-author/test-plugin'),
|
|
]
|
|
)
|
|
await plugin_db.flush()
|
|
app = Mock()
|
|
app.persistence_mgr.execute_async = plugin_db.execute
|
|
handler = get_handler_module().RuntimeConnectionHandler(Mock(), AsyncMock(), app)
|
|
binding = InstallationBinding(
|
|
instance_uuid='instance-a',
|
|
workspace_uuid='workspace-a',
|
|
placement_generation=1,
|
|
installation_uuid='00000000-0000-4000-8000-000000000001',
|
|
runtime_revision=1,
|
|
artifact_digest='a' * 64,
|
|
)
|
|
with handler.installation_scope(binding):
|
|
await handler.cleanup_plugin_data('test-author', 'test-plugin')
|
|
return plugin_db
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deletes_plugin_settings(self, cleanup):
|
|
import sqlalchemy
|
|
from langbot.pkg.entity.persistence.plugin import PluginSetting
|
|
|
|
rows = (
|
|
await cleanup.execute(
|
|
sqlalchemy.select(PluginSetting.workspace_uuid, PluginSetting.plugin_author, PluginSetting.plugin_name)
|
|
)
|
|
).all()
|
|
assert set(rows) == {
|
|
('workspace-b', 'test-author', 'test-plugin'),
|
|
('workspace-a', 'other-author', 'test-plugin'),
|
|
('workspace-a', 'test-author', 'other-plugin'),
|
|
}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deletes_binary_storage(self, cleanup):
|
|
"""Test that binary storage is deleted."""
|
|
import sqlalchemy
|
|
from langbot.pkg.entity.persistence.bstorage import BinaryStorage
|
|
|
|
keys = (await cleanup.execute(sqlalchemy.select(BinaryStorage.unique_key))).scalars().all()
|
|
assert set(keys) == {'other-workspace', 'other-owner', 'other-type'}
|