import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, expect, test, vi } from 'vitest'; import BusinessAssistantPage from '@/pages/BusinessAssistant'; import { apiRequest } from '@/services/api'; vi.mock('@/services/api', () => ({ apiRequest: vi.fn(), jsonOptions: vi.fn((method, body) => ({ method, body: JSON.stringify(body) })), })); vi.mock('@/contexts/BotAccess', () => ({ useBotAccess: () => ({ bot: { bot_id: 'primary', label: 'Test Bot', enabled: true, bot_token_configured: true, sudo_users_id: [], log_group_id: '0', gban_log_group_id: '0', message_dump_chat: '0', role_ids: ['business_assistant_manager'], permissions: ['business_assistant.manage'], identity: { id: '999', username: 'business_bot', name: 'Business Bot', can_connect_to_business: true, }, business_assistant: { base_url: 'https://models.example.com/v1', api_key_configured: true, model: 'support-model', timeout_seconds: 30, max_output_tokens: 600, configured: true, }, ready_to_connect: true, runtime: { state: 'running' }, }, }), })); const settings = { connection_id: 'conn-a', assistant_enabled: true, system_prompt: '只依据知识库回答。', language: 'zh-CN', tone: 'professional', account_daily_limit: 200, customer_daily_limit: 20, human_pause_hours: 24, notification_destination: 'both', ops_group_id: '-100123', timezone: 'Asia/Shanghai', digest_enabled: false, digest_time: '09:00', handoff_message: '请稍候,已转人工。', unsupported_message: '该消息需要人工处理。', feishu_webhook_enabled: true, feishu_webhook_url: '', feishu_webhook_signing_secret: '', feishu_webhook_configured: true, feishu_signing_secret_configured: true, feishu_message_preview_enabled: false, }; beforeEach(() => { vi.mocked(apiRequest).mockClear(); vi.mocked(apiRequest).mockImplementation(async (path) => { if (path === '/business-assistant/status') { return { polling_state: 'running', business_mode_supported: true, webhook_conflict: false, webhook_url: '', model_configured: true, usage: { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 }, }; } if (path.startsWith('/business-assistant/connections')) { return { items: [ { connection_id: 'conn-a', user: { id: '900', username: 'owner', first_name: '店主' }, user_chat_id: '900', rights: { can_reply: true, can_read_messages: true }, is_enabled: true, last_event_at: '2026-08-14T08:00:00Z', settings, }, ], total: 1, page: 1, page_size: 100, }; } if (path.startsWith('/business-assistant/settings')) return settings; if (path === '/business-assistant/feishu-webhook/test') { return { ok: true, account: '店主' }; } if (path.startsWith('/business-assistant/knowledge-sources')) { return { items: [ { source_id: 'source-1', connection_id: 'conn-a', source_type: 'channel', chat_id: '-100100', title: '官方公告', publication_mode: 'auto', author_policy: 'admins_or_allowlist', allowed_user_ids: [], include_linked_chat: true, linked_chat_id: '-100101', linked_chat_title: '官方讨论群', backfill_days: 30, backfill_limit: 100, enabled: true, sync_status: 'completed', last_sync_processed: 12, }, ], total: 1, page: 1, page_size: 20, }; } if (path.startsWith('/business-assistant/knowledge-candidates')) { return { items: [ { candidate_id: 'candidate-1', connection_id: 'conn-a', source_id: 'source-1', event_id: 'event-1', question: '配送范围?', aliases: [], keywords: ['配送'], answer: '仅支持市区配送。', tags: ['配送'], confidence: 0.92, status: 'pending', source_snapshot: { title: '官方公告', source_type: 'channel', chat_id: '-100100', message_id: '8', author_id: '900', }, }, ], total: 1, page: 1, page_size: 20, }; } if (path.startsWith('/business-assistant/knowledge')) { return { items: [ { entry_id: 'entry-1', connection_id: 'conn-a', question: '营业时间是什么?', aliases: ['几点营业'], keywords: ['营业时间'], answer: '每天 09:00 至 18:00 营业。', tags: ['营业'], priority: 10, enabled: true, }, ], total: 1, page: 1, page_size: 20, }; } if (path.startsWith('/business-assistant/conversations')) { return { items: [ { conversation_id: 'conversation-1', connection_id: 'conn-a', chat_id: '501', customer: { id: '501', first_name: '客户甲' }, status: 'handoff', summary: '客户询问退款。', handoff_reason: 'sensitive_request', last_message_at: '2026-08-14T08:01:00Z', }, ], total: 1, page: 1, page_size: 20, }; } if (path.startsWith('/business-assistant/usage')) { return { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 }; } throw new Error(`Unexpected request: ${path}`); }); }); test('展示运行状态、连接权限,并用最新搜索词查询知识库', async () => { render(); expect(screen.getByRole('heading', { name: '智能接待' })).toBeInTheDocument(); expect((await screen.findAllByText('店主')).length).toBeGreaterThan(0); expect(screen.getByText('允许回复')).toBeInTheDocument(); expect(screen.getByText('允许标记已读')).toBeInTheDocument(); fireEvent.click(await screen.findByRole('tab', { name: /知识库/ })); expect(await screen.findByText('营业时间是什么?')).toBeInTheDocument(); const search = screen.getByLabelText('搜索知识库'); fireEvent.change(search, { target: { value: '配送' } }); fireEvent.keyDown(search, { key: 'Enter', code: 'Enter' }); await waitFor(() => { expect( vi.mocked(apiRequest).mock.calls.some(([path]) => String(path).includes('query=%E9%85%8D%E9%80%81'), ), ).toBe(true); }); }); test('展示多源知识与待审核候选', async () => { render(); await screen.findAllByText('店主'); fireEvent.click(screen.getByRole('tab', { name: '知识来源' })); expect(await screen.findByText('官方公告')).toBeInTheDocument(); expect(screen.getByText('讨论群:官方讨论群')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '同步来源 官方公告' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: '知识候选' })); expect(await screen.findByText('配送范围?')).toBeInTheDocument(); expect(screen.getByText('仅支持市区配送。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '批准' })).toBeInTheDocument(); }); test('展示脱敏的飞书提醒配置并可发送测试消息', async () => { render(); await screen.findAllByText('店主'); expect(screen.getByText('消息提醒')).toBeInTheDocument(); expect(screen.getByText('第一期按每条用户消息提醒')).toBeInTheDocument(); expect(screen.getByText('飞书消息提醒')).toBeInTheDocument(); expect(screen.getByText('Webhook 已配置')).toBeInTheDocument(); expect(screen.getByText('签名已配置')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '发送测试提醒' })); fireEvent.click(await screen.findByRole('button', { name: '发送测试' })); await waitFor(() => { expect( vi.mocked(apiRequest).mock.calls.some( ([path]) => path === '/business-assistant/feishu-webhook/test', ), ).toBe(true); }); });