mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-05 00:57:14 +00:00
fix(bots): simplify event routing status
This commit is contained in:
@@ -276,14 +276,10 @@ class BotService:
|
|||||||
)
|
)
|
||||||
return result.first()
|
return result.first()
|
||||||
|
|
||||||
async def _get_agent_entity(
|
async def _get_agent_entity(self, context: TenantContext, agent_uuid: str) -> persistence_agent.Agent | None:
|
||||||
self, context: TenantContext, agent_uuid: str
|
|
||||||
) -> persistence_agent.Agent | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
scope_statement(
|
scope_statement(
|
||||||
sqlalchemy.select(persistence_agent.Agent).where(
|
sqlalchemy.select(persistence_agent.Agent).where(persistence_agent.Agent.uuid == agent_uuid),
|
||||||
persistence_agent.Agent.uuid == agent_uuid
|
|
||||||
),
|
|
||||||
persistence_agent.Agent,
|
persistence_agent.Agent,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@@ -390,11 +386,7 @@ class BotService:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
pipeline = (
|
pipeline = await self._get_pipeline_entity(tenant_context, target_uuid) if target_uuid else None
|
||||||
await self._get_pipeline_entity(tenant_context, target_uuid)
|
|
||||||
if target_uuid
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
if pipeline is None:
|
if pipeline is None:
|
||||||
return self._diagnostic_result(
|
return self._diagnostic_result(
|
||||||
matched=False,
|
matched=False,
|
||||||
@@ -509,9 +501,7 @@ class BotService:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _normalize_event_bindings(
|
async def _normalize_event_bindings(self, context: TenantContext, bindings: list[dict] | None) -> list[dict]:
|
||||||
self, context: TenantContext, bindings: list[dict] | None
|
|
||||||
) -> list[dict]:
|
|
||||||
"""Validate and normalize Bot event bindings."""
|
"""Validate and normalize Bot event bindings."""
|
||||||
if not bindings:
|
if not bindings:
|
||||||
return []
|
return []
|
||||||
@@ -544,9 +534,7 @@ class BotService:
|
|||||||
elif target_type == 'agent':
|
elif target_type == 'agent':
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
scope_statement(
|
scope_statement(
|
||||||
sqlalchemy.select(persistence_agent.Agent).where(
|
sqlalchemy.select(persistence_agent.Agent).where(persistence_agent.Agent.uuid == target_uuid),
|
||||||
persistence_agent.Agent.uuid == target_uuid
|
|
||||||
),
|
|
||||||
persistence_agent.Agent,
|
persistence_agent.Agent,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@@ -577,9 +565,7 @@ class BotService:
|
|||||||
|
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
async def _prepare_bot_data(
|
async def _prepare_bot_data(self, context: TenantContext, bot_data: dict, *, include_uuid: bool) -> dict:
|
||||||
self, context: TenantContext, bot_data: dict, *, include_uuid: bool
|
|
||||||
) -> dict:
|
|
||||||
"""Normalize Bot write payloads to the current event-routing model."""
|
"""Normalize Bot write payloads to the current event-routing model."""
|
||||||
update_data = bot_data.copy()
|
update_data = bot_data.copy()
|
||||||
if not include_uuid:
|
if not include_uuid:
|
||||||
@@ -750,21 +736,19 @@ class BotService:
|
|||||||
|
|
||||||
return [log.to_json() for log in logs], total_count
|
return [log.to_json() for log in logs], total_count
|
||||||
|
|
||||||
async def list_event_route_statuses(
|
async def list_event_route_statuses(self, context: TenantContext, bot_uuid: str) -> dict[str, typing.Any]:
|
||||||
self, context: TenantContext, bot_uuid: str
|
|
||||||
) -> dict[str, typing.Any]:
|
|
||||||
"""Return recent runtime status for Bot event routes from in-memory Bot logs."""
|
"""Return recent runtime status for Bot event routes from in-memory Bot logs."""
|
||||||
from ....platform.botmgr import RuntimeBot
|
from ....platform.botmgr import RuntimeBot
|
||||||
|
|
||||||
if await self.get_bot(context, bot_uuid, include_secret=False) is None:
|
bot = await self.get_bot(context, bot_uuid, include_secret=False)
|
||||||
|
if bot is None:
|
||||||
raise WorkspaceNotFoundError('Bot not found')
|
raise WorkspaceNotFoundError('Bot not found')
|
||||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(context, bot_uuid)
|
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(context, bot_uuid)
|
||||||
if runtime_bot is None:
|
|
||||||
raise Exception('Bot not found')
|
|
||||||
|
|
||||||
latest_by_binding: dict[str, dict[str, typing.Any]] = {}
|
latest_by_binding: dict[str, dict[str, typing.Any]] = {}
|
||||||
unmatched_events: list[dict[str, typing.Any]] = []
|
unmatched_events: list[dict[str, typing.Any]] = []
|
||||||
for log in getattr(runtime_bot.logger, 'logs', []):
|
runtime_logs = getattr(getattr(runtime_bot, 'logger', None), 'logs', [])
|
||||||
|
for log in runtime_logs:
|
||||||
status = self._event_route_status_from_log(log)
|
status = self._event_route_status_from_log(log)
|
||||||
if status is None:
|
if status is None:
|
||||||
continue
|
continue
|
||||||
@@ -774,7 +758,10 @@ class BotService:
|
|||||||
else:
|
else:
|
||||||
unmatched_events.append(status)
|
unmatched_events.append(status)
|
||||||
|
|
||||||
raw_bindings = getattr(getattr(runtime_bot, 'bot_entity', None), 'event_bindings', [])
|
runtime_entity = getattr(runtime_bot, 'bot_entity', None)
|
||||||
|
raw_bindings = getattr(runtime_entity, 'event_bindings', None) if runtime_entity is not None else None
|
||||||
|
if raw_bindings is None:
|
||||||
|
raw_bindings = bot.get('event_bindings') or []
|
||||||
bindings = RuntimeBot._get_event_bindings_from_value(raw_bindings)
|
bindings = RuntimeBot._get_event_bindings_from_value(raw_bindings)
|
||||||
routes: list[dict[str, typing.Any]] = []
|
routes: list[dict[str, typing.Any]] = []
|
||||||
current_binding_ids: set[str] = set()
|
current_binding_ids: set[str] = set()
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ def _set_discovered_adapters(ap, *webhook_adapters: str) -> None:
|
|||||||
)
|
)
|
||||||
for adapter_name in webhook_adapters
|
for adapter_name in webhook_adapters
|
||||||
]
|
]
|
||||||
ap.discover = SimpleNamespace(
|
ap.discover = SimpleNamespace(get_components_by_kind=Mock(return_value=components))
|
||||||
get_components_by_kind=Mock(return_value=components)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBotServiceGetBots:
|
class TestBotServiceGetBots:
|
||||||
@@ -583,6 +581,56 @@ class TestBotServiceListEventLogs:
|
|||||||
assert total == 5
|
assert total == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestBotServiceListEventRouteStatuses:
|
||||||
|
"""Tests for event route status when a persisted Bot is not running."""
|
||||||
|
|
||||||
|
async def test_returns_saved_routes_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',
|
||||||
|
'event_bindings': [
|
||||||
|
{
|
||||||
|
'id': 'binding-1',
|
||||||
|
'event_pattern': 'message.received',
|
||||||
|
'target_type': 'agent',
|
||||||
|
'target_uuid': 'agent-1',
|
||||||
|
'enabled': True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.list_event_route_statuses(WORKSPACE_UUID, 'bot-uuid')
|
||||||
|
|
||||||
|
assert result['routes'] == [
|
||||||
|
{
|
||||||
|
'binding_id': 'binding-1',
|
||||||
|
'event_pattern': 'message.received',
|
||||||
|
'event_type': None,
|
||||||
|
'target_type': 'agent',
|
||||||
|
'target_uuid': 'agent-1',
|
||||||
|
'last_status': None,
|
||||||
|
'failure_code': None,
|
||||||
|
'reason': None,
|
||||||
|
'run_id': None,
|
||||||
|
'timestamp': None,
|
||||||
|
'seq_id': None,
|
||||||
|
'level': None,
|
||||||
|
'message': '',
|
||||||
|
'order': 0,
|
||||||
|
'enabled': True,
|
||||||
|
'current': True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert result['unmatched_events'] == []
|
||||||
|
assert result['stale_routes'] == []
|
||||||
|
|
||||||
|
|
||||||
class TestBotServiceSendMessage:
|
class TestBotServiceSendMessage:
|
||||||
"""Tests for send_message method."""
|
"""Tests for send_message method."""
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -1516,8 +1521,8 @@ export default function EventBindingsEditor({
|
|||||||
const response = await backendClient.getBotEventRouteStatuses(botId);
|
const response = await backendClient.getBotEventRouteStatuses(botId);
|
||||||
setRouteStatuses(response.routes || []);
|
setRouteStatuses(response.routes || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as { msg?: string };
|
console.error('Failed to refresh Bot event route status', error);
|
||||||
setRouteStatusError(err.msg || t('bots.routeStatusRefreshFailed'));
|
setRouteStatusError(t('bots.routeStatusRefreshFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setRouteStatusLoading(false);
|
setRouteStatusLoading(false);
|
||||||
}
|
}
|
||||||
@@ -1653,18 +1658,18 @@ export default function EventBindingsEditor({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Alert>
|
{catchAllRouteIndex >= 0 && (
|
||||||
<Info className="h-4 w-4" />
|
<Alert>
|
||||||
<AlertDescription>
|
<Info className="h-4 w-4" />
|
||||||
{catchAllRouteIndex >= 0
|
<AlertDescription>
|
||||||
? t('bots.routeFallbackCatchAll', {
|
{t('bots.routeFallbackCatchAll', {
|
||||||
route: t('bots.dryRunRuleIndex', {
|
route: t('bots.dryRunRuleIndex', {
|
||||||
index: catchAllRouteIndex + 1,
|
index: catchAllRouteIndex + 1,
|
||||||
}),
|
}),
|
||||||
})
|
})}
|
||||||
: t('bots.routeFallbackIgnored')}
|
</AlertDescription>
|
||||||
</AlertDescription>
|
</Alert>
|
||||||
</Alert>
|
)}
|
||||||
|
|
||||||
{/* enabled section */}
|
{/* enabled section */}
|
||||||
<DndContext
|
<DndContext
|
||||||
@@ -1778,22 +1783,27 @@ export default function EventBindingsEditor({
|
|||||||
agentOptions={agentOptions}
|
agentOptions={agentOptions}
|
||||||
onRouteStatusUpdate={setRouteStatuses}
|
onRouteStatusUpdate={setRouteStatuses}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Tooltip>
|
||||||
type="button"
|
<TooltipTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
onClick={refreshRouteStatuses}
|
variant="ghost"
|
||||||
disabled={!botId || routeStatusLoading}
|
size="icon"
|
||||||
>
|
className={`size-8 ${routeStatusError ? 'text-destructive' : 'text-muted-foreground'}`}
|
||||||
<RefreshCw
|
aria-label={t('bots.refreshRouteStatus')}
|
||||||
className={`h-4 w-4 mr-1 ${routeStatusLoading ? 'animate-spin' : ''}`}
|
onClick={refreshRouteStatuses}
|
||||||
/>
|
disabled={!botId || routeStatusLoading}
|
||||||
{t('bots.refreshRouteStatus')}
|
>
|
||||||
</Button>
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${routeStatusLoading ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{routeStatusError || t('bots.refreshRouteStatus')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{routeStatusError && (
|
|
||||||
<p className="text-xs text-destructive">{routeStatusError}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* disabled section */}
|
{/* disabled section */}
|
||||||
{disabledBindings.length > 0 && (
|
{disabledBindings.length > 0 && (
|
||||||
|
|||||||
@@ -384,8 +384,7 @@ const enUS = {
|
|||||||
routingConnectionDescription:
|
routingConnectionDescription:
|
||||||
'Bind the pipeline that processes messages for this bot',
|
'Bind the pipeline that processes messages for this bot',
|
||||||
eventRouting: 'Event Routing',
|
eventRouting: 'Event Routing',
|
||||||
eventRoutingDescription:
|
eventRoutingDescription: 'Choose which processor handles each event.',
|
||||||
'Choose which processor handles each event received by this bot. Edit the logic in the corresponding Agent or Pipeline configuration. Pipelines only support message events.',
|
|
||||||
eventBindings: 'Event Routes',
|
eventBindings: 'Event Routes',
|
||||||
addEventBinding: 'Add Route',
|
addEventBinding: 'Add Route',
|
||||||
addBehavior: 'Add behavior',
|
addBehavior: 'Add behavior',
|
||||||
@@ -425,18 +424,16 @@ const enUS = {
|
|||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
disabledBindings: 'Disabled',
|
disabledBindings: 'Disabled',
|
||||||
adapterEventsTitle: 'Events this adapter can receive',
|
adapterEventsTitle: 'Supported events',
|
||||||
adapterEventsDescription:
|
adapterEventsDescription: '{{count}} event types',
|
||||||
'{{count}} event types are available. Routes are matched in order, and unmatched events are not sent to a processor.',
|
|
||||||
adapterEventsMore: '{{count}} more',
|
adapterEventsMore: '{{count}} more',
|
||||||
advancedEventValues: 'Advanced event values',
|
advancedEventValues: 'View all',
|
||||||
eventGroup: 'Group',
|
eventGroup: 'Group',
|
||||||
routeConflictTitle: 'Some routes overlap',
|
routeConflictTitle: 'Some routes overlap',
|
||||||
routeConflictShadowed:
|
routeConflictShadowed:
|
||||||
'{{shadowed}} may never run because {{winner}} handles the same events first.',
|
'{{shadowed}} may never run because {{winner}} handles the same events first.',
|
||||||
routeConflictMore: '{{count}} more route conflicts need attention.',
|
routeConflictMore: '{{count}} more route conflicts need attention.',
|
||||||
routeFallbackCatchAll:
|
routeFallbackCatchAll: '{{route}} is the catch-all route.',
|
||||||
'{{route}} is the catch-all route. Routes with higher priority run first.',
|
|
||||||
routeFallbackIgnored:
|
routeFallbackIgnored:
|
||||||
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
|
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
|
||||||
testRoute: 'Test route',
|
testRoute: 'Test route',
|
||||||
|
|||||||
@@ -390,8 +390,7 @@ const jaJP = {
|
|||||||
routingConnectionDescription:
|
routingConnectionDescription:
|
||||||
'このボットのメッセージを処理するパイプラインを紐付け',
|
'このボットのメッセージを処理するパイプラインを紐付け',
|
||||||
eventRouting: 'イベントルーティング',
|
eventRouting: 'イベントルーティング',
|
||||||
eventRoutingDescription:
|
eventRoutingDescription: 'イベントごとの処理先を設定します。',
|
||||||
'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。対応する Agent または Pipeline の設定で処理ロジックを編集します。Pipeline はメッセージイベントのみ対応します。',
|
|
||||||
eventBindings: 'イベントルート',
|
eventBindings: 'イベントルート',
|
||||||
addEventBinding: 'ルートを追加',
|
addEventBinding: 'ルートを追加',
|
||||||
addBehavior: '動作を追加',
|
addBehavior: '動作を追加',
|
||||||
@@ -432,18 +431,16 @@ const jaJP = {
|
|||||||
disable: '無効化',
|
disable: '無効化',
|
||||||
enable: '有効化',
|
enable: '有効化',
|
||||||
disabledBindings: '無効',
|
disabledBindings: '無効',
|
||||||
adapterEventsTitle: 'このアダプターが受信できるイベント',
|
adapterEventsTitle: '対応イベント',
|
||||||
adapterEventsDescription:
|
adapterEventsDescription: '{{count}} 種類',
|
||||||
'{{count}} 種類のイベントを利用できます。ルートは上から順に照合され、未一致のイベントはプロセッサーへ送られません。',
|
|
||||||
adapterEventsMore: 'ほか {{count}} 件',
|
adapterEventsMore: 'ほか {{count}} 件',
|
||||||
advancedEventValues: '高度なイベント値',
|
advancedEventValues: 'すべて表示',
|
||||||
eventGroup: 'グループ',
|
eventGroup: 'グループ',
|
||||||
routeConflictTitle: '一部のルートが重複しています',
|
routeConflictTitle: '一部のルートが重複しています',
|
||||||
routeConflictShadowed:
|
routeConflictShadowed:
|
||||||
'{{winner}} が同じイベントを先に処理するため、{{shadowed}} は実行されない可能性があります。',
|
'{{winner}} が同じイベントを先に処理するため、{{shadowed}} は実行されない可能性があります。',
|
||||||
routeConflictMore: 'ほか {{count}} 件のルート競合を確認してください。',
|
routeConflictMore: 'ほか {{count}} 件のルート競合を確認してください。',
|
||||||
routeFallbackCatchAll:
|
routeFallbackCatchAll: '{{route}} はフォールバックルートです。',
|
||||||
'{{route}} はすべてのイベントを受けるフォールバックです。優先度の高いルートが先に実行されます。',
|
|
||||||
routeFallbackIgnored:
|
routeFallbackIgnored:
|
||||||
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
|
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
|
||||||
testRoute: 'ルートをテスト',
|
testRoute: 'ルートをテスト',
|
||||||
|
|||||||
@@ -367,8 +367,7 @@ const zhHans = {
|
|||||||
routingConnection: '路由与连接',
|
routingConnection: '路由与连接',
|
||||||
routingConnectionDescription: '绑定处理此机器人消息的流水线',
|
routingConnectionDescription: '绑定处理此机器人消息的流水线',
|
||||||
eventRouting: '事件路由',
|
eventRouting: '事件路由',
|
||||||
eventRoutingDescription:
|
eventRoutingDescription: '设置收到事件后交给哪个处理器。',
|
||||||
'选择此机器人收到不同事件时交给哪个处理器。在对应的 Agent 或 Pipeline 配置中编辑处理逻辑;Pipeline 仅支持消息事件。',
|
|
||||||
eventBindings: '事件路由',
|
eventBindings: '事件路由',
|
||||||
addEventBinding: '添加路由',
|
addEventBinding: '添加路由',
|
||||||
addBehavior: '添加行为',
|
addBehavior: '添加行为',
|
||||||
@@ -404,18 +403,16 @@ const zhHans = {
|
|||||||
disable: '禁用',
|
disable: '禁用',
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
disabledBindings: '已禁用',
|
disabledBindings: '已禁用',
|
||||||
adapterEventsTitle: '此适配器可接收的事件',
|
adapterEventsTitle: '支持的事件',
|
||||||
adapterEventsDescription:
|
adapterEventsDescription: '共 {{count}} 类',
|
||||||
'已识别 {{count}} 类事件。路由会按顺序匹配,未命中时不会交给处理器。',
|
|
||||||
adapterEventsMore: '另有 {{count}} 类',
|
adapterEventsMore: '另有 {{count}} 类',
|
||||||
advancedEventValues: '高级事件值',
|
advancedEventValues: '查看全部',
|
||||||
eventGroup: '事件组',
|
eventGroup: '事件组',
|
||||||
routeConflictTitle: '部分路由存在覆盖冲突',
|
routeConflictTitle: '部分路由存在覆盖冲突',
|
||||||
routeConflictShadowed:
|
routeConflictShadowed:
|
||||||
'{{shadowed}} 可能永远不会运行,因为 {{winner}} 会先处理相同事件。',
|
'{{shadowed}} 可能永远不会运行,因为 {{winner}} 会先处理相同事件。',
|
||||||
routeConflictMore: '另有 {{count}} 个路由冲突需要处理。',
|
routeConflictMore: '另有 {{count}} 个路由冲突需要处理。',
|
||||||
routeFallbackCatchAll:
|
routeFallbackCatchAll: '{{route}} 是兜底路由。',
|
||||||
'{{route}} 是全局兜底路由,优先级更高的路由会先运行。',
|
|
||||||
routeFallbackIgnored:
|
routeFallbackIgnored:
|
||||||
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
|
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
|
||||||
testRoute: '测试路由',
|
testRoute: '测试路由',
|
||||||
|
|||||||
@@ -335,6 +335,37 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('bot advanced flows', () => {
|
test.describe('bot advanced flows', () => {
|
||||||
|
test('keeps event routing compact and hides raw status errors', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installLangBotApiMocks(page, { authenticated: true });
|
||||||
|
await page.route('**/api/v1/platform/bots/*/event-routes/status', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ code: -1, msg: 'Internal server error' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto('/home/bots?id=new');
|
||||||
|
await selectPlaywrightAdapter(page);
|
||||||
|
await page.locator('input[name="name"]').fill('Route Status Bot');
|
||||||
|
await submit(page);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
|
await expect(page.getByText('Supported events')).toBeVisible();
|
||||||
|
await expect(page.getByText('1 event types')).toBeVisible();
|
||||||
|
await expect(page.getByText('Internal server error')).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByText('Events that match no route are ignored.'),
|
||||||
|
).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Refresh status' }).hover();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Failed to refresh route status.'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('toggles bot enable/disable state', async ({ page }) => {
|
test('toggles bot enable/disable state', async ({ page }) => {
|
||||||
await installLangBotApiMocks(page, { authenticated: true });
|
await installLangBotApiMocks(page, { authenticated: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user