BusinessAssistant.test.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import { fireEvent, render, screen, waitFor } from '@testing-library/react';
  2. import { beforeEach, expect, test, vi } from 'vitest';
  3. import BusinessAssistantPage from '@/pages/BusinessAssistant';
  4. import { apiRequest } from '@/services/api';
  5. vi.mock('@/services/api', () => ({
  6. apiRequest: vi.fn(),
  7. jsonOptions: vi.fn((method, body) => ({ method, body: JSON.stringify(body) })),
  8. }));
  9. vi.mock('@/contexts/BotAccess', () => ({
  10. useBotAccess: () => ({
  11. bot: {
  12. bot_id: 'primary',
  13. label: 'Test Bot',
  14. enabled: true,
  15. bot_token_configured: true,
  16. sudo_users_id: [],
  17. log_group_id: '0',
  18. gban_log_group_id: '0',
  19. message_dump_chat: '0',
  20. role_ids: ['business_assistant_manager'],
  21. permissions: ['business_assistant.manage'],
  22. identity: {
  23. id: '999',
  24. username: 'business_bot',
  25. name: 'Business Bot',
  26. can_connect_to_business: true,
  27. },
  28. business_assistant: {
  29. base_url: 'https://models.example.com/v1',
  30. api_key_configured: true,
  31. model: 'support-model',
  32. timeout_seconds: 30,
  33. max_output_tokens: 600,
  34. configured: true,
  35. },
  36. ready_to_connect: true,
  37. runtime: { state: 'running' },
  38. },
  39. }),
  40. }));
  41. const settings = {
  42. connection_id: 'conn-a',
  43. assistant_enabled: true,
  44. system_prompt: '只依据知识库回答。',
  45. language: 'zh-CN',
  46. tone: 'professional',
  47. account_daily_limit: 200,
  48. customer_daily_limit: 20,
  49. human_pause_hours: 24,
  50. notification_destination: 'both',
  51. ops_group_id: '-100123',
  52. timezone: 'Asia/Shanghai',
  53. digest_enabled: false,
  54. digest_time: '09:00',
  55. handoff_message: '请稍候,已转人工。',
  56. unsupported_message: '该消息需要人工处理。',
  57. feishu_webhook_enabled: true,
  58. feishu_webhook_url: '',
  59. feishu_webhook_signing_secret: '',
  60. feishu_webhook_configured: true,
  61. feishu_signing_secret_configured: true,
  62. feishu_message_preview_enabled: false,
  63. feishu_outgoing_message_enabled: false,
  64. };
  65. beforeEach(() => {
  66. vi.mocked(apiRequest).mockClear();
  67. vi.mocked(apiRequest).mockImplementation(async (path) => {
  68. if (path === '/business-assistant/status') {
  69. return {
  70. polling_state: 'running',
  71. business_mode_supported: true,
  72. webhook_conflict: false,
  73. webhook_url: '',
  74. model_configured: true,
  75. usage: { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 },
  76. };
  77. }
  78. if (path.startsWith('/business-assistant/connections')) {
  79. return {
  80. items: [
  81. {
  82. connection_id: 'conn-a',
  83. user: { id: '900', username: 'owner', first_name: '店主' },
  84. user_chat_id: '900',
  85. rights: { can_reply: true, can_read_messages: true },
  86. is_enabled: true,
  87. last_event_at: '2026-08-14T08:00:00Z',
  88. settings,
  89. },
  90. ],
  91. total: 1,
  92. page: 1,
  93. page_size: 100,
  94. };
  95. }
  96. if (path.startsWith('/business-assistant/settings')) return settings;
  97. if (path === '/business-assistant/feishu-webhook/test') {
  98. return { ok: true, account: '店主' };
  99. }
  100. if (path.startsWith('/business-assistant/knowledge-sources')) {
  101. return {
  102. items: [
  103. {
  104. source_id: 'source-1',
  105. connection_id: 'conn-a',
  106. source_type: 'channel',
  107. chat_id: '-100100',
  108. title: '官方公告',
  109. publication_mode: 'auto',
  110. author_policy: 'admins_or_allowlist',
  111. allowed_user_ids: [],
  112. include_linked_chat: true,
  113. linked_chat_id: '-100101',
  114. linked_chat_title: '官方讨论群',
  115. backfill_days: 30,
  116. backfill_limit: 100,
  117. enabled: true,
  118. sync_status: 'completed',
  119. last_sync_processed: 12,
  120. },
  121. ],
  122. total: 1,
  123. page: 1,
  124. page_size: 20,
  125. };
  126. }
  127. if (path.startsWith('/business-assistant/knowledge-candidates')) {
  128. return {
  129. items: [
  130. {
  131. candidate_id: 'candidate-1',
  132. connection_id: 'conn-a',
  133. source_id: 'source-1',
  134. event_id: 'event-1',
  135. question: '配送范围?',
  136. aliases: [],
  137. keywords: ['配送'],
  138. answer: '仅支持市区配送。',
  139. tags: ['配送'],
  140. confidence: 0.92,
  141. status: 'pending',
  142. source_snapshot: {
  143. title: '官方公告',
  144. source_type: 'channel',
  145. chat_id: '-100100',
  146. message_id: '8',
  147. author_id: '900',
  148. },
  149. },
  150. ],
  151. total: 1,
  152. page: 1,
  153. page_size: 20,
  154. };
  155. }
  156. if (path.startsWith('/business-assistant/knowledge')) {
  157. return {
  158. items: [
  159. {
  160. entry_id: 'entry-1',
  161. connection_id: 'conn-a',
  162. question: '营业时间是什么?',
  163. aliases: ['几点营业'],
  164. keywords: ['营业时间'],
  165. answer: '每天 09:00 至 18:00 营业。',
  166. tags: ['营业'],
  167. priority: 10,
  168. enabled: true,
  169. },
  170. ],
  171. total: 1,
  172. page: 1,
  173. page_size: 20,
  174. };
  175. }
  176. if (path.startsWith('/business-assistant/conversations')) {
  177. return {
  178. items: [
  179. {
  180. conversation_id: 'conversation-1',
  181. connection_id: 'conn-a',
  182. chat_id: '501',
  183. customer: { id: '501', first_name: '客户甲' },
  184. status: 'handoff',
  185. summary: '客户询问退款。',
  186. handoff_reason: 'sensitive_request',
  187. last_message_at: '2026-08-14T08:01:00Z',
  188. },
  189. ],
  190. total: 1,
  191. page: 1,
  192. page_size: 20,
  193. };
  194. }
  195. if (path.startsWith('/business-assistant/usage')) {
  196. return { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 };
  197. }
  198. throw new Error(`Unexpected request: ${path}`);
  199. });
  200. });
  201. test('展示运行状态、连接权限,并用最新搜索词查询知识库', async () => {
  202. render(<BusinessAssistantPage />);
  203. expect(screen.getByRole('heading', { name: '智能接待' })).toBeInTheDocument();
  204. expect((await screen.findAllByText('店主')).length).toBeGreaterThan(0);
  205. expect(screen.getByText('允许回复')).toBeInTheDocument();
  206. expect(screen.getByText('允许标记已读')).toBeInTheDocument();
  207. fireEvent.click(await screen.findByRole('tab', { name: /知识库/ }));
  208. expect(await screen.findByText('营业时间是什么?')).toBeInTheDocument();
  209. const search = screen.getByLabelText('搜索知识库');
  210. fireEvent.change(search, { target: { value: '配送' } });
  211. fireEvent.keyDown(search, { key: 'Enter', code: 'Enter' });
  212. await waitFor(() => {
  213. expect(
  214. vi.mocked(apiRequest).mock.calls.some(([path]) =>
  215. String(path).includes('query=%E9%85%8D%E9%80%81'),
  216. ),
  217. ).toBe(true);
  218. });
  219. });
  220. test('展示多源知识与待审核候选', async () => {
  221. render(<BusinessAssistantPage />);
  222. await screen.findAllByText('店主');
  223. fireEvent.click(screen.getByRole('tab', { name: '知识来源' }));
  224. expect(await screen.findByText('官方公告')).toBeInTheDocument();
  225. expect(screen.getByText('讨论群:官方讨论群')).toBeInTheDocument();
  226. expect(screen.getByRole('button', { name: '同步来源 官方公告' })).toBeInTheDocument();
  227. fireEvent.click(screen.getByRole('tab', { name: '知识候选' }));
  228. expect(await screen.findByText('配送范围?')).toBeInTheDocument();
  229. expect(screen.getByText('仅支持市区配送。')).toBeInTheDocument();
  230. expect(screen.getByRole('button', { name: '批准' })).toBeInTheDocument();
  231. });
  232. test('展示脱敏的飞书提醒配置并可发送测试消息', async () => {
  233. render(<BusinessAssistantPage />);
  234. await screen.findAllByText('店主');
  235. expect(screen.getByText('消息提醒')).toBeInTheDocument();
  236. expect(screen.getByText('按消息方向选择飞书提醒')).toBeInTheDocument();
  237. expect(screen.getByText('飞书消息提醒')).toBeInTheDocument();
  238. expect(screen.getByText('托管账号发出的消息')).toBeInTheDocument();
  239. expect(screen.getByText('Webhook 已配置')).toBeInTheDocument();
  240. expect(screen.getByText('签名已配置')).toBeInTheDocument();
  241. fireEvent.click(screen.getByRole('switch', { name: '托管账号发出的消息' }));
  242. fireEvent.click(screen.getByRole('button', { name: '保存接待设置' }));
  243. fireEvent.click(await screen.findByRole('button', { name: '确认保存' }));
  244. await waitFor(() => {
  245. const saveCall = vi.mocked(apiRequest).mock.calls.find(
  246. ([path, options]) => path === '/business-assistant/settings' && options?.method === 'PUT',
  247. );
  248. expect(saveCall).toBeDefined();
  249. expect(JSON.parse(String(saveCall?.[1]?.body)).feishu_outgoing_message_enabled).toBe(true);
  250. });
  251. fireEvent.click(screen.getByRole('button', { name: '发送测试提醒' }));
  252. fireEvent.click(await screen.findByRole('button', { name: '发送测试' }));
  253. await waitFor(() => {
  254. expect(
  255. vi.mocked(apiRequest).mock.calls.some(
  256. ([path]) => path === '/business-assistant/feishu-webhook/test',
  257. ),
  258. ).toBe(true);
  259. });
  260. });