mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-25 01:45:23 +08:00
* fix(inbound-error-state): Finish original inbound monitoring row on pipeline error (LANG-134) * fix(pipeline): finalize monitoring on early exits and cancellation Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
109 lines
4.6 KiB
Python
109 lines
4.6 KiB
Python
"""Terminal monitoring must not prevent query cleanup."""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
from langbot.pkg.pipeline.monitoring_helper import MonitoringHelper
|
|
from langbot.pkg.pipeline.pipelinemgr import RuntimePipeline
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('failure', [None, RuntimeError('stage'), asyncio.CancelledError()])
|
|
async def test_terminal_cleanup(sample_query, failure):
|
|
sample_query.variables['_monitoring_message_id'] = 'original'
|
|
service = SimpleNamespace(update_message_status=AsyncMock())
|
|
app = SimpleNamespace(
|
|
monitoring_service=service, logger=Mock(), query_pool=SimpleNamespace(remove_query=AsyncMock())
|
|
)
|
|
runtime = SimpleNamespace(
|
|
ap=app, _assert_execution_active=AsyncMock(), _process_query_body=AsyncMock(side_effect=failure)
|
|
)
|
|
if failure:
|
|
with pytest.raises(type(failure)):
|
|
await RuntimePipeline._process_query(runtime, sample_query)
|
|
else:
|
|
await RuntimePipeline._process_query(runtime, sample_query)
|
|
assert service.update_message_status.await_args.kwargs['status'] == ('error' if failure else 'success')
|
|
app.query_pool.remove_query.assert_awaited_once_with(sample_query)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('failures', [1, 3])
|
|
async def test_terminal_write_retries_are_bounded(sample_query, failures):
|
|
service = SimpleNamespace(update_message_status=AsyncMock(side_effect=[RuntimeError('db')] * failures + [None]))
|
|
app = SimpleNamespace(monitoring_service=service, logger=Mock())
|
|
await MonitoringHelper.finish_query(app, sample_query, 'original', 'error')
|
|
assert service.update_message_status.await_count == min(failures + 1, 3)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stale_placement_does_not_write(sample_query):
|
|
sample_query.variables['_monitoring_message_id'] = 'original'
|
|
service = SimpleNamespace(update_message_status=AsyncMock())
|
|
app = SimpleNamespace(
|
|
monitoring_service=service, logger=Mock(), query_pool=SimpleNamespace(remove_query=AsyncMock())
|
|
)
|
|
runtime = SimpleNamespace(
|
|
ap=app, _assert_execution_active=AsyncMock(side_effect=RuntimeError('stale')), _process_query_body=AsyncMock()
|
|
)
|
|
await RuntimePipeline._process_query(runtime, sample_query)
|
|
service.update_message_status.assert_not_awaited()
|
|
app.query_pool.remove_query.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
'veto,cancel_adapter,stage_error', [(True, False, False), (False, True, False), (False, False, True)]
|
|
)
|
|
async def test_real_body_early_exits(sample_query, veto, cancel_adapter, stage_error):
|
|
service = SimpleNamespace(
|
|
update_message_status=AsyncMock(), record_message=AsyncMock(return_value='original'), record_error=AsyncMock()
|
|
)
|
|
event = SimpleNamespace(is_prevented_default=lambda: veto, event=sample_query)
|
|
app = SimpleNamespace(
|
|
monitoring_service=service,
|
|
logger=Mock(),
|
|
query_pool=SimpleNamespace(remove_query=AsyncMock()),
|
|
plugin_connector=SimpleNamespace(emit_event=AsyncMock(return_value=event)),
|
|
)
|
|
runtime = object.__new__(RuntimePipeline)
|
|
runtime.ap = app
|
|
runtime.pipeline_entity = SimpleNamespace(uuid='pipeline')
|
|
runtime._assert_execution_active = AsyncMock()
|
|
runtime._record_lane_failure = Mock()
|
|
runtime._execute_from_stage = AsyncMock(side_effect=RuntimeError('stage') if stage_error else None)
|
|
sample_query.pipeline_config = {}
|
|
sample_query.adapter.on_monitoring_message_created = AsyncMock(
|
|
side_effect=asyncio.CancelledError() if cancel_adapter else None
|
|
)
|
|
if cancel_adapter:
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await runtime._process_query(sample_query)
|
|
else:
|
|
await runtime._process_query(sample_query)
|
|
assert service.update_message_status.await_args.kwargs['status'] == (
|
|
'error' if cancel_adapter or stage_error else 'success'
|
|
)
|
|
app.query_pool.remove_query.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_detail_survives_status_write_failure(sample_query):
|
|
sample_query.variables['_monitoring_message_id'] = 'original'
|
|
service = SimpleNamespace(
|
|
update_message_status=AsyncMock(side_effect=RuntimeError('db')),
|
|
record_message=AsyncMock(return_value='detail'),
|
|
record_error=AsyncMock(),
|
|
)
|
|
app = SimpleNamespace(monitoring_service=service, logger=Mock())
|
|
assert (
|
|
await MonitoringHelper.record_query_error(
|
|
app, sample_query, 'bot', 'bot', 'pipeline', 'pipeline', ValueError('failed')
|
|
)
|
|
== 'detail'
|
|
)
|
|
service.record_error.assert_awaited_once()
|