mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-28 05:07:14 +00:00
refactor(bots): remove route execution test
This commit is contained in:
@@ -171,18 +171,6 @@ def fake_bot_app():
|
||||
'diagnostic_details': [{'step': 'evaluate_binding', 'binding_id': 'binding-1', 'matched': True}],
|
||||
}
|
||||
)
|
||||
app.bot_service.dispatch_test_event_route = AsyncMock(
|
||||
return_value={
|
||||
'dispatched': True,
|
||||
'event_type': 'message.received',
|
||||
'suppressed_outputs': [],
|
||||
'route_status': {
|
||||
'routes': [],
|
||||
'unmatched_events': [],
|
||||
'stale_routes': [],
|
||||
},
|
||||
}
|
||||
)
|
||||
app.bot_service.send_message = AsyncMock()
|
||||
|
||||
# Platform manager
|
||||
@@ -373,35 +361,6 @@ class TestBotEventRouteStatusEndpoint:
|
||||
fake_bot_app.bot_service.list_event_route_statuses.assert_awaited_with(ANY, 'test-bot-uuid')
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestBotEventRouteTestEndpoint:
|
||||
"""Tests for bot event route synthetic dispatch endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_test_event_route_success(self, quart_test_client, fake_bot_app):
|
||||
"""POST test route dispatches a synthetic event."""
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/platform/bots/test-bot-uuid/event-routes/test',
|
||||
headers={'Authorization': 'Bearer test_token'},
|
||||
json={
|
||||
'event_type': 'message.received',
|
||||
'payload': {'message_text': 'hello'},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['dispatched'] is True
|
||||
assert data['data']['event_type'] == 'message.received'
|
||||
fake_bot_app.bot_service.dispatch_test_event_route.assert_awaited_with(
|
||||
ANY,
|
||||
bot_uuid='test-bot-uuid',
|
||||
event_type='message.received',
|
||||
payload={'message_text': 'hello'},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestBotSendMessageEndpoint:
|
||||
"""Tests for bot send message endpoint."""
|
||||
|
||||
@@ -56,18 +56,6 @@ def build_ap() -> SimpleNamespace:
|
||||
ap.bot_service = SimpleNamespace(
|
||||
get_bots=AsyncMock(return_value=[{'uuid': 'bot-1', 'name': 'Demo Bot', 'adapter': 'telegram'}]),
|
||||
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': [],
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
ap.pipeline_service = SimpleNamespace(get_pipelines=AsyncMock(return_value=[{'uuid': 'pl-1', 'name': 'default'}]))
|
||||
ap.llm_model_service = SimpleNamespace(get_llm_models=AsyncMock(return_value=[]))
|
||||
@@ -126,7 +114,7 @@ async def main() -> int:
|
||||
tools = await session.list_tools()
|
||||
names = [t.name for t in tools.tools]
|
||||
print(f'PASS: listed {len(names)} tools')
|
||||
for required in ('list_bots', 'get_system_info', 'list_skills', 'test_bot_event_route'):
|
||||
for required in ('list_bots', 'get_system_info', 'list_skills'):
|
||||
if required not in names:
|
||||
failures.append(f'missing tool {required}')
|
||||
|
||||
@@ -144,20 +132,6 @@ async def main() -> int:
|
||||
else:
|
||||
print('PASS: get_system_info returned version')
|
||||
|
||||
res3 = await session.call_tool(
|
||||
'test_bot_event_route',
|
||||
{
|
||||
'bot_uuid': 'bot-1',
|
||||
'event_type': 'message.received',
|
||||
'payload': {'message_text': 'hello'},
|
||||
},
|
||||
)
|
||||
text3 = res3.content[0].text if res3.content else ''
|
||||
if '"dispatched": true' not in text3:
|
||||
failures.append(f'test_bot_event_route wrong: {text3!r}')
|
||||
else:
|
||||
print('PASS: test_bot_event_route returned dispatch result')
|
||||
|
||||
shutdown.set()
|
||||
with contextlib.suppress(Exception):
|
||||
await asyncio.wait_for(server_task, timeout=5)
|
||||
|
||||
@@ -656,46 +656,6 @@ class TestBotServiceListEventRouteStatuses:
|
||||
assert result['stale_routes'] == []
|
||||
|
||||
|
||||
class TestBotServiceDispatchTestEventRoute:
|
||||
"""Tests for dispatching a synthetic event through a saved route."""
|
||||
|
||||
async def test_returns_actionable_failure_when_runtime_bot_is_unavailable(self):
|
||||
ap = SimpleNamespace()
|
||||
ap.platform_mgr = SimpleNamespace()
|
||||
ap.platform_mgr.get_bot_by_uuid = AsyncMock(return_value=None)
|
||||
|
||||
service = BotService(ap)
|
||||
service.get_bot = AsyncMock(return_value={'uuid': 'bot-uuid'})
|
||||
service.list_event_route_statuses = AsyncMock(
|
||||
return_value={
|
||||
'routes': [],
|
||||
'unmatched_events': [],
|
||||
'stale_routes': [],
|
||||
}
|
||||
)
|
||||
|
||||
result = await service.dispatch_test_event_route(
|
||||
WORKSPACE_UUID,
|
||||
'bot-uuid',
|
||||
'message.received',
|
||||
{'message_text': 'Hello'},
|
||||
)
|
||||
|
||||
assert result == {
|
||||
'dispatched': False,
|
||||
'event_type': 'message.received',
|
||||
'failure_code': 'bot_runtime_unavailable',
|
||||
'reason': 'Bot runtime is unavailable',
|
||||
'suppressed_outputs': [],
|
||||
'route_status': {
|
||||
'routes': [],
|
||||
'unmatched_events': [],
|
||||
'stale_routes': [],
|
||||
},
|
||||
}
|
||||
service.list_event_route_statuses.assert_awaited_once_with(WORKSPACE_UUID, 'bot-uuid')
|
||||
|
||||
|
||||
class TestBotServiceSendMessage:
|
||||
"""Tests for send_message method."""
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -20,18 +19,6 @@ def _make_app() -> SimpleNamespace:
|
||||
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=[]),
|
||||
@@ -75,30 +62,6 @@ async def test_mcp_server_exposes_bot_event_route_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
|
||||
assert 'test_bot_event_route' not in tool_names
|
||||
assert 'list_processors' in tool_names
|
||||
assert 'list_agents' not 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'
|
||||
|
||||
@@ -95,63 +95,6 @@ class TestEventRouteTrace:
|
||||
assert metadata['target_uuid'] == 'agent-1'
|
||||
assert metadata['status'] == 'failed'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_test_event_suppresses_agent_output_delivery(self):
|
||||
"""Synthetic test dispatch runs the route but does not call the real adapter."""
|
||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||||
|
||||
captured_envelopes = []
|
||||
|
||||
async def fake_run(envelope, binding, adapter_context=None):
|
||||
captured_envelopes.append(envelope)
|
||||
yield provider_message.Message(role='assistant', content='test response')
|
||||
|
||||
bot = self._make_bot(
|
||||
[
|
||||
{
|
||||
'id': 'agent-binding',
|
||||
'enabled': True,
|
||||
'event_pattern': 'message.received',
|
||||
'target_type': 'agent',
|
||||
'target_uuid': 'agent-1',
|
||||
'priority': 0,
|
||||
'order': 0,
|
||||
}
|
||||
]
|
||||
)
|
||||
bot.ap = SimpleNamespace(
|
||||
workspace_service=active_workspace_service(),
|
||||
agent_service=SimpleNamespace(
|
||||
get_agent=AsyncMock(
|
||||
return_value={
|
||||
'uuid': 'agent-1',
|
||||
'kind': 'agent',
|
||||
'enabled': True,
|
||||
'supported_event_patterns': ['message.received'],
|
||||
'config': {'runner': {'id': 'runner-1'}, 'runner_config': {'runner-1': {}}},
|
||||
}
|
||||
)
|
||||
),
|
||||
agent_run_orchestrator=SimpleNamespace(run=fake_run),
|
||||
)
|
||||
bot.adapter = SimpleNamespace(
|
||||
bot_account_id='bot-account',
|
||||
config={},
|
||||
logger=bot.logger,
|
||||
send_message=AsyncMock(),
|
||||
get_supported_apis=Mock(return_value=['send_message', 'edit_message', 'add_reaction', 'get_group_info']),
|
||||
)
|
||||
|
||||
result = await bot.dispatch_test_event('message.received', {'chat_id': 'user-1', 'message_text': 'hello'})
|
||||
|
||||
bot.adapter.send_message.assert_not_awaited()
|
||||
assert result['dispatched'] is True
|
||||
assert result['status'] == 'delivered'
|
||||
assert result['suppressed_outputs'][0]['method'] == 'send_message'
|
||||
assert captured_envelopes[0].delivery.supports_edit is False
|
||||
assert captured_envelopes[0].delivery.supports_reaction is False
|
||||
assert captured_envelopes[0].delivery.platform_capabilities['supported_apis'] == ['get_group_info']
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_malformed_agent_config_fails_one_event_and_processes_next(self):
|
||||
"""Persisted malformed Agent config cannot escape the per-event route boundary."""
|
||||
@@ -208,128 +151,6 @@ class TestEventRouteTrace:
|
||||
assert delivered['status'] == 'delivered'
|
||||
assert len(runner_calls) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_test_event_pipeline_receives_synthetic_adapter(self):
|
||||
"""Pipeline route tests enqueue queries with the no-op adapter."""
|
||||
bot = self._make_bot(
|
||||
[
|
||||
{
|
||||
'id': 'pipeline-binding',
|
||||
'enabled': True,
|
||||
'event_pattern': 'message.received',
|
||||
'target_type': 'pipeline',
|
||||
'target_uuid': 'pipeline-1',
|
||||
'priority': 0,
|
||||
'order': 0,
|
||||
}
|
||||
]
|
||||
)
|
||||
bot.ap = SimpleNamespace(
|
||||
workspace_service=active_workspace_service(),
|
||||
msg_aggregator=SimpleNamespace(add_message=AsyncMock()),
|
||||
)
|
||||
bot.adapter = SimpleNamespace(
|
||||
bot_account_id='bot-account',
|
||||
config={},
|
||||
logger=bot.logger,
|
||||
send_message=AsyncMock(),
|
||||
)
|
||||
|
||||
result = await bot.dispatch_test_event(
|
||||
'message.received',
|
||||
{'chat_id': 'user-1', 'message_text': 'hello'},
|
||||
)
|
||||
|
||||
bot.adapter.send_message.assert_not_awaited()
|
||||
bot.ap.msg_aggregator.add_message.assert_awaited_once()
|
||||
_, kwargs = bot.ap.msg_aggregator.add_message.await_args
|
||||
query_adapter = kwargs['adapter']
|
||||
assert query_adapter is not bot.adapter
|
||||
assert getattr(query_adapter, 'source') is bot.adapter
|
||||
assert result['dispatched'] is True
|
||||
assert result['status'] == 'delivered'
|
||||
assert result['suppressed_outputs'] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_test_event_reports_unmatched_route_as_failure(self):
|
||||
"""Synthetic dispatch does not report success when no saved route matches."""
|
||||
bot = self._make_bot([])
|
||||
bot.adapter = SimpleNamespace(
|
||||
bot_account_id='bot-account',
|
||||
config={},
|
||||
logger=bot.logger,
|
||||
)
|
||||
|
||||
result = await bot.dispatch_test_event(
|
||||
'message.received',
|
||||
{'chat_id': 'user-1', 'message_text': 'hello'},
|
||||
)
|
||||
|
||||
assert result['dispatched'] is False
|
||||
assert result['status'] == 'not_matched'
|
||||
assert result['failure_code'] == 'route_not_found'
|
||||
assert result['reason'] == 'No event route matched'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_synthetic_adapter_suppresses_platform_side_effect_apis(self):
|
||||
"""Synthetic adapter blocks optional platform APIs that mutate external state."""
|
||||
from langbot.pkg.platform.botmgr import SyntheticRouteTestAdapter
|
||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||
|
||||
source = SimpleNamespace(
|
||||
bot_account_id='bot-account',
|
||||
config={},
|
||||
logger=Mock(),
|
||||
get_supported_apis=Mock(
|
||||
return_value=[
|
||||
'send_message',
|
||||
'delete_message',
|
||||
'get_group_info',
|
||||
'call_platform_api',
|
||||
]
|
||||
),
|
||||
delete_message=AsyncMock(),
|
||||
call_platform_api=AsyncMock(),
|
||||
)
|
||||
adapter = SyntheticRouteTestAdapter(source)
|
||||
|
||||
await adapter.delete_message('group', 'group-1', 'message-1')
|
||||
await adapter.call_platform_api('set_title', {'name': 'New Title'})
|
||||
upload_result = await adapter.upload_file(b'data', 'test.txt')
|
||||
|
||||
source.delete_message.assert_not_awaited()
|
||||
source.call_platform_api.assert_not_awaited()
|
||||
assert upload_result == 'suppressed:test.txt'
|
||||
assert [item['method'] for item in adapter.suppressed_outputs] == [
|
||||
'delete_message',
|
||||
'call_platform_api',
|
||||
'upload_file',
|
||||
]
|
||||
assert adapter.get_supported_apis() == ['get_group_info']
|
||||
assert adapter._message_to_payload(platform_message.MessageChain([platform_message.Plain(text='ok')]))
|
||||
|
||||
def test_build_test_platform_event_message_received_uses_payload(self):
|
||||
"""Synthetic message events preserve common route filter fields."""
|
||||
from langbot.pkg.platform.botmgr import RuntimeBot
|
||||
|
||||
event = RuntimeBot._build_test_platform_event(
|
||||
'message.received',
|
||||
{
|
||||
'chat_type': 'group',
|
||||
'chat_id': 'group-1',
|
||||
'group_name': 'QA Group',
|
||||
'user_id': 'user-1',
|
||||
'user_name': 'QA User',
|
||||
'message_text': 'hello',
|
||||
},
|
||||
)
|
||||
|
||||
assert event.type == 'message.received'
|
||||
assert str(event.chat_id) == 'group-1'
|
||||
assert event.group.name == 'QA Group'
|
||||
assert event.sender.nickname == 'QA User'
|
||||
assert str(event.message_chain) == 'hello'
|
||||
|
||||
def test_agent_envelope_projects_adapter_delivery_capabilities(self):
|
||||
"""Runner delivery context reflects the active adapter's declared APIs."""
|
||||
from langbot_plugin.api.entities.builtin.platform import entities, events, message
|
||||
|
||||
Reference in New Issue
Block a user