import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { ChannelAccounts } from '@/pages/CloudAgents/ChannelAccounts'; const api = vi.hoisted(() => ({ call: vi.fn(), list: vi.fn(), recovery: vi.fn(), resolvePending: vi.fn() })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); vi.mock('@/pages/CloudAgents/CloudWechatQr', () => ({ CloudWechatQr: () => 个人微信登录二维码 })); const agentA = { slug: 'ml-' + 'a'.repeat(32), name: '写作助手', published_version: 2, draft_revision: 3 }; const agentB = { slug: 'ml-' + 'b'.repeat(32), name: '资料助手', published_version: 4, draft_revision: 5 }; const legacy = { id: 'wechat-legacy', display_name: '原有微信', binding_id: 'binding-legacy', revision: 7, enabled: true, status: 'active', health: 'connected', blockers: [], target_agent_slug: agentA.slug, target_agent_name: agentA.name, target_agent_published_version: 2, provider_generation: 'generation-legacy', }; const spare = { id: 'wechat-spare', display_name: '备用微信', binding_id: 'binding-spare', revision: 2, enabled: false, status: 'paused', health: 'login_required', blockers: ['target_required'], target_agent_slug: null, target_agent_name: null, target_agent_published_version: null, provider_generation: 'generation-spare', }; beforeEach(() => { vi.clearAllMocks(); api.list.mockResolvedValue({ agents: [agentA, agentB], next_cursor: null }); api.recovery.mockResolvedValue({ recent: null, pending: [] }); api.call.mockImplementation(async (operation: string, input: Record) => { if (operation === 'channelAccounts') return { items: [legacy, spare] }; if (operation === 'channelAccountWechatBindStart') return { session_key: 'qr-spare', status: 'verification_required', qrcode_url: 'https://login.example.test/qr' }; if (operation === 'routeChannelAccount') return { channel: { ...spare, target_agent_slug: input.target_agent_slug } }; if (operation === 'channelActivity') return { items: [], next_offset: null }; throw new Error(`Unexpected operation ${operation}`); }); }); afterEach(() => vi.useRealTimers()); it('closes the QR after confirmed polling and shows the next step for an unassigned account', async () => { const base = api.call.getMockImplementation()!; let connected = false; api.call.mockImplementation(async (operation, input) => { if (operation === 'channelAccounts') return { items: [connected ? { ...spare, health: 'connected' } : spare] }; if (operation === 'channelAccountWechatBindStatus') { connected = true; return { session_key: 'qr-spare', status: 'confirmed', qrcode_url: 'https://login.example.test/qr' }; } return base(operation, input); }); render(); const detail = await screen.findByRole('article', { name: '备用微信账号详情' }); vi.useFakeTimers(); await act(async () => fireEvent.click(within(detail).getByRole('button', { name: '扫码连接' }))); expect(screen.getByRole('img', { name: '个人微信登录二维码' })).toBeVisible(); await act(async () => vi.advanceTimersByTimeAsync(2000)); expect(screen.queryByRole('img', { name: '个人微信登录二维码' })).not.toBeInTheDocument(); expect(screen.queryByLabelText('微信验证码')).not.toBeInTheDocument(); expect(screen.getByRole('status')).toHaveTextContent('微信已连接'); expect(screen.getByRole('status')).toHaveTextContent('选择目标智能体'); expect(within(detail).getByRole('button', { name: '重新连接' })).toBeEnabled(); await act(async () => vi.advanceTimersByTimeAsync(6000)); expect(api.call.mock.calls.filter(([operation]) => operation === 'channelAccountWechatBindStatus')).toHaveLength(1); expect(api.call.mock.calls.some(([operation]) => operation === 'routeChannelAccount' || operation === 'enableChannelAccount')).toBe(false); }); it.each(['channelAccountWechatBindStart', 'channelAccountWechatBindVerification'])('handles confirmed %s for an already routed account', async confirmedOperation => { const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation, input) => { if (operation === confirmedOperation) return { session_key: 'qr-spare', status: 'confirmed', qrcode_url: 'https://login.example.test/qr' }; return base(operation, input); }); render(); const detail = await screen.findByRole('article', { name: '原有微信账号详情' }); fireEvent.click(within(detail).getByRole('button', { name: '重新连接' })); if (confirmedOperation === 'channelAccountWechatBindVerification') { expect(await screen.findByRole('img', { name: '个人微信登录二维码' })).toBeVisible(); fireEvent.change(screen.getByLabelText('微信验证码'), { target: { value: '123456' } }); fireEvent.click(screen.getByRole('button', { name: '提交验证码' })); } expect(await screen.findByText('微信已连接', { exact: true })).toBeVisible(); expect(screen.queryByRole('img', { name: '个人微信登录二维码' })).not.toBeInTheDocument(); expect(screen.queryByLabelText('微信验证码')).not.toBeInTheDocument(); expect(within(detail).getByRole('status')).toHaveTextContent('返回微信与智能体对话'); fireEvent.click(screen.getByRole('button', { name: /备用微信.*未分配智能体/ })); expect(screen.queryByText('微信已连接', { exact: true })).not.toBeInTheDocument(); }); it('keeps the QR while verification is pending instead of claiming the connection is confirmed', async () => { const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation, input) => { if (operation === 'channelAccountWechatBindVerification') return { session_key: 'qr-spare', status: 'pending', qrcode_url: 'https://login.example.test/qr' }; return base(operation, input); }); render(); const detail = await screen.findByRole('article', { name: '备用微信账号详情' }); fireEvent.click(within(detail).getByRole('button', { name: '扫码连接' })); fireEvent.change(await screen.findByLabelText('微信验证码'), { target: { value: '123456' } }); fireEvent.click(screen.getByRole('button', { name: '提交验证码' })); expect(await screen.findByText('验证码已提交,请等待微信确认。')).toBeVisible(); expect(screen.getByRole('img', { name: '个人微信登录二维码' })).toBeVisible(); expect(screen.queryByText('微信已连接', { exact: true })).not.toBeInTheDocument(); }); it('shows legacy and unassigned accounts and can connect before choosing an Agent', async () => { render(); expect(await screen.findByText('原有微信')).toBeVisible(); expect(screen.getAllByText('备用微信')).toHaveLength(2); const detail = await screen.findByRole('article', { name: '备用微信账号详情' }); expect(within(detail).getByText('尚未选择目标智能体')).toBeVisible(); fireEvent.click(within(detail).getByRole('button', { name: '扫码连接' })); expect(await within(detail).findByRole('img', { name: '个人微信登录二维码' })).toBeVisible(); expect(api.call).toHaveBeenCalledWith('channelAccountWechatBindStart', expect.objectContaining({ channel_account_id: 'wechat-spare' })); }); it('routes two independent accounts to the same Agent without starting a new QR login', async () => { render(); const spareDetail = await screen.findByRole('article', { name: '备用微信账号详情' }); fireEvent.change(within(spareDetail).getByLabelText('目标智能体'), { target: { value: agentA.slug } }); fireEvent.click(within(spareDetail).getByRole('button', { name: '保存目标' })); await waitFor(() => expect(api.call).toHaveBeenCalledWith('routeChannelAccount', expect.objectContaining({ channel_account_id: 'wechat-spare', target_agent_slug: agentA.slug, enabled: false, }))); expect(api.call.mock.calls.some(([operation]) => operation === 'channelAccountWechatBindStart')).toBe(false); }); it('switches an enabled legacy account target while preserving its enabled connection', async () => { render(); const detail = await screen.findByRole('article', { name: '原有微信账号详情' }); fireEvent.change(within(detail).getByLabelText('目标智能体'), { target: { value: agentB.slug } }); fireEvent.click(within(detail).getByRole('button', { name: '保存目标' })); await waitFor(() => expect(api.call).toHaveBeenCalledWith('routeChannelAccount', expect.objectContaining({ channel_account_id: 'wechat-legacy', target_agent_slug: agentB.slug, expected_revision: 7, enabled: true, }))); expect(api.call.mock.calls.some(([operation]) => String(operation).includes('WechatBindStart'))).toBe(false); });