diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index 069e07da3..92ea36fa1 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -22,6 +22,7 @@ class BotService: FAILURE_PROCESSOR_NOT_FOUND = 'processor_not_found' FAILURE_PROCESSOR_INCOMPATIBLE = 'processor_incompatible' FAILURE_INVALID_EVENT = 'invalid_event' + FAILURE_BOT_RUNTIME_UNAVAILABLE = 'bot_runtime_unavailable' ROUTE_TRACE_KIND = 'event_route_trace' BOT_FIELDS = { @@ -846,7 +847,14 @@ class BotService: raise WorkspaceNotFoundError('Bot not found') runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(context, bot_uuid) if runtime_bot is None: - raise Exception('Bot not found') + return { + 'dispatched': False, + 'event_type': event_type, + 'failure_code': self.FAILURE_BOT_RUNTIME_UNAVAILABLE, + 'reason': 'Bot runtime is unavailable', + 'suppressed_outputs': [], + 'route_status': await self.list_event_route_statuses(context, bot_uuid), + } dispatch_result = await runtime_bot.dispatch_test_event(event_type, payload or {}) route_status = await self.list_event_route_statuses(context, bot_uuid) diff --git a/tests/unit_tests/api/service/test_bot_service.py b/tests/unit_tests/api/service/test_bot_service.py index 32d4ee7cb..f8a87c196 100644 --- a/tests/unit_tests/api/service/test_bot_service.py +++ b/tests/unit_tests/api/service/test_bot_service.py @@ -631,6 +631,46 @@ 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.""" diff --git a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx index 842074576..54c079b1e 100644 --- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx @@ -964,6 +964,7 @@ function RouteDryRunDialog({ async function dispatchTestEvent() { setRunError(null); + setResult(null); setDispatchResult(null); const payload = parsePayload(); diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index ba0688800..70f756ed8 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -466,10 +466,12 @@ const enUS = { 'The selected processor cannot handle this event.', processor_not_found: 'The selected processor is unavailable.', processor_disabled: 'The selected processor is disabled.', + bot_runtime_unavailable: + 'The bot is not running. Check its platform settings and enable it before running a full test.', runner_failed: 'The Agent runner failed while processing the event.', delivery_failed: 'The processor finished, but delivery failed.', }, - routeTestAction: 'Run saved configuration', + routeTestAction: 'Run full test', routeTestRunning: 'Running…', routeTestFailed: 'Failed to run the saved route. Try again later.', routeTestDispatched: diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index f90fd0ce0..ea278cbb8 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -473,10 +473,12 @@ const jaJP = { '選択したプロセッサーはこのイベントを処理できません。', processor_not_found: '選択したプロセッサーを利用できません。', processor_disabled: '選択したプロセッサーは無効です。', + bot_runtime_unavailable: + 'ボットが実行されていません。プラットフォーム設定を確認してボットを有効にした後、完全テストを実行してください。', runner_failed: 'Agent Runner がイベント処理中に失敗しました。', delivery_failed: '処理は完了しましたが、結果の配信に失敗しました。', }, - routeTestAction: '保存済み設定を実行', + routeTestAction: '完全テストを実行', routeTestRunning: '実行中…', routeTestFailed: '保存済みルートの実行に失敗しました。後でもう一度お試しください。', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index af14bd267..a178737b0 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -444,10 +444,12 @@ const zhHans = { processor_incompatible: '所选处理器无法处理此事件。', processor_not_found: '所选处理器不可用。', processor_disabled: '所选处理器已禁用。', + bot_runtime_unavailable: + '机器人尚未运行。请检查平台配置并启用机器人,再运行完整测试。', runner_failed: 'Agent Runner 处理事件时失败。', delivery_failed: '处理器已完成,但结果投递失败。', }, - routeTestAction: '运行已保存配置', + routeTestAction: '运行完整测试', routeTestRunning: '运行中…', routeTestFailed: '运行已保存路由失败,请稍后重试。', routeTestDispatched: '已保存路由运行成功,{{count}} 个平台操作已被阻止。', diff --git a/web/tests/e2e/crud-smoke.spec.ts b/web/tests/e2e/crud-smoke.spec.ts index 9e434f3c8..d3546a287 100644 --- a/web/tests/e2e/crud-smoke.spec.ts +++ b/web/tests/e2e/crud-smoke.spec.ts @@ -346,6 +346,53 @@ test.describe('bot advanced flows', () => { body: JSON.stringify({ code: -1, msg: 'Internal server error' }), }), ); + await page.route( + '**/api/v1/platform/bots/*/event-routes/dry-run', + (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + msg: 'ok', + data: { + matched: true, + event_type: 'message.received', + matched_binding_id: 'binding-1', + matched_binding_index: 0, + target: { + target_type: 'agent', + target_uuid: 'agent-1', + target_name: 'NewAgent', + }, + diagnostic_steps: ['Matched route 1'], + diagnostic_details: [], + }, + }), + }), + ); + await page.route('**/api/v1/platform/bots/*/event-routes/test', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + msg: 'ok', + data: { + 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: [], + }, + }, + }), + }), + ); await page.goto('/home/bots?id=new'); await selectPlaywrightAdapter(page); @@ -397,8 +444,28 @@ test.describe('bot advanced flows', () => { routeDialog.getByRole('button', { name: 'Preview match' }), ).toBeVisible(); await expect( - routeDialog.getByRole('button', { name: 'Run saved configuration' }), + routeDialog.getByRole('button', { name: 'Run full test' }), ).toBeVisible(); + + await routeDialog.getByRole('button', { name: 'Preview match' }).click(); + await expect(routeDialog.getByText('Matched route')).toBeVisible(); + + await routeDialog.getByRole('button', { name: 'Run full test' }).click(); + await expect(routeDialog.getByText('Matched route')).toHaveCount(0); + await expect( + routeDialog.getByText( + 'The bot is not running. Check its platform settings and enable it before running a full test.', + ), + ).toBeVisible(); + await expect(routeDialog.getByText('Internal server error')).toHaveCount(0); + + await routeDialog.getByRole('button', { name: 'Preview match' }).click(); + await expect( + routeDialog.getByText( + 'The bot is not running. Check its platform settings and enable it before running a full test.', + ), + ).toHaveCount(0); + await expect(routeDialog.getByText('Matched route')).toBeVisible(); const dialogBox = await routeDialog.boundingBox(); expect(dialogBox).not.toBeNull(); expect(dialogBox!.height).toBeLessThan(500);