395 lines
27 KiB
TypeScript
395 lines
27 KiB
TypeScript
import { act, cleanup, 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';
|
|
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudUserChannelAccount, type CloudWechatBindState } from '../../shared/cloud-agents';
|
|
|
|
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: CloudAgentDraft = { slug: 'ml-' + 'a'.repeat(32), name: '写作助手', purpose: '', system_prompt: '', published_version: 2, draft_revision: 3, updated_at: '', configuration: EMPTY_CLOUD_CONFIGURATION, enabled: true };
|
|
const agentB: CloudAgentDraft = { ...agentA, slug: 'ml-' + 'b'.repeat(32), name: '资料助手', published_version: 4, draft_revision: 5 };
|
|
const legacy: CloudUserChannelAccount = { 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: CloudUserChannelAccount = { 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' };
|
|
let accounts: CloudUserChannelAccount[];
|
|
let agents: CloudAgentDraft[];
|
|
let qrStatus: CloudWechatBindState['status'];
|
|
let verifyStatus: CloudWechatBindState['status'];
|
|
|
|
const calls = (operation: string) => api.call.mock.calls.filter(([name]) => name === operation);
|
|
const writes = () => api.call.mock.calls.filter(([name]) => !['channelAccounts', 'channelAccountWechatBindStatus'].includes(name));
|
|
function renderPage(entry = '/cloud-agents/channels') { return render(<MemoryRouter initialEntries={[entry]}><ChannelAccounts /></MemoryRouter>); }
|
|
async function addWechat(fakeTimers = false) {
|
|
fireEvent.click(await screen.findByRole('button', { name: '添加微信', exact: true }));
|
|
const dialog = screen.getByRole('dialog', { name: '添加微信', exact: true });
|
|
fireEvent.change(within(dialog).getByLabelText('选择智能体', { exact: true }), { target: { value: agentA.slug } });
|
|
fireEvent.change(within(dialog).getByLabelText('微信备注(可选)', { exact: true }), { target: { value: '创作微信' } });
|
|
if (fakeTimers) vi.useFakeTimers();
|
|
await act(async () => fireEvent.click(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true })));
|
|
return dialog;
|
|
}
|
|
async function manage(name = legacy.display_name) {
|
|
const card = await screen.findByRole('article', { name: name + '微信', exact: true });
|
|
fireEvent.click(within(card).getByRole('button', { name: /管理绑定|继续绑定/ }));
|
|
return screen.getByRole('dialog', { name: '管理微信绑定', exact: true });
|
|
}
|
|
function installMock() {
|
|
api.call.mockImplementation(async (operation: string, input: Record<string, unknown>) => {
|
|
if (operation === 'channelAccounts') return { items: accounts.map(item => ({ ...item })) };
|
|
if (operation === 'createChannelAccount') {
|
|
const channel = { ...spare, id: 'wechat-created', display_name: String(input.display_name), revision: 1 };
|
|
accounts.push(channel); return { channel };
|
|
}
|
|
const account = accounts.find(item => item.id === input.channel_account_id);
|
|
if (!account) throw new Error('Unknown fixture account');
|
|
if (operation === 'routeChannelAccount') {
|
|
const target = agents.find(item => item.slug === input.target_agent_slug);
|
|
Object.assign(account, { revision: account.revision + 1, target_agent_slug: target?.slug ?? null,
|
|
target_agent_name: target?.name ?? null, target_agent_published_version: target?.published_version ?? null,
|
|
enabled: input.enabled, blockers: target ? [] : ['target_required'] });
|
|
return { channel: { ...account } };
|
|
}
|
|
if (operation === 'enableChannelAccount' || operation === 'pauseChannelAccount') {
|
|
Object.assign(account, { revision: account.revision + 1, enabled: operation === 'enableChannelAccount' });
|
|
return { channel: { ...account } };
|
|
}
|
|
if (operation === 'disconnectChannelAccount') {
|
|
Object.assign(account, { revision: account.revision + 1, health: 'login_required', enabled: false });
|
|
return { channel: { ...account }, disconnected: true };
|
|
}
|
|
if (['channelAccountWechatBindStart', 'channelAccountWechatBindStatus', 'channelAccountWechatBindVerification'].includes(operation)) {
|
|
const status = operation === 'channelAccountWechatBindVerification' ? verifyStatus : qrStatus;
|
|
if (status === 'confirmed') account.health = 'connected';
|
|
return { session_key: 'qr-' + account.id, status, qrcode_url: 'https://login.example.test/qr' };
|
|
}
|
|
throw new Error('Unexpected fixture operation ' + operation);
|
|
});
|
|
}
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
accounts = [{ ...legacy }, { ...spare }]; agents = [agentA, agentB];
|
|
qrStatus = 'verification_required'; verifyStatus = 'pending';
|
|
api.list.mockImplementation(async () => ({ agents, next_cursor: null }));
|
|
api.recovery.mockResolvedValue({ recent: null, pending: [] });
|
|
installMock();
|
|
});
|
|
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
|
|
|
it('shows binding cards and never fetches or embeds conversation or activity panels', async () => {
|
|
renderPage();
|
|
const card = await screen.findByRole('article', { name: '原有微信微信', exact: true });
|
|
expect(within(card).getByText(agentA.name, { exact: true })).toBeVisible();
|
|
expect(within(card).getByRole('button', { name: '管理绑定', exact: true })).toBeVisible();
|
|
expect(await screen.findByRole('article', { name: '备用微信微信', exact: true })).toBeVisible();
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('微信对话', { exact: true })).not.toBeInTheDocument();
|
|
expect(screen.queryByText('活动记录', { exact: true })).not.toBeInTheDocument();
|
|
expect(writes()).toEqual([]);
|
|
expect(api.call.mock.calls.every(([operation]) => operation === 'channelAccounts')).toBe(true);
|
|
});
|
|
|
|
it('offers only available published Agents and needs a selection before connecting', async () => {
|
|
agents.push({ ...agentA, slug: 'ml-' + 'c'.repeat(32), name: '尚未制作好', published_version: null },
|
|
{ ...agentA, slug: 'ml-' + 'd'.repeat(32), name: '已停用助手', enabled: false });
|
|
renderPage();
|
|
fireEvent.click(await screen.findByRole('button', { name: '添加微信', exact: true }));
|
|
const dialog = screen.getByRole('dialog', { name: '添加微信', exact: true });
|
|
expect(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true })).toBeDisabled();
|
|
expect(within(dialog).queryByRole('option', { name: /尚未制作好|已停用助手/ })).not.toBeInTheDocument();
|
|
expect(within(dialog).getByRole('option', { name: agentA.name, exact: true })).toBeInTheDocument();
|
|
expect(writes()).toEqual([]);
|
|
});
|
|
|
|
it('creates, associates then requests a QR without enabling the channel', async () => {
|
|
renderPage(); const dialog = await addWechat();
|
|
expect(await within(dialog).findByRole('img', { name: '个人微信登录二维码' })).toBeVisible();
|
|
expect(writes().map(([operation]) => operation)).toEqual(['createChannelAccount', 'routeChannelAccount', 'channelAccountWechatBindStart']);
|
|
expect(calls('routeChannelAccount')[0][1]).toMatchObject({ channel_account_id: 'wechat-created', target_agent_slug: agentA.slug, expected_revision: 1, enabled: false });
|
|
expect(accounts.find(item => item.id === 'wechat-created')).toMatchObject({ enabled: false });
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it('keeps a pending verification visible and cannot claim usable or enable before confirmation', async () => {
|
|
renderPage(); const dialog = await addWechat();
|
|
const input = await within(dialog).findByLabelText('微信验证码', { exact: true });
|
|
fireEvent.change(input, { target: { value: '123456' } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '提交验证码', exact: true }));
|
|
await waitFor(() => expect(calls('channelAccountWechatBindVerification')).toHaveLength(1));
|
|
expect(within(dialog).getByRole('img', { name: '个人微信登录二维码' })).toBeVisible();
|
|
expect(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true })).toBeDisabled();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it('closes a confirmed QR, enables only with an explicit click and reports success afterward', async () => {
|
|
verifyStatus = 'confirmed';
|
|
renderPage(); const dialog = await addWechat();
|
|
fireEvent.change(await within(dialog).findByLabelText('微信验证码', { exact: true }), { target: { value: '123456' } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '提交验证码', exact: true }));
|
|
expect(await within(dialog).findByText('微信已连接', { exact: true })).toBeVisible();
|
|
expect(within(dialog).queryByRole('img', { name: '个人微信登录二维码' })).not.toBeInTheDocument();
|
|
expect(within(dialog).queryByLabelText('微信验证码')).not.toBeInTheDocument();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
await waitFor(() => expect(calls('enableChannelAccount')).toHaveLength(1));
|
|
expect(await within(dialog).findByRole('button', { name: '完成', exact: true })).toBeEnabled();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成', exact: true }));
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
expect(accounts.find(item => item.id === 'wechat-created')).toMatchObject({ enabled: true, health: 'connected', target_agent_slug: agentA.slug });
|
|
});
|
|
|
|
it('does not report binding success when enable fails after the QR is confirmed', async () => {
|
|
qrStatus = 'confirmed';
|
|
const base = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation(async (operation, input) => { if (operation === 'enableChannelAccount') throw new Error('启用失败'); return base(operation, input); });
|
|
renderPage(); const dialog = await addWechat();
|
|
fireEvent.click(await within(dialog).findByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
expect(await within(dialog).findByText(/启用失败/)).toBeVisible();
|
|
expect(within(dialog).queryByRole('button', { name: '完成', exact: true })).not.toBeInTheDocument();
|
|
expect(accounts.find(item => item.id === 'wechat-created')?.enabled).toBe(false);
|
|
});
|
|
|
|
it('ends QR polling once connected and does not automatically enable', async () => {
|
|
renderPage(); const dialog = await addWechat(true);
|
|
expect(within(dialog).getByRole('img', { name: '个人微信登录二维码' })).toBeVisible();
|
|
qrStatus = 'confirmed';
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(2000); });
|
|
expect(within(dialog).queryByRole('img', { name: '个人微信登录二维码' })).not.toBeInTheDocument();
|
|
expect(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true })).toBeVisible();
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(6000); });
|
|
expect(calls('channelAccountWechatBindStatus')).toHaveLength(1);
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it('changes an existing association only after confirmation and keeps its login and enabled state', async () => {
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.change(within(dialog).getByLabelText('绑定的智能体', { exact: true }), { target: { value: agentB.slug } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '保存更换', exact: true }));
|
|
expect(calls('routeChannelAccount')).toHaveLength(0);
|
|
const confirmation = screen.getByRole('dialog', { name: '更换绑定的智能体', exact: true });
|
|
fireEvent.click(within(confirmation).getByRole('button', { name: /确认更换/ }));
|
|
await waitFor(() => expect(calls('routeChannelAccount')).toHaveLength(1));
|
|
expect(calls('routeChannelAccount')[0][1]).toMatchObject({ channel_account_id: legacy.id, target_agent_slug: agentB.slug, expected_revision: 7, enabled: true });
|
|
expect(calls('createChannelAccount')).toHaveLength(0);
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
expect(accounts[0].health).toBe('connected');
|
|
});
|
|
|
|
it('lets independent WeChat accounts choose the same Agent without creating another slot', async () => {
|
|
accounts[1] = { ...spare, health: 'connected', target_agent_slug: agentB.slug, target_agent_name: agentB.name, target_agent_published_version: 4 };
|
|
renderPage(); const dialog = await manage(spare.display_name);
|
|
fireEvent.change(within(dialog).getByLabelText('绑定的智能体', { exact: true }), { target: { value: agentA.slug } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '保存更换', exact: true }));
|
|
const confirmation = screen.getByRole('dialog', { name: '更换绑定的智能体', exact: true });
|
|
fireEvent.click(within(confirmation).getByRole('button', { name: /确认更换/ }));
|
|
await waitFor(() => expect(calls('routeChannelAccount')).toHaveLength(1));
|
|
expect(accounts.every(item => item.target_agent_slug === agentA.slug)).toBe(true);
|
|
expect(accounts[1].enabled).toBe(false);
|
|
expect(calls('createChannelAccount')).toHaveLength(0);
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
});
|
|
|
|
it('pauses and resumes an existing binding without clearing its target or login', async () => {
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '暂停使用', exact: true }));
|
|
expect(await within(dialog).findByRole('button', { name: '恢复使用', exact: true })).toBeEnabled();
|
|
expect(accounts[0]).toMatchObject({ enabled: false, health: 'connected', target_agent_slug: agentA.slug });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '恢复使用', exact: true }));
|
|
await within(dialog).findByRole('button', { name: '暂停使用', exact: true });
|
|
expect(calls('pauseChannelAccount')[0][1]).toMatchObject({ channel_account_id: legacy.id, expected_revision: 7 });
|
|
expect(calls('enableChannelAccount')[0][1]).toMatchObject({ channel_account_id: legacy.id, expected_revision: 8 });
|
|
expect(calls('routeChannelAccount')).toHaveLength(0);
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
});
|
|
|
|
it('clears an association only after confirmation and preserves the WeChat login', async () => {
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '解除绑定', exact: true }));
|
|
expect(calls('routeChannelAccount')).toHaveLength(0);
|
|
const confirmation = screen.getByRole('dialog', { name: '解除智能体绑定', exact: true });
|
|
fireEvent.click(within(confirmation).getByRole('button', { name: '确认解除', exact: true }));
|
|
await waitFor(() => expect(calls('routeChannelAccount')).toHaveLength(1));
|
|
expect(calls('routeChannelAccount')[0][1]).toMatchObject({ channel_account_id: legacy.id, target_agent_slug: null, enabled: false, expected_revision: 7 });
|
|
expect(accounts[0]).toMatchObject({ health: 'connected', target_agent_slug: null, enabled: false });
|
|
expect(calls('disconnectChannelAccount')).toHaveLength(0);
|
|
expect(calls('channelAccountWechatUnbind')).toHaveLength(0);
|
|
});
|
|
|
|
it('disconnects only after confirmation and retains the selected Agent and account card', async () => {
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '断开微信连接', exact: true }));
|
|
expect(calls('disconnectChannelAccount')).toHaveLength(0);
|
|
const confirmation = screen.getByRole('dialog', { name: '断开微信连接', exact: true });
|
|
fireEvent.click(within(confirmation).getByRole('button', { name: '确认断开', exact: true }));
|
|
await waitFor(() => expect(calls('disconnectChannelAccount')).toHaveLength(1));
|
|
expect(accounts[0]).toMatchObject({ enabled: false, health: 'login_required', target_agent_slug: agentA.slug });
|
|
expect(calls('routeChannelAccount')).toHaveLength(0);
|
|
await within(dialog).findByRole('button', { name: '扫码连接', exact: true });
|
|
});
|
|
|
|
it('retries a failed association without creating another account and reuses the original request', async () => {
|
|
const base = api.call.getMockImplementation()!;
|
|
let attempts = 0;
|
|
api.call.mockImplementation(async (operation, input) => { if (operation === 'routeChannelAccount' && ++attempts === 1) throw new Error('绑定未完成'); return base(operation, input); });
|
|
renderPage(); const dialog = await addWechat();
|
|
expect(await within(dialog).findByText('绑定未完成', { exact: true })).toBeVisible();
|
|
expect(calls('createChannelAccount')).toHaveLength(1);
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true }));
|
|
await within(dialog).findByRole('img', { name: '个人微信登录二维码' });
|
|
expect(calls('createChannelAccount')).toHaveLength(1);
|
|
expect(calls('routeChannelAccount')).toHaveLength(2);
|
|
expect(calls('routeChannelAccount')[0][1]).toEqual(calls('routeChannelAccount')[1][1]);
|
|
});
|
|
|
|
it('continues an unfinished account without creating another account slot', async () => {
|
|
accounts = [{ ...spare, health: 'connected' }];
|
|
renderPage(); const dialog = await manage(spare.display_name);
|
|
fireEvent.change(within(dialog).getByLabelText('绑定的智能体', { exact: true }), { target: { value: agentA.slug } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true }));
|
|
expect(await within(dialog).findByRole('button', { name: '完成绑定并启用', exact: true })).toBeEnabled();
|
|
expect(calls('createChannelAccount')).toHaveLength(0);
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
expect(calls('routeChannelAccount')[0][1]).toMatchObject({ channel_account_id: spare.id, target_agent_slug: agentA.slug });
|
|
});
|
|
|
|
it('does not erase the old association or claim success after a rejected change', async () => {
|
|
const base = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation(async (operation, input) => { if (operation === 'routeChannelAccount') throw new Error('渠道状态已变化'); return base(operation, input); });
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.change(within(dialog).getByLabelText('绑定的智能体', { exact: true }), { target: { value: agentB.slug } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '保存更换', exact: true }));
|
|
fireEvent.click(within(screen.getByRole('dialog', { name: '更换绑定的智能体', exact: true })).getByRole('button', { name: '确认更换', exact: true }));
|
|
await screen.findByText('渠道状态已变化', { exact: true });
|
|
expect(accounts[0].target_agent_slug).toBe(agentA.slug);
|
|
expect(within(dialog).getByLabelText('绑定的智能体', { exact: true })).toHaveValue(agentB.slug);
|
|
expect(screen.queryByText('已更换绑定的智能体,微信连接保留。')).not.toBeInTheDocument();
|
|
});
|
|
|
|
|
|
it.each(['failed', 'expired'] as const)('cannot enable an unsuccessful %s QR connection and can explicitly refresh it', async (status) => {
|
|
qrStatus = status;
|
|
renderPage(); const dialog = await addWechat();
|
|
expect(await within(dialog).findByRole('button', { name: '刷新二维码', exact: true })).toBeEnabled();
|
|
expect(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true })).toBeDisabled();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
qrStatus = 'confirmed';
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '刷新二维码', exact: true }));
|
|
expect(await within(dialog).findByText('微信已连接', { exact: true })).toBeVisible();
|
|
expect(calls('channelAccountWechatBindStart')[1][1]).toMatchObject({ channel_account_id: 'wechat-created', force: true });
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it('does not call a blocked or unconfirmed existing account ready to use', async () => {
|
|
accounts = [{ ...legacy, health: 'unknown' }, { ...legacy, id: spare.id, display_name: spare.display_name, blockers: ['worker_offline'] }];
|
|
renderPage();
|
|
const unknown = await screen.findByRole('article', { name: '原有微信微信', exact: true });
|
|
const blocked = screen.getByRole('article', { name: '备用微信微信', exact: true });
|
|
expect(within(unknown).queryByText('可以使用', { exact: true })).not.toBeInTheDocument();
|
|
expect(within(blocked).queryByText('可以使用', { exact: true })).not.toBeInTheDocument();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
|
|
it('keeps adding unavailable after a failed initial read instead of showing a false empty account list', async () => {
|
|
api.call.mockRejectedValue(new Error('微信列表暂时不可用'));
|
|
renderPage();
|
|
await screen.findByText('微信列表暂时不可用', { exact: true });
|
|
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeDisabled();
|
|
expect(screen.queryByText('还没有微信绑定', { exact: true })).not.toBeInTheDocument();
|
|
installMock();
|
|
fireEvent.click(screen.getByRole('button', { name: '刷新微信列表', exact: true }));
|
|
expect(await screen.findByRole('article', { name: '原有微信微信', exact: true })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled();
|
|
});
|
|
|
|
it('allows the optional WeChat remark to stay empty', async () => {
|
|
renderPage();
|
|
fireEvent.click(await screen.findByRole('button', { name: '添加微信', exact: true }));
|
|
const dialog = screen.getByRole('dialog', { name: '添加微信', exact: true });
|
|
fireEvent.change(within(dialog).getByLabelText('选择智能体', { exact: true }), { target: { value: agentA.slug } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true }));
|
|
await within(dialog).findByRole('img', { name: '个人微信登录二维码' });
|
|
expect(calls('createChannelAccount')).toHaveLength(1);
|
|
expect(calls('createChannelAccount')[0][1].display_name).toMatch(/\S/);
|
|
});
|
|
|
|
|
|
it('completes a reconnected already enabled account without sending another enable request', async () => {
|
|
accounts = [{ ...legacy, health: 'login_required' }]; qrStatus = 'confirmed';
|
|
renderPage(); const dialog = await manage();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '扫码连接', exact: true }));
|
|
expect(await within(dialog).findByText('微信已连接', { exact: true })).toBeVisible();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
expect(await within(dialog).findByRole('button', { name: '完成', exact: true })).toBeVisible();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
expect(calls('createChannelAccount')).toHaveLength(0);
|
|
expect(calls('routeChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it.each(['channelAccountWechatBindStart', 'channelAccountWechatBindVerification'])('blocks new slots and QR actions while %s has an uncertain result', async operation => {
|
|
accounts = [{ ...legacy, health: 'login_required' }];
|
|
api.recovery.mockResolvedValue({ recent: null, pending: [{ id: 'uncertain-wechat', operation, input: { channel_account_id: legacy.id }, created_at: '2026-10-12T00:00:00Z' }] });
|
|
renderPage('/cloud-agents/channels?account=' + legacy.id);
|
|
const dialog = await screen.findByRole('dialog', { name: '管理微信绑定', exact: true });
|
|
expect(screen.getByRole('button', { name: '添加微信', exact: true, hidden: true })).toBeDisabled();
|
|
expect(within(dialog).getByRole('button', { name: '扫码连接', exact: true })).toBeDisabled();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '扫码连接', exact: true }));
|
|
expect(calls('channelAccountWechatBindStart')).toHaveLength(0);
|
|
expect(calls('createChannelAccount')).toHaveLength(0);
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
});
|
|
|
|
it('uses the latest account state when enabling after QR confirmation', async () => {
|
|
qrStatus = 'confirmed'; renderPage(); const dialog = await addWechat();
|
|
await within(dialog).findByText('微信已连接', { exact: true });
|
|
const created = accounts.find(item => item.id === 'wechat-created')!;
|
|
created.revision = 77;
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
await within(dialog).findByRole('button', { name: '完成', exact: true });
|
|
expect(calls('enableChannelAccount')[0][1]).toMatchObject({ channel_account_id: 'wechat-created', expected_revision: 77 });
|
|
});
|
|
|
|
it('does not enable when the authoritative account has become blocked after QR confirmation', async () => {
|
|
qrStatus = 'confirmed'; renderPage(); const dialog = await addWechat();
|
|
await within(dialog).findByText('微信已连接', { exact: true });
|
|
accounts.find(item => item.id === 'wechat-created')!.blockers = ['worker_offline'];
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
expect(await within(dialog).findByText('微信还没有准备好,请刷新连接状态后再试。', { exact: true })).toBeVisible();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
expect(within(dialog).queryByRole('button', { name: '完成', exact: true })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not silently enable another Agent if the association changes while scanning', async () => {
|
|
qrStatus = 'confirmed'; renderPage(); const dialog = await addWechat();
|
|
await within(dialog).findByText('微信已连接', { exact: true });
|
|
const created = accounts.find(item => item.id === 'wechat-created')!;
|
|
Object.assign(created, { target_agent_slug: agentB.slug, target_agent_name: agentB.name });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
expect(await within(dialog).findByText('绑定的智能体已发生变化,请返回上一步确认。', { exact: true })).toBeVisible();
|
|
expect(calls('enableChannelAccount')).toHaveLength(0);
|
|
expect(within(dialog).queryByRole('button', { name: '完成', exact: true })).not.toBeInTheDocument();
|
|
});
|
|
|
|
|
|
it('does not show successful enable or completed binding when the enable receipt remains disabled', async () => {
|
|
qrStatus = 'confirmed';
|
|
const base = api.call.getMockImplementation()!;
|
|
api.call.mockImplementation(async (operation, input) => {
|
|
if (operation === 'enableChannelAccount') return { channel: { ...accounts.find(item => item.id === input.channel_account_id)!, enabled: false } };
|
|
return base(operation, input);
|
|
});
|
|
renderPage(); const dialog = await addWechat();
|
|
fireEvent.click(await within(dialog).findByRole('button', { name: '完成绑定并启用', exact: true }));
|
|
await within(dialog).findByText('微信还没有准备好,请刷新连接状态后再试。', { exact: true });
|
|
expect(calls('enableChannelAccount')).toHaveLength(1);
|
|
expect(within(dialog).queryByRole('heading', { name: '绑定完成啦', exact: true })).not.toBeInTheDocument();
|
|
expect(within(dialog).queryByRole('button', { name: '完成', exact: true })).not.toBeInTheDocument();
|
|
expect(screen.queryByText('微信绑定已启用。', { exact: true })).not.toBeInTheDocument();
|
|
expect(accounts.find(item => item.id === 'wechat-created')?.enabled).toBe(false);
|
|
});
|