mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-21 20:06:06 +00:00
feat(agent-platform): show bot route runtime status
This commit is contained in:
@@ -997,6 +997,134 @@ class TestBotServiceListEventLogs:
|
||||
assert total == 5
|
||||
|
||||
|
||||
class TestBotServiceEventRouteStatuses:
|
||||
"""Tests for event route runtime status aggregation."""
|
||||
|
||||
async def test_list_event_route_statuses_bot_not_found_raises(self):
|
||||
"""Raises Exception when runtime bot not found."""
|
||||
ap = SimpleNamespace()
|
||||
ap.platform_mgr = SimpleNamespace()
|
||||
ap.platform_mgr.get_bot_by_uuid = AsyncMock(return_value=None)
|
||||
|
||||
service = BotService(ap)
|
||||
|
||||
with pytest.raises(Exception, match='Bot not found'):
|
||||
await service.list_event_route_statuses('missing-bot')
|
||||
|
||||
async def test_list_event_route_statuses_merges_latest_trace_by_binding(self):
|
||||
"""Current route definitions are enriched with the latest trace log."""
|
||||
ap = SimpleNamespace()
|
||||
ap.platform_mgr = SimpleNamespace()
|
||||
|
||||
runtime_bot = SimpleNamespace()
|
||||
runtime_bot.bot_entity = SimpleNamespace(
|
||||
event_bindings=[
|
||||
{
|
||||
'id': 'binding-1',
|
||||
'event_pattern': 'platform.member.joined',
|
||||
'target_type': 'agent',
|
||||
'target_uuid': 'agent-1',
|
||||
'enabled': True,
|
||||
'order': 0,
|
||||
},
|
||||
{
|
||||
'id': 'binding-2',
|
||||
'event_pattern': 'message.received',
|
||||
'target_type': 'pipeline',
|
||||
'target_uuid': 'pipeline-1',
|
||||
'enabled': False,
|
||||
'order': 1,
|
||||
},
|
||||
]
|
||||
)
|
||||
runtime_bot.logger = SimpleNamespace(
|
||||
logs=[
|
||||
SimpleNamespace(
|
||||
to_json=Mock(
|
||||
return_value={
|
||||
'seq_id': 1,
|
||||
'timestamp': 100,
|
||||
'level': 'info',
|
||||
'text': 'old matched',
|
||||
'metadata': {
|
||||
'kind': 'event_route_trace',
|
||||
'binding_id': 'binding-1',
|
||||
'event_pattern': 'platform.member.joined',
|
||||
'event_type': 'platform.member.joined',
|
||||
'target_type': 'agent',
|
||||
'target_uuid': 'agent-1',
|
||||
'status': 'matched',
|
||||
'failure_code': None,
|
||||
'reason': 'matched',
|
||||
'run_id': None,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
SimpleNamespace(
|
||||
to_json=Mock(
|
||||
return_value={
|
||||
'seq_id': 2,
|
||||
'timestamp': 120,
|
||||
'level': 'error',
|
||||
'text': 'runner failed',
|
||||
'metadata': {
|
||||
'kind': 'event_route_trace',
|
||||
'binding_id': 'binding-1',
|
||||
'event_pattern': 'platform.member.joined',
|
||||
'event_type': 'platform.member.joined',
|
||||
'target_type': 'agent',
|
||||
'target_uuid': 'agent-1',
|
||||
'status': 'failed',
|
||||
'failure_code': 'runner_failed',
|
||||
'reason': 'Agent runner failed',
|
||||
'run_id': None,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
SimpleNamespace(
|
||||
to_json=Mock(
|
||||
return_value={
|
||||
'seq_id': 3,
|
||||
'timestamp': 130,
|
||||
'level': 'info',
|
||||
'text': 'no route',
|
||||
'metadata': {
|
||||
'kind': 'event_route_trace',
|
||||
'binding_id': None,
|
||||
'event_pattern': None,
|
||||
'event_type': 'platform.member.left',
|
||||
'target_type': None,
|
||||
'target_uuid': '',
|
||||
'status': 'not_matched',
|
||||
'failure_code': 'route_not_found',
|
||||
'reason': 'No event route matched',
|
||||
'run_id': None,
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
ap.platform_mgr.get_bot_by_uuid = AsyncMock(return_value=runtime_bot)
|
||||
|
||||
service = BotService(ap)
|
||||
result = await service.list_event_route_statuses('bot-1')
|
||||
|
||||
assert len(result['routes']) == 2
|
||||
assert result['routes'][0]['binding_id'] == 'binding-1'
|
||||
assert result['routes'][0]['last_status'] == 'failed'
|
||||
assert result['routes'][0]['failure_code'] == 'runner_failed'
|
||||
assert result['routes'][0]['timestamp'] == 120
|
||||
assert result['routes'][0]['current'] is True
|
||||
assert result['routes'][1]['binding_id'] == 'binding-2'
|
||||
assert result['routes'][1]['last_status'] is None
|
||||
assert result['routes'][1]['enabled'] is False
|
||||
assert result['unmatched_events'][0]['event_type'] == 'platform.member.left'
|
||||
assert result['unmatched_events'][0]['failure_code'] == 'route_not_found'
|
||||
|
||||
|
||||
class TestBotServiceSendMessage:
|
||||
"""Tests for send_message method."""
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ RuntimeBot.resolve_pipeline_uuid and _match_operator unit tests
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestMatchOperator:
|
||||
@@ -54,6 +56,90 @@ class TestMatchOperator:
|
||||
assert cls._match_operator('hello', 'unknown_op', 'hello') is False
|
||||
|
||||
|
||||
class TestEventRouteTrace:
|
||||
"""Test structured event route trace logging."""
|
||||
|
||||
@staticmethod
|
||||
def _make_bot(event_bindings: list[dict]):
|
||||
from langbot.pkg.platform.botmgr import RuntimeBot
|
||||
|
||||
bot = object.__new__(RuntimeBot)
|
||||
bot.bot_entity = SimpleNamespace(uuid='bot-1', event_bindings=event_bindings)
|
||||
bot.logger = SimpleNamespace(
|
||||
info=AsyncMock(),
|
||||
warning=AsyncMock(),
|
||||
error=AsyncMock(),
|
||||
)
|
||||
return bot
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_no_matching_route_records_trace(self):
|
||||
"""A route miss is visible as structured route trace metadata."""
|
||||
bot = self._make_bot([])
|
||||
|
||||
await bot._dispatch_eba_event_to_agent(SimpleNamespace(type='platform.member.joined'), Mock())
|
||||
|
||||
bot.logger.info.assert_awaited_once()
|
||||
_, kwargs = bot.logger.info.await_args
|
||||
metadata = kwargs['metadata']
|
||||
assert metadata['kind'] == 'event_route_trace'
|
||||
assert metadata['event_type'] == 'platform.member.joined'
|
||||
assert metadata['status'] == 'not_matched'
|
||||
assert metadata['failure_code'] == 'route_not_found'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_event_route_trace_includes_binding_and_target(self):
|
||||
"""Trace metadata preserves binding and processor identifiers."""
|
||||
bot = self._make_bot([])
|
||||
|
||||
await bot._record_event_route_trace(
|
||||
event_type='platform.member.joined',
|
||||
status='failed',
|
||||
level='warning',
|
||||
binding={
|
||||
'id': 'binding-1',
|
||||
'event_pattern': 'platform.member.*',
|
||||
'target_type': 'agent',
|
||||
'target_uuid': 'agent-1',
|
||||
},
|
||||
failure_code='processor_disabled',
|
||||
reason='Agent target is disabled',
|
||||
text='disabled',
|
||||
)
|
||||
|
||||
bot.logger.warning.assert_awaited_once()
|
||||
_, kwargs = bot.logger.warning.await_args
|
||||
metadata = kwargs['metadata']
|
||||
assert metadata['binding_id'] == 'binding-1'
|
||||
assert metadata['event_pattern'] == 'platform.member.*'
|
||||
assert metadata['target_type'] == 'agent'
|
||||
assert metadata['target_uuid'] == 'agent-1'
|
||||
assert metadata['status'] == 'failed'
|
||||
|
||||
|
||||
class TestEventLoggerMetadata:
|
||||
"""Test platform EventLogger metadata compatibility."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_is_serialized_without_breaking_no_throw_position(self):
|
||||
"""Metadata is optional and no_throw remains the fourth positional argument."""
|
||||
from langbot.pkg.platform.logger import EventLogger
|
||||
|
||||
logger = EventLogger(name='test', ap=SimpleNamespace())
|
||||
|
||||
await logger.info('plain log', None, None, False)
|
||||
await logger.info(
|
||||
'route trace',
|
||||
metadata={'kind': 'event_route_trace', 'status': 'matched'},
|
||||
)
|
||||
|
||||
assert logger.logs[0].to_json()['metadata'] is None
|
||||
assert logger.logs[1].to_json()['metadata'] == {
|
||||
'kind': 'event_route_trace',
|
||||
'status': 'matched',
|
||||
}
|
||||
|
||||
|
||||
class TestResolvePipelineUuid:
|
||||
"""Test the resolve_pipeline_uuid method."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user