From 600a1739182495d03490ff30c0d164a34e031371 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 26 Aug 2026 14:28:30 +0800 Subject: [PATCH] refactor(bots): remove route execution test --- .../e2e/bot-event-routing-product-flow.mjs | 21 +- skills/skills/langbot-mcp-ops/SKILL.md | 8 +- .../http/controller/groups/platform/bots.py | 28 +- src/langbot/pkg/api/http/service/bot.py | 63 ---- src/langbot/pkg/api/mcp/server.py | 19 - src/langbot/pkg/platform/botmgr.py | 342 +----------------- tests/integration/api/test_bots.py | 41 --- tests/manual/mcp_smoke.py | 28 +- .../api/service/test_bot_service.py | 40 -- tests/unit_tests/api/test_mcp_server.py | 39 +- .../unit_tests/platform/test_routing_rules.py | 179 --------- .../bot-form/EventBindingsEditor.tsx | 82 +---- web/src/app/infra/entities/api/index.ts | 16 - web/src/app/infra/http/BackendClient.ts | 12 - web/src/i18n/locales/en-US.ts | 25 +- web/src/i18n/locales/ja-JP.ts | 27 +- web/src/i18n/locales/zh-Hans.ts | 24 +- web/tests/e2e/crud-smoke.spec.ts | 51 +-- 18 files changed, 36 insertions(+), 1009 deletions(-) diff --git a/skills/scripts/e2e/bot-event-routing-product-flow.mjs b/skills/scripts/e2e/bot-event-routing-product-flow.mjs index c0032de83..e200e361a 100644 --- a/skills/scripts/e2e/bot-event-routing-product-flow.mjs +++ b/skills/scripts/e2e/bot-event-routing-product-flow.mjs @@ -216,11 +216,11 @@ try { ); await page - .getByRole("button", { name: /Test route|测试路由|ルートをテスト/ }) + .getByRole("button", { name: /Check route|检查路由|ルートを確認/ }) .click(); await page.getByRole("dialog").waitFor(); await page - .getByRole("button", { name: /Preview route|预览路由|ルートをプレビュー/ }) + .getByRole("button", { name: /View match|查看匹配结果|一致結果を確認/ }) .click(); await page .getByText(/Route matched|已命中路由|ルートに一致しました/) @@ -231,28 +231,11 @@ try { .waitFor(); result.visible_signals.push("dry-run-matched", "discard-target"); - await page - .getByRole("button", { - name: /Run saved route|运行已保存路由|保存済みルートを実行/, - }) - .click(); - await page - .getByText( - /saved route ran successfully|已保存路由运行成功|保存済みルートを実行しました/, - ) - .waitFor({ timeout: 20_000 }); - result.visible_signals.push("test-event-dispatched"); - await page .getByRole("button", { name: /Close|关闭|閉じる/ }) .first() .click(); await page.getByRole("dialog").waitFor({ state: "hidden" }); - await page - .getByText(/Discarded|已丢弃|破棄済み/) - .first() - .waitFor({ timeout: 10_000 }); - result.visible_signals.push("route-status-discarded"); const text = await bodyText(page); if (/\bEBA event\b/.test(text)) { diff --git a/skills/skills/langbot-mcp-ops/SKILL.md b/skills/skills/langbot-mcp-ops/SKILL.md index 3c40f7963..18c651191 100644 --- a/skills/skills/langbot-mcp-ops/SKILL.md +++ b/skills/skills/langbot-mcp-ops/SKILL.md @@ -64,7 +64,7 @@ The tools wrap the LangBot service layer. Current tools (v1): | --- | --- | | `get_system_info` | Version, edition, instance id | | `list_bots` / `get_bot` / `create_bot` / `update_bot` / `delete_bot` | Manage messaging-platform bots (secrets redacted on read) | -| `list_bot_event_route_statuses` / `test_bot_event_route` | Inspect bot event-route runtime status and dispatch a synthetic test event through saved routes without sending real outbound platform messages | +| `list_bot_event_route_statuses` | Inspect bot event-route runtime status | | `list_processors` / `get_processor` / `create_processor` / `update_processor` / `delete_processor` | Manage the peer Agent and Pipeline processor types | | `list_pipelines` / `get_pipeline` / `create_pipeline` / `update_pipeline` / `delete_pipeline` | Manage pipelines | | `list_llm_models` / `get_llm_model` / `list_embedding_models` / `list_model_providers` | Inspect models & providers | @@ -78,12 +78,6 @@ shape as the corresponding HTTP API request body. Discover resources with the `resource.view`; mutations require `resource.manage`. All service calls inherit the immutable Workspace context authenticated at the MCP transport boundary. -`test_bot_event_route` uses the bot's saved runtime route table, injects a -synthetic event such as `message.received`, and suppresses platform delivery. -It still executes the selected processor, so tools and external services may -have side effects. Use `payload` for sample event fields, for example -`{"message_text": "hello", "chat_type": "private", "chat_id": "u1"}`. - ## How to use 1. Get an API key (web UI key, or set `api.global_api_key` in config.yaml). diff --git a/src/langbot/pkg/api/http/controller/groups/platform/bots.py b/src/langbot/pkg/api/http/controller/groups/platform/bots.py index 05a150d1a..6aaff3ca0 100644 --- a/src/langbot/pkg/api/http/controller/groups/platform/bots.py +++ b/src/langbot/pkg/api/http/controller/groups/platform/bots.py @@ -90,15 +90,9 @@ class BotsRouterGroup(group.RouterGroup): permission=Permission.RESOURCE_VIEW, ) async def _(bot_uuid: str, request_context: RequestContext) -> str: - return self.success( - data=await self.ap.bot_service.list_event_route_statuses( - request_context, bot_uuid - ) - ) + return self.success(data=await self.ap.bot_service.list_event_route_statuses(request_context, bot_uuid)) - async def _dry_run_event_route( - bot_uuid: str, request_context: RequestContext - ) -> str: + async def _dry_run_event_route(bot_uuid: str, request_context: RequestContext) -> str: json_data = await quart.request.json if not isinstance(json_data, dict): return self.http_status(400, -1, 'invalid request body') @@ -128,24 +122,6 @@ class BotsRouterGroup(group.RouterGroup): permission=Permission.RESOURCE_VIEW, )(_dry_run_event_route) - @self.route( - '//event-routes/test', - methods=['POST'], - auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, - permission=Permission.RUNTIME_OPERATE, - ) - async def _(bot_uuid: str, request_context: RequestContext) -> str: - json_data = await quart.request.json - if not isinstance(json_data, dict): - return self.http_status(400, -1, 'invalid request body') - result = await self.ap.bot_service.dispatch_test_event_route( - request_context, - bot_uuid=bot_uuid, - event_type=json_data.get('event_type'), - payload=json_data.get('event_data', json_data.get('payload')), - ) - return self.success(data=result) - @self.route( '//send_message', methods=['POST'], diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index a187085ab..8b4a8ece6 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -21,7 +21,6 @@ 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 = { @@ -798,68 +797,6 @@ class BotService: 'stale_routes': stale_routes, } - async def dispatch_test_event_route( - self, - context: TenantContext, - bot_uuid: str, - event_type: str, - payload: dict[str, typing.Any] | None = None, - ) -> dict[str, typing.Any]: - """Dispatch a synthetic event through the saved Bot runtime route configuration.""" - event_type = str(event_type or '').strip() - if not event_type: - return { - 'dispatched': False, - 'event_type': '', - 'failure_code': self.FAILURE_INVALID_EVENT, - 'reason': 'event_type is required', - 'suppressed_outputs': [], - 'route_status': { - 'routes': [], - 'unmatched_events': [], - 'stale_routes': [], - }, - } - if payload is not None and not isinstance(payload, dict): - return { - 'dispatched': False, - 'event_type': event_type, - 'failure_code': self.FAILURE_INVALID_EVENT, - 'reason': 'payload must be an object', - 'suppressed_outputs': [], - 'route_status': { - 'routes': [], - 'unmatched_events': [], - 'stale_routes': [], - }, - } - - if await self.get_bot(context, bot_uuid, include_secret=False) is None: - raise WorkspaceNotFoundError('Bot not found') - runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(context, bot_uuid) - if runtime_bot is None: - 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) - return { - 'dispatched': bool(dispatch_result.get('dispatched')), - 'event_type': event_type, - 'status': dispatch_result.get('status'), - 'binding_id': dispatch_result.get('binding_id'), - 'failure_code': dispatch_result.get('failure_code'), - 'reason': dispatch_result.get('reason'), - 'suppressed_outputs': dispatch_result.get('suppressed_outputs', []), - 'route_status': route_status, - } - async def send_message( self, context: TenantContext, diff --git a/src/langbot/pkg/api/mcp/server.py b/src/langbot/pkg/api/mcp/server.py index 9c39d7ceb..34a8ab5ce 100644 --- a/src/langbot/pkg/api/mcp/server.py +++ b/src/langbot/pkg/api/mcp/server.py @@ -132,25 +132,6 @@ class LangBotMCPServer: async def list_bot_event_route_statuses(bot_uuid: str) -> str: return _dump(await ap.bot_service.list_event_route_statuses(bot_uuid)) - @mcp.tool( - description=( - 'Dispatch a synthetic event through the saved bot event routes. ' - 'This validates routing without sending real outbound platform messages.' - ) - ) - async def test_bot_event_route( - bot_uuid: str, - event_type: str, - payload: dict | None = None, - ) -> str: - return _dump( - await ap.bot_service.dispatch_test_event_route( - bot_uuid=bot_uuid, - event_type=event_type, - payload=payload, - ) - ) - # ----- Pipelines ----------------------------------------------- # @mcp.tool(description='List all pipelines.') async def list_pipelines() -> str: diff --git a/src/langbot/pkg/platform/botmgr.py b/src/langbot/pkg/platform/botmgr.py index c03fc1ac2..1e7373470 100644 --- a/src/langbot/pkg/platform/botmgr.py +++ b/src/langbot/pkg/platform/botmgr.py @@ -51,226 +51,6 @@ from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext -class SyntheticRouteTestAdapter: - """Adapter wrapper that suppresses outbound platform delivery for test events.""" - - SIDE_EFFECT_API_NAMES = { - 'send_message', - 'reply_message', - 'reply_message_chunk', - 'create_message_card', - 'edit_message', - 'delete_message', - 'add_reaction', - 'remove_reaction', - 'forward_message', - 'set_group_name', - 'mute_member', - 'unmute_member', - 'kick_member', - 'leave_group', - 'approve_friend_request', - 'approve_group_invite', - 'upload_file', - 'call_platform_api', - } - - def __init__(self, source: abstract_platform_adapter.AbstractMessagePlatformAdapter): - self.source = source - self.bot_account_id = getattr(source, 'bot_account_id', '') - self.config = getattr(source, 'config', {}) - self.logger = getattr(source, 'logger', None) - self.suppressed_outputs: list[dict[str, typing.Any]] = [] - - @staticmethod - def _message_to_payload(message: platform_message.MessageChain) -> typing.Any: - return message.model_dump() if hasattr(message, 'model_dump') else str(message) - - def _suppress(self, method: str, **payload: typing.Any) -> None: - self.suppressed_outputs.append({'method': method, **payload}) - - def __getattr__(self, name: str) -> typing.Any: - return getattr(self.source, name) - - def get_supported_apis(self) -> list[str]: - get_supported_apis = getattr(self.source, 'get_supported_apis', None) - if not callable(get_supported_apis): - return [] - return [api_name for api_name in get_supported_apis() if api_name not in self.SIDE_EFFECT_API_NAMES] - - async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain, - ) -> dict[str, typing.Any]: - self._suppress( - 'send_message', - target_type=target_type, - target_id=target_id, - message=self._message_to_payload(message), - ) - return {'suppressed': True} - - async def reply_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False, - ) -> dict[str, typing.Any]: - self._suppress( - 'reply_message', - message=self._message_to_payload(message), - quote_origin=quote_origin, - ) - return {'suppressed': True} - - async def reply_message_chunk( - self, - message_source: platform_events.MessageEvent, - bot_message: dict, - message: platform_message.MessageChain, - quote_origin: bool = False, - is_final: bool = False, - ) -> dict[str, typing.Any]: - self._suppress( - 'reply_message_chunk', - message=self._message_to_payload(message), - quote_origin=quote_origin, - is_final=is_final, - ) - return {'suppressed': True} - - async def create_message_card( - self, - message_id: str | int, - event: platform_events.MessageEvent, - ) -> bool: - self._suppress('create_message_card', message_id=str(message_id)) - return False - - async def is_stream_output_supported(self) -> bool: - return False - - async def edit_message( - self, - chat_type: str, - chat_id: typing.Union[int, str], - message_id: typing.Union[int, str], - new_content: platform_message.MessageChain, - ) -> None: - self._suppress( - 'edit_message', - chat_type=str(chat_type), - chat_id=str(chat_id), - message_id=str(message_id), - new_content=self._message_to_payload(new_content), - ) - - async def delete_message( - self, - chat_type: str, - chat_id: typing.Union[int, str], - message_id: typing.Union[int, str], - ) -> None: - self._suppress( - 'delete_message', - chat_type=str(chat_type), - chat_id=str(chat_id), - message_id=str(message_id), - ) - - async def forward_message( - self, - from_chat_type: str, - from_chat_id: typing.Union[int, str], - message_id: typing.Union[int, str], - to_chat_type: str, - to_chat_id: typing.Union[int, str], - ) -> platform_events.MessageResult: - self._suppress( - 'forward_message', - from_chat_type=str(from_chat_type), - from_chat_id=str(from_chat_id), - message_id=str(message_id), - to_chat_type=str(to_chat_type), - to_chat_id=str(to_chat_id), - ) - return platform_events.MessageResult(raw={'suppressed': True}) - - async def set_group_name( - self, - group_id: typing.Union[int, str], - name: str, - ) -> None: - self._suppress('set_group_name', group_id=str(group_id), name=name) - - async def mute_member( - self, - group_id: typing.Union[int, str], - user_id: typing.Union[int, str], - duration: int = 0, - ) -> None: - self._suppress( - 'mute_member', - group_id=str(group_id), - user_id=str(user_id), - duration=duration, - ) - - async def unmute_member( - self, - group_id: typing.Union[int, str], - user_id: typing.Union[int, str], - ) -> None: - self._suppress('unmute_member', group_id=str(group_id), user_id=str(user_id)) - - async def kick_member( - self, - group_id: typing.Union[int, str], - user_id: typing.Union[int, str], - ) -> None: - self._suppress('kick_member', group_id=str(group_id), user_id=str(user_id)) - - async def leave_group( - self, - group_id: typing.Union[int, str], - ) -> None: - self._suppress('leave_group', group_id=str(group_id)) - - async def approve_friend_request( - self, - request_id: typing.Union[int, str], - approve: bool = True, - remark: str | None = None, - ) -> None: - self._suppress( - 'approve_friend_request', - request_id=str(request_id), - approve=approve, - remark=remark, - ) - - async def approve_group_invite( - self, - request_id: typing.Union[int, str], - approve: bool = True, - ) -> None: - self._suppress( - 'approve_group_invite', - request_id=str(request_id), - approve=approve, - ) - - async def upload_file(self, file_data: bytes, filename: str) -> str: - self._suppress('upload_file', filename=filename, size=len(file_data)) - return f'suppressed:{filename}' - - async def call_platform_api(self, action: str, params: dict | None = None) -> dict: - self._suppress('call_platform_api', action=action, params=params or {}) - return {'suppressed': True} - - class RuntimeBot: """运行时机器人""" @@ -568,126 +348,6 @@ class RuntimeBot: """Return the selected event binding plus per-binding diagnostic steps.""" return self._evaluate_eba_event_bindings(self._get_event_bindings(), event, event_type) - @staticmethod - def _build_test_platform_event( - event_type: str, - payload: dict[str, typing.Any] | None = None, - ) -> platform_events.EBAEvent: - """Build a synthetic platform event for route validation.""" - payload = payload or {} - now = time.time() - common = { - 'type': event_type, - 'timestamp': payload.get('timestamp') or now, - 'adapter_name': payload.get('adapter_name') or 'test-event', - 'source_platform_object': {'synthetic': True, 'payload': payload}, - } - - user_id = str(payload.get('user_id') or payload.get('sender_id') or 'test-user') - user_name = str(payload.get('user_name') or payload.get('sender_name') or 'Test User') - group_id = str(payload.get('group_id') or payload.get('chat_id') or 'test-group') - group_name = str(payload.get('group_name') or 'Test Group') - - if event_type == 'message.received': - chat_type_value = str(payload.get('chat_type') or 'private') - chat_type = ( - platform_entities.ChatType.GROUP - if chat_type_value == platform_entities.ChatType.GROUP.value - else platform_entities.ChatType.PRIVATE - ) - chat_id = str( - payload.get('chat_id') or (group_id if chat_type == platform_entities.ChatType.GROUP else user_id) - ) - message_text = str(payload.get('message_text') or payload.get('text') or '') - message_chain_data = payload.get('message_chain') - if message_chain_data is None: - message_chain = platform_message.MessageChain([platform_message.Plain(text=message_text)]) - else: - message_chain = platform_message.MessageChain.model_validate(message_chain_data) - group = ( - platform_entities.UserGroup(id=chat_id, name=group_name) - if chat_type == platform_entities.ChatType.GROUP - else None - ) - return platform_events.MessageReceivedEvent( - **common, - message_id=str(payload.get('message_id') or f'test-message:{uuid.uuid4()}'), - message_chain=message_chain, - sender=platform_entities.User(id=user_id, nickname=user_name), - chat_type=chat_type, - chat_id=chat_id, - group=group, - ) - - if event_type == 'group.member_joined': - return platform_events.MemberJoinedEvent( - **common, - group=platform_entities.UserGroup(id=group_id, name=group_name), - member=platform_entities.User(id=user_id, nickname=user_name), - inviter=platform_entities.User( - id=str(payload.get('inviter_id')), - nickname=str(payload.get('inviter_name') or ''), - ) - if payload.get('inviter_id') - else None, - join_type=payload.get('join_type'), - ) - - if event_type == 'group.member_left': - return platform_events.MemberLeftEvent( - **common, - group=platform_entities.UserGroup(id=group_id, name=group_name), - member=platform_entities.User(id=user_id, nickname=user_name), - is_kicked=bool(payload.get('is_kicked', False)), - operator=platform_entities.User( - id=str(payload.get('operator_id')), - nickname=str(payload.get('operator_name') or ''), - ) - if payload.get('operator_id') - else None, - ) - - if event_type == 'platform.specific': - return platform_events.PlatformSpecificEvent( - **common, - action=str(payload.get('action') or 'test'), - data=payload.get('data') if isinstance(payload.get('data'), dict) else payload, - ) - - return platform_events.EBAEvent(**common) - - async def dispatch_test_event( - self, - event_type: str, - payload: dict[str, typing.Any] | None = None, - ) -> dict[str, typing.Any]: - """Dispatch a synthetic event through the real runtime route path.""" - event_type = str(event_type or '').strip() - if not event_type: - raise ValueError('event_type is required') - - event = self._build_test_platform_event(event_type, payload) - await self._record_event_route_trace( - event_type=event_type, - status='test_started', - reason='Synthetic test event dispatched from control plane', - text=f'Test event {event_type} dispatched from control plane', - ) - test_adapter = SyntheticRouteTestAdapter(self.adapter) - outcome = await self._dispatch_eba_event_to_processor( - event, - typing.cast(abstract_platform_adapter.AbstractMessagePlatformAdapter, test_adapter), - ) - return { - 'event_type': event_type, - 'dispatched': outcome['status'] in {'delivered', 'discarded'}, - 'status': outcome['status'], - 'binding_id': outcome.get('binding_id'), - 'failure_code': outcome.get('failure_code'), - 'reason': outcome.get('reason'), - 'suppressed_outputs': test_adapter.suppressed_outputs, - } - async def _record_event_route_trace( self, *, @@ -1170,7 +830,7 @@ class RuntimeBot: self, envelope: AgentEventEnvelope, outputs: list[provider_message.Message | provider_message.MessageChunk], - adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter | SyntheticRouteTestAdapter | None = None, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter | None = None, ) -> None: if not outputs or not envelope.delivery.reply_target: return diff --git a/tests/integration/api/test_bots.py b/tests/integration/api/test_bots.py index e218a56b2..3e3a3a871 100644 --- a/tests/integration/api/test_bots.py +++ b/tests/integration/api/test_bots.py @@ -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.""" diff --git a/tests/manual/mcp_smoke.py b/tests/manual/mcp_smoke.py index 931d7c801..679e52389 100644 --- a/tests/manual/mcp_smoke.py +++ b/tests/manual/mcp_smoke.py @@ -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) diff --git a/tests/unit_tests/api/service/test_bot_service.py b/tests/unit_tests/api/service/test_bot_service.py index 03dd322bd..b6f1c9c36 100644 --- a/tests/unit_tests/api/service/test_bot_service.py +++ b/tests/unit_tests/api/service/test_bot_service.py @@ -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.""" diff --git a/tests/unit_tests/api/test_mcp_server.py b/tests/unit_tests/api/test_mcp_server.py index f232113d9..228b23ffa 100644 --- a/tests/unit_tests/api/test_mcp_server.py +++ b/tests/unit_tests/api/test_mcp_server.py @@ -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' diff --git a/tests/unit_tests/platform/test_routing_rules.py b/tests/unit_tests/platform/test_routing_rules.py index ece6fdc75..7cda0ae66 100644 --- a/tests/unit_tests/platform/test_routing_rules.py +++ b/tests/unit_tests/platform/test_routing_rules.py @@ -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 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 cf11290b4..0b5b1a49f 100644 --- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx @@ -112,7 +112,6 @@ import { Agent, BotRouteDryRunResult, BotEventRouteStatus, - BotRouteTestResult, } from '@/app/infra/entities/api'; import { backendClient } from '@/app/infra/http'; import { @@ -885,13 +884,11 @@ function RouteDryRunDialog({ bindings, eventOptions, agentOptions, - onRouteStatusUpdate, }: { botId?: string; bindings: EventBinding[]; eventOptions: string[]; agentOptions: Agent[]; - onRouteStatusUpdate?: (statuses: BotEventRouteStatus[]) => void; }) { const { t } = useTranslation(); const firstEvent = eventOptions[0] ?? DEFAULT_EVENTS[0]; @@ -902,12 +899,9 @@ function RouteDryRunDialog({ ); const [advancedPayloadOpen, setAdvancedPayloadOpen] = useState(false); const [isRunning, setIsRunning] = useState(false); - const [isDispatching, setIsDispatching] = useState(false); const [payloadError, setPayloadError] = useState(null); const [runError, setRunError] = useState(null); const [result, setResult] = useState(null); - const [dispatchResult, setDispatchResult] = - useState(null); useEffect(() => { if (!eventOptions.includes(eventType)) { @@ -919,7 +913,6 @@ function RouteDryRunDialog({ setPayloadText(JSON.stringify(samplePayloadForEvent(eventType), null, 2)); setPayloadError(null); setResult(null); - setDispatchResult(null); }, [eventType]); function resolveTargetName(resultTarget?: BotRouteDryRunResult['target']) { @@ -956,7 +949,6 @@ function RouteDryRunDialog({ async function runDryRun() { setRunError(null); setResult(null); - setDispatchResult(null); const payload = parsePayload(); if (payload === null) return; @@ -985,44 +977,6 @@ function RouteDryRunDialog({ } } - async function dispatchTestEvent() { - setRunError(null); - setResult(null); - setDispatchResult(null); - - const payload = parsePayload(); - if (payload === null) return; - - if (!botId) { - setRunError(t('bots.dryRunNeedsSavedBot')); - return; - } - - setIsDispatching(true); - try { - const testResult = await backendClient.testBotEventRoute(botId, { - event_type: eventType, - payload, - }); - setDispatchResult(testResult); - onRouteStatusUpdate?.(testResult.route_status?.routes || []); - if (!testResult.dispatched) { - setRunError( - localizedFailureReason( - testResult.failure_code, - testResult.reason, - t, - ) || t('bots.routeTestFailed'), - ); - } - } catch (error) { - const err = error as { msg?: string }; - setRunError(err.msg || t('bots.routeTestFailed')); - } finally { - setIsDispatching(false); - } - } - const targetName = result ? resolveTargetName(result.target) : ''; return ( @@ -1206,24 +1160,6 @@ function RouteDryRunDialog({ )} )} - - {dispatchResult?.dispatched && ( - - - - {t('bots.routeTestDispatched', { - count: dispatchResult.suppressed_outputs?.length || 0, - })} - - - )} - - - - - {t('bots.routeTestSideEffectWarning')} - - @@ -1234,25 +1170,10 @@ function RouteDryRunDialog({ > {t('common.close')} - - @@ -1878,7 +1799,6 @@ export default function EventBindingsEditor({ bindings={bindings} eventOptions={dryRunEventOptions} agentOptions={agentOptions} - onRouteStatusUpdate={setRouteStatuses} /> diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index bc815f279..ac90e87bf 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -275,11 +275,6 @@ export interface BotRouteDryRunRequest { event_bindings?: EventBinding[]; } -export interface BotRouteTestRequest { - event_type: string; - payload?: Record; -} - export interface BotRouteDryRunTarget { target_type: EventBinding['target_type']; target_uuid?: string | null; @@ -334,17 +329,6 @@ export interface BotEventRouteStatusResponse { stale_routes: BotEventRouteStatus[]; } -export interface BotRouteTestResult { - dispatched: boolean; - event_type: string; - status?: BotEventRouteStatus['last_status']; - binding_id?: string | null; - failure_code?: string | null; - reason?: string | null; - suppressed_outputs: Array>; - route_status: BotEventRouteStatusResponse; -} - export interface ApiRespKnowledgeBases { bases: KnowledgeBase[]; } diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 9a96a51c5..7b8147976 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -61,8 +61,6 @@ import { ApiRespSkill, BotRouteDryRunRequest, BotRouteDryRunResult, - BotRouteTestRequest, - BotRouteTestResult, BotEventRouteStatusResponse, } from '@/app/infra/entities/api'; import { Plugin } from '@/app/infra/entities/plugin'; @@ -512,16 +510,6 @@ export class BackendClient extends BaseHttpClient { return this.get(`/api/v1/platform/bots/${botId}/event-routes/status`); } - public testBotEventRoute( - botId: string, - request: BotRouteTestRequest, - ): Promise { - return this.post( - `/api/v1/platform/bots/${botId}/event-routes/test`, - request, - ); - } - public deleteBot(uuid: string): Promise { return this.delete(`/api/v1/platform/bots/${uuid}`); } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 4ede514c9..2a77c0af8 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -450,7 +450,7 @@ const enUS = { routeFallbackCatchAll: '{{route}} is the catch-all route.', routeFallbackIgnored: 'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.', - testRoute: 'Test route', + testRoute: 'Check route', refreshRouteStatus: 'Refresh status', routeStatusIdle: 'No run yet', routeStatusRefreshFailed: 'Failed to refresh route status.', @@ -460,7 +460,6 @@ const enUS = { discarded: 'Discarded', failed: 'Failed', not_matched: 'Not matched', - test_started: 'Testing', }, routeStatusDetail: { matched: 'This route matched the event.', @@ -468,7 +467,6 @@ const enUS = { discarded: 'The event was intentionally discarded.', failed: 'The route could not finish.', not_matched: 'No configured route matched the event.', - test_started: 'The saved route is running.', }, routeFailure: { binding_disabled: 'This route is disabled.', @@ -479,21 +477,12 @@ const enUS = { processor_incompatible: 'The selected processor cannot handle this event.', processor_not_found: 'The selected processor is unavailable.', - 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 full test', - routeTestRunning: 'Running…', - routeTestFailed: 'Failed to run the saved route. Try again later.', - routeTestDispatched: - 'The saved route ran successfully. {{count}} platform actions were blocked.', - routeTestSideEffectWarning: - 'The processor and tools will run, but replies will not be sent to messaging platforms.', - dryRunTitle: 'Test route', + dryRunTitle: 'Check event route', dryRunDescription: - 'Choose an event to see which processor handles it, or run a test.', + 'Choose an event to see which route and processor it matches.', dryRunEventType: 'Event type', dryRunSampleReady: 'Sample event is ready', dryRunSampleDescription: @@ -504,10 +493,10 @@ const enUS = { dryRunPayloadHint: 'Use this to test message and conversation conditions.', dryRunPayloadJsonError: 'Enter valid JSON.', dryRunPayloadObjectError: 'Payload must be a JSON object.', - dryRunNeedsSavedBot: 'Save the bot before testing routes.', - dryRunFailed: 'Route test failed. Try again later.', - dryRunAction: 'Preview match', - dryRunRunning: 'Testing…', + dryRunNeedsSavedBot: 'Save the bot before checking routes.', + dryRunFailed: 'Failed to check the route. Try again later.', + dryRunAction: 'View match', + dryRunRunning: 'Checking…', dryRunMatched: 'Route matched', dryRunNotMatched: 'No route matched', dryRunTarget: 'Target processor', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 0c54cac00..76efd3600 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -457,7 +457,7 @@ const jaJP = { routeFallbackCatchAll: '{{route}} はフォールバックルートです。', routeFallbackIgnored: 'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。', - testRoute: 'ルートをテスト', + testRoute: 'ルートを確認', refreshRouteStatus: '状態を更新', routeStatusIdle: '実行記録なし', routeStatusRefreshFailed: 'ルート状態の更新に失敗しました。', @@ -467,7 +467,6 @@ const jaJP = { discarded: '破棄済み', failed: '失敗', not_matched: '未一致', - test_started: 'テスト中', }, routeStatusDetail: { matched: 'このルートがイベントに一致しました。', @@ -475,7 +474,6 @@ const jaJP = { discarded: '設定に従ってイベントを破棄しました。', failed: 'ルートを完了できませんでした。', not_matched: '設定済みルートに一致しませんでした。', - test_started: '保存済みルートを実行しています。', }, routeFailure: { binding_disabled: 'このルートは無効です。', @@ -486,22 +484,11 @@ const jaJP = { processor_incompatible: '選択したプロセッサーはこのイベントを処理できません。', processor_not_found: '選択したプロセッサーを利用できません。', - bot_runtime_unavailable: - 'ボットが実行されていません。プラットフォーム設定を確認してボットを有効にした後、完全テストを実行してください。', runner_failed: 'Agent Runner がイベント処理中に失敗しました。', delivery_failed: '処理は完了しましたが、結果の配信に失敗しました。', }, - routeTestAction: '完全テストを実行', - routeTestRunning: '実行中…', - routeTestFailed: - '保存済みルートの実行に失敗しました。後でもう一度お試しください。', - routeTestDispatched: - '保存済みルートを実行しました。{{count}} 件のプラットフォーム操作を抑制しました。', - routeTestSideEffectWarning: - 'プロセッサーとツールは実行されますが、返信はメッセージプラットフォームへ送信されません。', - dryRunTitle: 'ルートをテスト', - dryRunDescription: - 'イベントを選び、処理先を確認するか、テストを実行します。', + dryRunTitle: 'イベントルートを確認', + dryRunDescription: 'イベントを選び、一致するルートと処理先を確認します。', dryRunEventType: 'イベントタイプ', dryRunSampleReady: 'サンプルイベントを準備しました', dryRunSampleDescription: @@ -513,10 +500,10 @@ const jaJP = { dryRunPayloadJsonError: '有効な JSON を入力してください。', dryRunPayloadObjectError: 'ペイロードは JSON オブジェクトである必要があります。', - dryRunNeedsSavedBot: 'ルートをテストする前にボットを保存してください。', - dryRunFailed: 'ルートテストに失敗しました。後でもう一度お試しください。', - dryRunAction: '一致を確認', - dryRunRunning: 'テスト中…', + dryRunNeedsSavedBot: 'ルートを確認する前にボットを保存してください。', + dryRunFailed: 'ルートを確認できませんでした。後でもう一度お試しください。', + dryRunAction: '一致結果を確認', + dryRunRunning: '確認中…', dryRunMatched: 'ルートに一致しました', dryRunNotMatched: '一致するルートはありません', dryRunTarget: '対象プロセッサー', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 87b5aff90..c105721b3 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -429,7 +429,7 @@ const zhHans = { routeFallbackCatchAll: '{{route}} 是兜底路由。', routeFallbackIgnored: '未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。', - testRoute: '测试路由', + testRoute: '检查路由', refreshRouteStatus: '刷新状态', routeStatusIdle: '暂无运行记录', routeStatusRefreshFailed: '刷新路由状态失败。', @@ -439,7 +439,6 @@ const zhHans = { discarded: '已丢弃', failed: '失败', not_matched: '未命中', - test_started: '测试中', }, routeStatusDetail: { matched: '此路由已命中事件。', @@ -447,7 +446,6 @@ const zhHans = { discarded: '此事件已按路由配置丢弃。', failed: '此路由未能完成。', not_matched: '没有已配置路由命中此事件。', - test_started: '正在运行已保存路由。', }, routeFailure: { binding_disabled: '此路由已禁用。', @@ -457,19 +455,11 @@ const zhHans = { route_not_found: '没有路由命中此事件。', processor_incompatible: '所选处理器无法处理此事件。', processor_not_found: '所选处理器不可用。', - bot_runtime_unavailable: - '机器人尚未运行。请检查平台配置并启用机器人,再运行完整测试。', runner_failed: 'Agent Runner 处理事件时失败。', delivery_failed: '处理器已完成,但结果投递失败。', }, - routeTestAction: '运行完整测试', - routeTestRunning: '运行中…', - routeTestFailed: '运行已保存路由失败,请稍后重试。', - routeTestDispatched: '已保存路由运行成功,{{count}} 个平台操作已被阻止。', - routeTestSideEffectWarning: - '测试会运行处理器和工具,但不会把回复发送到微信、QQ 等聊天平台。', - dryRunTitle: '测试路由', - dryRunDescription: '选择事件,查看它会交给哪个处理器,或运行一次测试。', + dryRunTitle: '检查事件路由', + dryRunDescription: '选择事件,查看它会匹配哪条路由、交给哪个处理器。', dryRunEventType: '事件类型', dryRunSampleReady: '示例事件已准备好', dryRunSampleDescription: @@ -480,10 +470,10 @@ const zhHans = { dryRunPayloadHint: '用于测试消息内容、会话类型等条件。', dryRunPayloadJsonError: '请输入合法 JSON。', dryRunPayloadObjectError: '载荷必须是 JSON 对象。', - dryRunNeedsSavedBot: '请先保存机器人后再测试路由。', - dryRunFailed: '路由测试失败,请稍后重试。', - dryRunAction: '预览匹配', - dryRunRunning: '测试中…', + dryRunNeedsSavedBot: '请先保存机器人后再检查路由。', + dryRunFailed: '无法检查路由,请稍后重试。', + dryRunAction: '查看匹配结果', + dryRunRunning: '检查中…', dryRunMatched: '已命中路由', dryRunNotMatched: '未命中路由', dryRunTarget: '目标处理器', diff --git a/web/tests/e2e/crud-smoke.spec.ts b/web/tests/e2e/crud-smoke.spec.ts index 681497897..51eb2fdc5 100644 --- a/web/tests/e2e/crud-smoke.spec.ts +++ b/web/tests/e2e/crud-smoke.spec.ts @@ -389,29 +389,6 @@ test.describe('bot advanced flows', () => { }), }), ); - 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); await page.locator('input[name="name"]').fill('Route Status Bot'); @@ -454,14 +431,14 @@ test.describe('bot advanced flows', () => { page.getByText('Failed to refresh route status.'), ).toBeVisible(); - await page.getByRole('button', { name: 'Test route' }).click(); + await page.getByRole('button', { name: 'Check route' }).click(); const routeDialog = page.getByRole('dialog'); await expect( - routeDialog.getByText('Test route', { exact: true }), + routeDialog.getByText('Check event route', { exact: true }), ).toBeVisible(); await expect( routeDialog.getByText( - 'Choose an event to see which processor handles it, or run a test.', + 'Choose an event to see which route and processor it matches.', ), ).toBeVisible(); await expect(routeDialog.getByText('Sample event is ready')).toHaveCount(0); @@ -469,11 +446,11 @@ test.describe('bot advanced flows', () => { routeDialog.getByRole('button', { name: 'Test data' }), ).toBeVisible(); await expect( - routeDialog.getByRole('button', { name: 'Preview match' }), + routeDialog.getByRole('button', { name: 'View match' }), ).toBeVisible(); await expect( routeDialog.getByRole('button', { name: 'Run full test' }), - ).toBeVisible(); + ).toHaveCount(0); const routeEventPicker = routeDialog.getByRole('combobox', { name: 'Event type', }); @@ -494,25 +471,9 @@ test.describe('bot advanced flows', () => { ); await page.keyboard.press('Escape'); - await routeDialog.getByRole('button', { name: 'Preview match' }).click(); + await routeDialog.getByRole('button', { name: 'View 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);