import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { CloudChannelPanel } from '@/pages/CloudAgents/CloudChannelPanel'; const api = vi.hoisted(() => ({ call: vi.fn(), recovery: vi.fn(), resolvePending: vi.fn(), list: vi.fn() })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); vi.mock('@/pages/CloudAgents/CloudChannelConversations', () => ({ CloudChannelConversations: () =>
本人微信对话
, })); const slug = 'ml-' + 'a'.repeat(32); const binding = { id: 'wechat-1', address: 'wechat://1', display_name: '我的微信', target_agent_address: 'agent://1', route_revision: 3, provider_generation: 'provider-1', binding_id: 'binding-1', enabled: false, status: 'bound', worker_online: true, agent_slug: slug, published_version: 2, desired_state: 'paused', sync_state: 'ready', health: 'connected', revision: 3, blockers: [], access_mode: 'self_only', policy_revision: 1, }; const bindingB = { ...binding, id: 'wechat-2', address: 'wechat://2', display_name: '备用微信', target_agent_address: 'agent://2', binding_id: 'binding-2', provider_generation: 'provider-2', agent_slug: slug, }; beforeEach(() => { vi.resetAllMocks(); api.recovery.mockResolvedValue({ recent: null, pending: [] }); api.call.mockImplementation(async (operation: string) => { if (operation === 'channelBindings') return { items: [binding], available_accounts: [] }; if (operation === 'budget') return { request_limit_points: '2.00', daily_limit_points: '10.00' }; if (operation === 'channelCallers') return { items: [] }; if (operation === 'channelActivity') return { items: [], next_offset: null }; if (operation === 'channelOperation') return { operation_id: 'receipt', status: 'failed' }; if (operation === 'enableChannelBinding' || operation === 'pauseChannelBinding' || operation === 'disconnectChannelBinding') return { channel: { ...binding, enabled: true, desired_state: 'enabled' } }; if (operation === 'channelPolicy') return { binding_id: 'binding-1', channel_account_id: 'wechat-1', access_mode: 'invited', policy_revision: 2, revoked_caller_ids: [], revoked_invitation_ids: [] }; if (operation === 'createChannelPairing') return { binding_id: 'binding-1', provider_generation: 'provider-1', kind: 'self', consumed: false, code: 'ABC123' }; throw new Error(`Unexpected operation ${operation}`); }); }); afterEach(cleanup); it('keeps the connection action available when recovery is still loading at the first click', async () => { let finishRecovery!: (value: { recent: null; pending: [] }) => void; api.recovery.mockReturnValue(new Promise(resolve => { finishRecovery = resolve; })); const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation: string, input) => operation === 'wechatBindStart' || operation === 'wechatBindStatus' ? { session_key: 'new-qr', status: 'pending', qrcode_url: 'https://wechat.example.test/login' } : base(operation, input)); render(); fireEvent.click(await screen.findByRole('button', { name: '重新连接', exact: true })); expect(api.call.mock.calls.some(call => call[0] === 'wechatBindStart')).toBe(false); expect(screen.getByRole('button', { name: '重新连接', exact: true })).toBeEnabled(); await act(async () => { finishRecovery({ recent: null, pending: [] }); }); fireEvent.click(screen.getByRole('button', { name: '重新连接', exact: true })); await screen.findByRole('img', { name: '个人微信登录二维码' }); expect(api.call.mock.calls.filter(call => call[0] === 'wechatBindStart')).toHaveLength(1); }); it('requires an explicit enable confirmation and sends the typed binding operation once', async () => { render(); const enable = await screen.findByRole('button', { name: '启用渠道' }); expect(api.call.mock.calls.some(call => call[0] === 'enableChannelBinding')).toBe(false); fireEvent.click(enable); expect(screen.getByRole('dialog')).toHaveTextContent('确认启用'); await screen.findByText('每次任务上限:2.00 词元点数'); fireEvent.click(screen.getByRole('button', { name: '确认启用' })); await waitFor(() => expect(api.call.mock.calls.some(call => call[0] === 'enableChannelBinding')).toBe(true)); const input = api.call.mock.calls.find(call => call[0] === 'enableChannelBinding')?.[1]; expect(input).toEqual(expect.objectContaining({ slug, channel_account_id: 'wechat-1', expected_revision: 3 })); expect(typeof input.operation_id).toBe('string'); }); it('uses the pairing code command and does not mutate when the access tab is opened', async () => { render(); await screen.findByRole('button', { name: '使用权限' }); fireEvent.click(screen.getByRole('button', { name: '使用权限' })); fireEvent.click(await screen.findByRole('button', { name: '生成本人配对码' })); await screen.findByText('ABC123'); expect(api.call.mock.calls.some(call => call[0] === 'channelPolicy')).toBe(false); expect(api.call.mock.calls.find(call => call[0] === 'createChannelPairing')?.[1]).toEqual(expect.objectContaining({ kind: 'self', channel_account_id: 'wechat-1' })); }); it('looks up delivery by the result message ID before offering file retry', async () => { const activity = { run_id: 'run-1', request_id: 'request-1', status: 'completed', created_at: '2026-09-13T00:00:00Z', delivery: { state: 'known', result: { message_id: 'result-message-1' } }, }; api.call.mockImplementation(async (operation: string) => { if (operation === 'channelBindings') return { items: [binding], available_accounts: [] }; if (operation === 'channelActivity') return { items: [activity], next_offset: null }; if (operation === 'channelDelivery') return { message_id: 'result-message-1', status: 'partial', parts: [{ part_id: 'part-1', type: 'file', status: 'failed' }] }; if (operation === 'retryChannelDeliveryPart') return { message_id: 'result-message-1', status: 'queued', parts: [] }; if (operation === 'channelCallers') return { items: [] }; throw new Error(`Unexpected operation ${operation}`); }); render(); fireEvent.click(await screen.findByRole('button', { name: '活动记录' })); fireEvent.click(await screen.findByRole('button', { name: '重新补发文件' })); await waitFor(() => expect(api.call).toHaveBeenCalledWith('retryChannelDeliveryPart', { slug, channel_account_id: 'wechat-1', logical_message_id: 'result-message-1', part_id: 'part-1', })); }); it('requires an explicit recovery click and reuses the durable original channel operation', async () => { api.recovery.mockResolvedValue({ recent: null, pending: [{ id: 'channelPolicy:pending', operation: 'channelPolicy', created_at: '2026-09-13T00:00:00Z', input: { slug, channel_account_id: 'wechat-1', operation_id: 'original-operation', access_mode: 'invited', expected_policy_revision: 1 }, }] }); api.resolvePending.mockResolvedValue({ operation: 'channelPolicy', result: { binding_id: 'binding-1', channel_account_id: 'wechat-1', access_mode: 'invited', policy_revision: 2, revoked_caller_ids: [], revoked_invitation_ids: [] } }); render(); const recover = await screen.findByRole('button', { name: '恢复上次操作' }); expect(api.resolvePending).not.toHaveBeenCalled(); fireEvent.click(recover); await waitFor(() => expect(api.resolvePending).toHaveBeenCalledWith('channelPolicy:pending', false)); expect(api.call.mock.calls.some(call => call[0] === 'channelPolicy')).toBe(false); }); it('can explicitly dismiss a timed-out channel operation without starting a mutation', async () => { api.recovery.mockResolvedValue({ recent: null, pending: [{ id: 'retryChannelDeliveryPart:pending', operation: 'retryChannelDeliveryPart', created_at: '2026-09-13T00:00:00Z', input: { slug, channel_account_id: 'wechat-1', logical_message_id: 'result-1', part_id: 'part-1' }, }] }); render(); const dismiss = await screen.findByRole('button', { name: '移除提醒' }); fireEvent.click(dismiss); await waitFor(() => expect(api.resolvePending).toHaveBeenCalledWith('retryChannelDeliveryPart:pending', true)); expect(api.call.mock.calls.some(call => call[0] === 'retryChannelDeliveryPart')).toBe(false); }); it('opens the current verification input when recovery requires a new code', async () => { const pending = { id: 'wechatBindVerification:pending', operation: 'wechatBindVerification', created_at: '2026-09-13T00:00:00Z', input: { slug, channel_account_id: 'wechat-1', session_key: 'session-1', operation_id: 'operation-1' }, }; api.recovery.mockResolvedValue({ recent: null, pending: [pending] }); api.resolvePending.mockResolvedValue({ operation: 'wechatBindVerification', requires_input: true, input: pending.input }); api.call.mockImplementation(async (operation: string) => { if (operation === 'channelBindings') return { items: [binding], available_accounts: [] }; if (operation === 'channelCallers') return { items: [] }; if (operation === 'channelActivity') return { items: [], next_offset: null }; if (operation === 'wechatBindStatus') return { session_key: 'session-1', status: 'verification_required', message: '等待验证码' }; throw new Error(`Unexpected operation ${operation}`); }); render(); fireEvent.click(await screen.findByRole('button', { name: '恢复上次操作' })); await waitFor(() => expect(screen.getByLabelText('微信验证码')).toBeInTheDocument()); expect(screen.getByText('上次扫码需要新的验证码,请输入后重新提交')).toBeInTheDocument(); expect(screen.queryByText('已恢复上次渠道操作,状态已经刷新')).toBeNull(); expect(api.call.mock.calls.some(call => call[0] === 'wechatBindVerification')).toBe(false); }); it('drops an old QR session when switching bindings and ignores its late status', async () => { let releaseOldStatus!: (value: unknown) => void; const oldStatus = new Promise(resolve => { releaseOldStatus = resolve; }); let statusCalls = 0; api.call.mockImplementation(async (operation: string, input: Record) => { if (operation === 'channelBindings') return { items: [binding, bindingB], available_accounts: [] }; if (operation === 'channelCallers') return { items: [] }; if (operation === 'channelActivity') return { items: [], next_offset: null }; if (operation === 'wechatBindStart') return { session_key: 'session-a', status: 'waiting', qrcode_url: 'https://provider.example/qr-a' }; if (operation === 'wechatBindStatus' && input.session_key === 'session-a') { statusCalls += 1; return statusCalls === 1 ? oldStatus : { session_key: 'session-a', status: 'waiting', qrcode_url: 'https://provider.example/qr-a' }; } throw new Error(`Unexpected operation ${operation}`); }); render(); fireEvent.click(await screen.findByRole('button', { name: '重新连接' })); await screen.findByText('等待中'); fireEvent.change(await screen.findByLabelText('当前渠道'), { target: { value: 'wechat-2' } }); await waitFor(() => expect(screen.queryByText('等待中')).toBeNull()); await new Promise(resolve => setTimeout(resolve, 0)); const callsAfterSwitch = statusCalls; releaseOldStatus({ session_key: 'session-a', status: 'waiting', qrcode_url: 'https://provider.example/qr-a' }); await waitFor(() => expect(screen.queryByText('等待中')).toBeNull()); expect(statusCalls).toBe(callsAfterSwitch); expect(screen.queryByAltText('个人微信登录二维码')).toBeNull(); }); it('clears the previous binding pairing, callers, and activity when switching accounts', async () => { const callerA = { caller_id: 'caller-a', access_mode: 'invited', grant_state: 'authorized', created_at: '2026-09-13T00:00:00Z' }; const activityA = { run_id: 'run-a', request_id: 'request-a', status: 'completed', token_usage: { total_tokens: 7 }, delivery: { state: 'known' }, timing: { created_at: '2026-09-13T00:00:00Z' }, }; api.call.mockImplementation(async (operation: string, input: Record) => { if (operation === 'channelBindings') return { items: [binding, bindingB], available_accounts: [] }; if (operation === 'channelCallers') return { items: input.channel_account_id === 'wechat-1' ? [callerA] : [] }; if (operation === 'channelActivity') return { items: input.channel_account_id === 'wechat-1' ? [activityA] : [], next_offset: null }; if (operation === 'createChannelPairing') return { binding_id: 'binding-1', provider_generation: 'provider-1', kind: 'self', consumed: false, code: 'PAIR-A' }; throw new Error(`Unexpected operation ${operation}`); }); render(); fireEvent.click(await screen.findByRole('button', { name: '使用权限' })); await screen.findByText('受邀联系人', { exact: true }); fireEvent.click(screen.getByRole('button', { name: '生成本人配对码' })); await screen.findByText('PAIR-A'); fireEvent.click(screen.getByRole('button', { name: '活动记录' })); await screen.findByText('词元 7'); fireEvent.click(screen.getByRole('button', { name: '连接微信' })); fireEvent.change(await screen.findByLabelText('当前渠道'), { target: { value: 'wechat-2' } }); await waitFor(() => expect(screen.queryByText('PAIR-A')).toBeNull()); fireEvent.click(screen.getByRole('button', { name: '使用权限' })); await screen.findByText('还没有已配对的调用者。'); expect(screen.queryByText('受邀联系人', { exact: true })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '活动记录' })); await screen.findByText('还没有渠道活动。'); expect(screen.queryByText('词元 7')).toBeNull(); }); it('loads a later published target when the route agent list has a next cursor', async () => { api.list.mockResolvedValueOnce({ agents: [{ slug: 'ml-' + 'b'.repeat(32), name: '未发布', published_version: null }], next_cursor: 'cursor-2' }) .mockResolvedValueOnce({ agents: [{ slug: 'ml-' + 'c'.repeat(32), name: '第二页目标', published_version: 1 }], next_cursor: null }); render(); fireEvent.click(await screen.findByRole('button', { name: '选择目标智能体' })); await waitFor(() => expect(screen.getByRole('option', { name: /第二页目标/ })).toBeInTheDocument()); expect(api.list.mock.calls.some(call => call[0] === 'cursor-2')).toBe(true); }); it('removes file retry after queued delivery and keeps it hidden after a sent refresh', async () => { const activity = { run_id: 'run-2', request_id: 'request-2', status: 'completed', created_at: '2026-09-13T00:00:00Z', delivery: { state: 'known', result: { message_id: 'result-message-2' } }, }; let deliveryState: 'failed' | 'queued' | 'sent' = 'failed'; let deliveryCalls = 0; api.call.mockImplementation(async (operation: string) => { if (operation === 'channelBindings') return { items: [binding], available_accounts: [] }; if (operation === 'channelCallers') return { items: [] }; if (operation === 'channelActivity') return { items: [activity], next_offset: null }; if (operation === 'channelDelivery') { deliveryCalls += 1; const status = deliveryCalls === 1 ? 'failed' : deliveryState; return { message_id: 'result-message-2', status, parts: [{ part_id: 'part-2', type: 'file', status }] }; } if (operation === 'retryChannelDeliveryPart') { deliveryState = 'queued'; return { message_id: 'result-message-2', status: 'queued', parts: [] }; } throw new Error(`Unexpected operation ${operation}`); }); render(); fireEvent.click(await screen.findByRole('button', { name: '活动记录' })); const retry = await screen.findByRole('button', { name: '重新补发文件' }); expect(screen.getByText('微信投递:发送失败')).toBeVisible(); fireEvent.click(retry); await waitFor(() => expect(api.call).toHaveBeenCalledWith('retryChannelDeliveryPart', { slug, channel_account_id: 'wechat-1', logical_message_id: 'result-message-2', part_id: 'part-2', })); await waitFor(() => expect(screen.queryByRole('button', { name: '重新补发文件' })).toBeNull()); deliveryState = 'sent'; fireEvent.click(screen.getByRole('button', { name: '刷新', exact: true })); await waitFor(() => expect(deliveryCalls).toBeGreaterThanOrEqual(3)); await screen.findByText('微信投递:已送达'); expect(screen.queryByRole('button', { name: '重新补发文件' })).toBeNull(); }); it('does not label a known execution receipt as a successful WeChat delivery', async () => { const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation: string, input) => operation === 'channelActivity' ? { items: [{ run_id: 'run-unknown', status: 'completed', delivery: { state: 'known', core: { state: 'unknown' } } }], next_offset: null } : base(operation, input)); render(); fireEvent.click(await screen.findByRole('button', { name: '活动记录' })); await screen.findByText('微信投递:状态未知'); expect(screen.queryByText('微信投递:已送达')).toBeNull(); }); it('retains verification recovery while the corresponding channel binding is still loading', async () => { const pending = { id: 'waiting-bindings', operation: 'wechatBindVerification', created_at: '2026-09-13T00:00:00Z', input: { slug, channel_account_id: 'wechat-1', session_key: 'original-session', operation_id: 'original-verify' } }; let bindingsReady!: (value: { items: typeof binding[]; available_accounts: [] }) => void; const delayedBindings = new Promise(resolve => { bindingsReady = resolve; }); const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation: string, input) => operation === 'channelBindings' ? delayedBindings : operation === 'wechatBindStatus' ? { session_key: 'original-session', status: 'verification_required' } : base(operation, input)); api.recovery.mockResolvedValue({ recent: null, pending: [pending] }); api.resolvePending.mockImplementation(async (_id, discard) => { if (discard) { api.recovery.mockResolvedValue({ recent: null, pending: [] }); return { discarded: true }; } return { requires_input: true, operation: pending.operation, input: pending.input }; }); render(); fireEvent.click(await screen.findByRole('button', { name: '恢复上次操作' })); await screen.findByText(/尚未读取到该操作的微信连接/); expect(api.resolvePending).not.toHaveBeenCalled(); await act(async () => { bindingsReady({ items: [binding], available_accounts: [] }); }); fireEvent.click(screen.getByRole('button', { name: '恢复上次操作' })); await screen.findByLabelText('微信验证码'); expect(api.resolvePending).toHaveBeenCalledWith('waiting-bindings', false); expect(api.resolvePending).toHaveBeenCalledWith('waiting-bindings', true); }); it('refreshes the recovery gate after a temporary local journal read failure', async () => { api.recovery.mockRejectedValueOnce(new Error('本机记录暂时无法读取')).mockResolvedValue({ recent: null, pending: [] }); const base = api.call.getMockImplementation()!; api.call.mockImplementation(async (operation: string, input) => operation === 'wechatBindStart' || operation === 'wechatBindStatus' ? { session_key: 'retry-read', status: 'pending', qrcode_url: 'https://wechat.example.test/login' } : base(operation, input)); render(); await screen.findByText('本机记录暂时无法读取'); fireEvent.click(screen.getByRole('button', { name: '重试', exact: true })); await waitFor(() => expect(api.recovery).toHaveBeenCalledTimes(2)); await waitFor(() => expect(screen.queryByText('本机记录暂时无法读取')).toBeNull()); fireEvent.click(await screen.findByRole('button', { name: '重新连接', exact: true })); await screen.findByRole('img', { name: '个人微信登录二维码' }); });