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);
});
});