135 lines
9.3 KiB
TypeScript
135 lines
9.3 KiB
TypeScript
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: () => <img alt="个人微信登录二维码" src="data:image/png;base64,test" /> }));
|
|
|
|
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<string, unknown>) => {
|
|
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(<MemoryRouter><ChannelAccounts /></MemoryRouter>);
|
|
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(<MemoryRouter initialEntries={['/cloud-agents/channels?account=wechat-legacy']}><ChannelAccounts /></MemoryRouter>);
|
|
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(<MemoryRouter initialEntries={['/cloud-agents/channels?account=wechat-spare']}><ChannelAccounts /></MemoryRouter>);
|
|
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(<MemoryRouter initialEntries={['/cloud-agents/channels?account=wechat-spare']}><ChannelAccounts /></MemoryRouter>);
|
|
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(<MemoryRouter initialEntries={['/cloud-agents/channels?account=wechat-spare']}><ChannelAccounts /></MemoryRouter>);
|
|
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(<MemoryRouter initialEntries={['/cloud-agents/channels?account=wechat-legacy']}><ChannelAccounts /></MemoryRouter>);
|
|
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);
|
|
});
|