BusinessAssistant.test.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. };
  64. beforeEach(() => {
  65. vi.mocked(apiRequest).mockClear();
  66. vi.mocked(apiRequest).mockImplementation(async (path) => {
  67. if (path === '/business-assistant/status') {
  68. return {
  69. polling_state: 'running',
  70. business_mode_supported: true,
  71. webhook_conflict: false,
  72. webhook_url: '',
  73. model_configured: true,
  74. usage: { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 },
  75. };
  76. }
  77. if (path.startsWith('/business-assistant/connections')) {
  78. return {
  79. items: [
  80. {
  81. connection_id: 'conn-a',
  82. user: { id: '900', username: 'owner', first_name: '店主' },
  83. user_chat_id: '900',
  84. rights: { can_reply: true, can_read_messages: true },
  85. is_enabled: true,
  86. last_event_at: '2026-08-14T08:00:00Z',
  87. settings,
  88. },
  89. ],
  90. total: 1,
  91. page: 1,
  92. page_size: 100,
  93. };
  94. }
  95. if (path.startsWith('/business-assistant/settings')) return settings;
  96. if (path === '/business-assistant/feishu-webhook/test') {
  97. return { ok: true, account: '店主' };
  98. }
  99. if (path.startsWith('/business-assistant/knowledge-sources')) {
  100. return {
  101. items: [
  102. {
  103. source_id: 'source-1',
  104. connection_id: 'conn-a',
  105. source_type: 'channel',
  106. chat_id: '-100100',
  107. title: '官方公告',
  108. publication_mode: 'auto',
  109. author_policy: 'admins_or_allowlist',
  110. allowed_user_ids: [],
  111. include_linked_chat: true,
  112. linked_chat_id: '-100101',
  113. linked_chat_title: '官方讨论群',
  114. backfill_days: 30,
  115. backfill_limit: 100,
  116. enabled: true,
  117. sync_status: 'completed',
  118. last_sync_processed: 12,
  119. },
  120. ],
  121. total: 1,
  122. page: 1,
  123. page_size: 20,
  124. };
  125. }
  126. if (path.startsWith('/business-assistant/knowledge-candidates')) {
  127. return {
  128. items: [
  129. {
  130. candidate_id: 'candidate-1',
  131. connection_id: 'conn-a',
  132. source_id: 'source-1',
  133. event_id: 'event-1',
  134. question: '配送范围?',
  135. aliases: [],
  136. keywords: ['配送'],
  137. answer: '仅支持市区配送。',
  138. tags: ['配送'],
  139. confidence: 0.92,
  140. status: 'pending',
  141. source_snapshot: {
  142. title: '官方公告',
  143. source_type: 'channel',
  144. chat_id: '-100100',
  145. message_id: '8',
  146. author_id: '900',
  147. },
  148. },
  149. ],
  150. total: 1,
  151. page: 1,
  152. page_size: 20,
  153. };
  154. }
  155. if (path.startsWith('/business-assistant/knowledge')) {
  156. return {
  157. items: [
  158. {
  159. entry_id: 'entry-1',
  160. connection_id: 'conn-a',
  161. question: '营业时间是什么?',
  162. aliases: ['几点营业'],
  163. keywords: ['营业时间'],
  164. answer: '每天 09:00 至 18:00 营业。',
  165. tags: ['营业'],
  166. priority: 10,
  167. enabled: true,
  168. },
  169. ],
  170. total: 1,
  171. page: 1,
  172. page_size: 20,
  173. };
  174. }
  175. if (path.startsWith('/business-assistant/conversations')) {
  176. return {
  177. items: [
  178. {
  179. conversation_id: 'conversation-1',
  180. connection_id: 'conn-a',
  181. chat_id: '501',
  182. customer: { id: '501', first_name: '客户甲' },
  183. status: 'handoff',
  184. summary: '客户询问退款。',
  185. handoff_reason: 'sensitive_request',
  186. last_message_at: '2026-08-14T08:01:00Z',
  187. },
  188. ],
  189. total: 1,
  190. page: 1,
  191. page_size: 20,
  192. };
  193. }
  194. if (path.startsWith('/business-assistant/usage')) {
  195. return { ai_calls: 8, customers: 3, conversations: 3, handoffs: 1, human_paused: 1 };
  196. }
  197. throw new Error(`Unexpected request: ${path}`);
  198. });
  199. });
  200. test('展示运行状态、连接权限,并用最新搜索词查询知识库', async () => {
  201. render(<BusinessAssistantPage />);
  202. expect(screen.getByRole('heading', { name: '智能接待' })).toBeInTheDocument();
  203. expect((await screen.findAllByText('店主')).length).toBeGreaterThan(0);
  204. expect(screen.getByText('允许回复')).toBeInTheDocument();
  205. expect(screen.getByText('允许标记已读')).toBeInTheDocument();
  206. fireEvent.click(await screen.findByRole('tab', { name: /知识库/ }));
  207. expect(await screen.findByText('营业时间是什么?')).toBeInTheDocument();
  208. const search = screen.getByLabelText('搜索知识库');
  209. fireEvent.change(search, { target: { value: '配送' } });
  210. fireEvent.keyDown(search, { key: 'Enter', code: 'Enter' });
  211. await waitFor(() => {
  212. expect(
  213. vi.mocked(apiRequest).mock.calls.some(([path]) =>
  214. String(path).includes('query=%E9%85%8D%E9%80%81'),
  215. ),
  216. ).toBe(true);
  217. });
  218. });
  219. test('展示多源知识与待审核候选', async () => {
  220. render(<BusinessAssistantPage />);
  221. await screen.findAllByText('店主');
  222. fireEvent.click(screen.getByRole('tab', { name: '知识来源' }));
  223. expect(await screen.findByText('官方公告')).toBeInTheDocument();
  224. expect(screen.getByText('讨论群:官方讨论群')).toBeInTheDocument();
  225. expect(screen.getByRole('button', { name: '同步来源 官方公告' })).toBeInTheDocument();
  226. fireEvent.click(screen.getByRole('tab', { name: '知识候选' }));
  227. expect(await screen.findByText('配送范围?')).toBeInTheDocument();
  228. expect(screen.getByText('仅支持市区配送。')).toBeInTheDocument();
  229. expect(screen.getByRole('button', { name: '批准' })).toBeInTheDocument();
  230. });
  231. test('展示脱敏的飞书提醒配置并可发送测试消息', async () => {
  232. render(<BusinessAssistantPage />);
  233. await screen.findAllByText('店主');
  234. expect(screen.getByText('消息提醒')).toBeInTheDocument();
  235. expect(screen.getByText('第一期按每条用户消息提醒')).toBeInTheDocument();
  236. expect(screen.getByText('飞书消息提醒')).toBeInTheDocument();
  237. expect(screen.getByText('Webhook 已配置')).toBeInTheDocument();
  238. expect(screen.getByText('签名已配置')).toBeInTheDocument();
  239. fireEvent.click(screen.getByRole('button', { name: '发送测试提醒' }));
  240. fireEvent.click(await screen.findByRole('button', { name: '发送测试' }));
  241. await waitFor(() => {
  242. expect(
  243. vi.mocked(apiRequest).mock.calls.some(
  244. ([path]) => path === '/business-assistant/feishu-webhook/test',
  245. ),
  246. ).toBe(true);
  247. });
  248. });