mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat(agent-platform): productize bot event route testing
This commit is contained in:
@@ -54,11 +54,7 @@ def _create_mock_result(items: list = None, first_item=None):
|
||||
|
||||
def _compiled_update_values(statement):
|
||||
"""Return update values without SQLAlchemy WHERE bind params."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in statement.compile().params.items()
|
||||
if not key.startswith('uuid_')
|
||||
}
|
||||
return {key: value for key, value in statement.compile().params.items() if not key.startswith('uuid_')}
|
||||
|
||||
|
||||
def _create_mock_discover(adapter_webhook_flags: dict[str, bool] = None):
|
||||
@@ -363,7 +359,8 @@ class TestBotServiceCreateBot:
|
||||
}
|
||||
}
|
||||
ap.platform_mgr = SimpleNamespace()
|
||||
ap.platform_mgr.load_bot = AsyncMock()
|
||||
runtime_bot = SimpleNamespace(enable=True, run=AsyncMock())
|
||||
ap.platform_mgr.load_bot = AsyncMock(return_value=runtime_bot)
|
||||
|
||||
# Mock pipeline query
|
||||
pipeline_result = Mock()
|
||||
@@ -394,6 +391,7 @@ class TestBotServiceCreateBot:
|
||||
# Verify
|
||||
assert bot_uuid is not None
|
||||
assert len(bot_uuid) == 36 # UUID format
|
||||
runtime_bot.run.assert_awaited_once()
|
||||
|
||||
|
||||
class TestBotServiceUpdateBot:
|
||||
@@ -1125,6 +1123,64 @@ class TestBotServiceEventRouteStatuses:
|
||||
assert result['unmatched_events'][0]['failure_code'] == 'route_not_found'
|
||||
|
||||
|
||||
class TestBotServiceDispatchTestEventRoute:
|
||||
"""Tests for synthetic Bot route test dispatch."""
|
||||
|
||||
async def test_dispatch_test_event_route_rejects_invalid_event_type(self):
|
||||
"""Missing event_type returns a non-dispatched test result."""
|
||||
service = BotService(SimpleNamespace())
|
||||
|
||||
result = await service.dispatch_test_event_route('bot-1', '', {})
|
||||
|
||||
assert result['dispatched'] is False
|
||||
assert result['failure_code'] == 'invalid_event'
|
||||
assert result['reason'] == 'event_type is required'
|
||||
assert result['route_status']['routes'] == []
|
||||
|
||||
async def test_dispatch_test_event_route_rejects_non_object_payload(self):
|
||||
"""Payload must be a JSON object."""
|
||||
service = BotService(SimpleNamespace())
|
||||
|
||||
result = await service.dispatch_test_event_route('bot-1', 'message.received', []) # type: ignore[arg-type]
|
||||
|
||||
assert result['dispatched'] is False
|
||||
assert result['failure_code'] == 'invalid_event'
|
||||
assert result['reason'] == 'payload must be an object'
|
||||
assert result['route_status']['routes'] == []
|
||||
|
||||
async def test_dispatch_test_event_route_calls_runtime_and_returns_status(self):
|
||||
"""Synthetic route tests run against the runtime bot and return route status."""
|
||||
ap = SimpleNamespace()
|
||||
ap.platform_mgr = SimpleNamespace()
|
||||
runtime_bot = SimpleNamespace()
|
||||
runtime_bot.dispatch_test_event = AsyncMock(
|
||||
return_value={
|
||||
'event_type': 'message.received',
|
||||
'dispatched': True,
|
||||
'status': 'delivered',
|
||||
'binding_id': 'binding-1',
|
||||
'failure_code': None,
|
||||
'reason': 'Delivered to processor',
|
||||
'suppressed_outputs': [{'method': 'send_message'}],
|
||||
}
|
||||
)
|
||||
runtime_bot.bot_entity = SimpleNamespace(event_bindings=[])
|
||||
runtime_bot.logger = SimpleNamespace(logs=[])
|
||||
ap.platform_mgr.get_bot_by_uuid = AsyncMock(return_value=runtime_bot)
|
||||
|
||||
service = BotService(ap)
|
||||
result = await service.dispatch_test_event_route('bot-1', 'message.received', {'message_text': 'hello'})
|
||||
|
||||
runtime_bot.dispatch_test_event.assert_awaited_once_with('message.received', {'message_text': 'hello'})
|
||||
assert result['dispatched'] is True
|
||||
assert result['status'] == 'delivered'
|
||||
assert result['binding_id'] == 'binding-1'
|
||||
assert result['reason'] == 'Delivered to processor'
|
||||
assert result['event_type'] == 'message.received'
|
||||
assert result['suppressed_outputs'] == [{'method': 'send_message'}]
|
||||
assert result['route_status']['routes'] == []
|
||||
|
||||
|
||||
class TestBotServiceSendMessage:
|
||||
"""Tests for send_message method."""
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.api.mcp.server import LangBotMCPServer
|
||||
|
||||
|
||||
def _make_app() -> SimpleNamespace:
|
||||
app = SimpleNamespace()
|
||||
app.instance_config = SimpleNamespace(data={'system': {'edition': 'community', 'instance_id': 'inst-1'}})
|
||||
app.ver_mgr = SimpleNamespace(get_current_version=lambda: 'test-version')
|
||||
app.bot_service = SimpleNamespace(
|
||||
get_bots=AsyncMock(return_value=[]),
|
||||
get_bot=AsyncMock(return_value={'uuid': 'bot-1'}),
|
||||
create_bot=AsyncMock(return_value='bot-1'),
|
||||
update_bot=AsyncMock(),
|
||||
delete_bot=AsyncMock(),
|
||||
list_event_route_statuses=AsyncMock(return_value={'routes': [], 'unmatched_events': [], 'stale_routes': []}),
|
||||
dispatch_test_event_route=AsyncMock(
|
||||
return_value={
|
||||
'dispatched': True,
|
||||
'event_type': 'message.received',
|
||||
'suppressed_outputs': [],
|
||||
'route_status': {
|
||||
'routes': [],
|
||||
'unmatched_events': [],
|
||||
'stale_routes': [],
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
app.pipeline_service = SimpleNamespace(
|
||||
get_pipelines=AsyncMock(return_value=[]),
|
||||
get_pipeline=AsyncMock(return_value={}),
|
||||
create_pipeline=AsyncMock(return_value='pipeline-1'),
|
||||
update_pipeline=AsyncMock(),
|
||||
delete_pipeline=AsyncMock(),
|
||||
)
|
||||
app.agent_service = SimpleNamespace(
|
||||
get_agents=AsyncMock(return_value=[]),
|
||||
get_agent=AsyncMock(return_value={}),
|
||||
create_agent=AsyncMock(return_value={'uuid': 'agent-1', 'kind': 'agent'}),
|
||||
update_agent=AsyncMock(),
|
||||
delete_agent=AsyncMock(),
|
||||
)
|
||||
app.llm_model_service = SimpleNamespace(
|
||||
get_llm_models=AsyncMock(return_value=[]),
|
||||
get_llm_model=AsyncMock(return_value={}),
|
||||
)
|
||||
app.embedding_models_service = SimpleNamespace(get_embedding_models=AsyncMock(return_value=[]))
|
||||
app.provider_service = SimpleNamespace(get_providers=AsyncMock(return_value=[]))
|
||||
app.knowledge_service = SimpleNamespace(
|
||||
get_knowledge_bases=AsyncMock(return_value=[]),
|
||||
get_knowledge_base=AsyncMock(return_value={}),
|
||||
retrieve_knowledge_base=AsyncMock(return_value=[]),
|
||||
)
|
||||
app.mcp_service = SimpleNamespace(get_mcp_servers=AsyncMock(return_value=[]))
|
||||
app.skill_service = SimpleNamespace(
|
||||
list_skills=AsyncMock(return_value=[]),
|
||||
get_skill=AsyncMock(return_value={}),
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_server_exposes_bot_event_route_tools():
|
||||
app = _make_app()
|
||||
server = LangBotMCPServer(app)
|
||||
|
||||
tools = await server.mcp.list_tools()
|
||||
tool_names = {tool.name for tool in tools}
|
||||
|
||||
assert 'list_bot_event_route_statuses' in tool_names
|
||||
assert 'test_bot_event_route' in tool_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_test_bot_event_route_calls_service_layer():
|
||||
app = _make_app()
|
||||
server = LangBotMCPServer(app)
|
||||
|
||||
result_blocks, _ = await server.mcp.call_tool(
|
||||
'test_bot_event_route',
|
||||
{
|
||||
'bot_uuid': 'bot-1',
|
||||
'event_type': 'message.received',
|
||||
'payload': {'message_text': 'hello'},
|
||||
},
|
||||
)
|
||||
|
||||
app.bot_service.dispatch_test_event_route.assert_awaited_once_with(
|
||||
bot_uuid='bot-1',
|
||||
event_type='message.received',
|
||||
payload={'message_text': 'hello'},
|
||||
)
|
||||
data = json.loads(result_blocks[0].text)
|
||||
assert data['dispatched'] is True
|
||||
assert data['event_type'] == 'message.received'
|
||||
Reference in New Issue
Block a user