feat: add user-level WeChat channel accounts

This commit is contained in:
2026-09-14 00:26:29 +08:00
parent 46e7f6f1cd
commit b7d8b1298f
15 changed files with 575 additions and 63 deletions

View File

@@ -2,6 +2,8 @@ import { closeElectronApp, expect, getStableWindow, test } from './fixtures/elec
test('own WeChat conversation approval and generated PPT delivery in the desktop workspace', async ({ launchElectronApp }, testInfo) => {
const app = await launchElectronApp({ skipSetup: true });
const agentSlug = 'ml-' + 'a'.repeat(32);
const secondAgentSlug = 'ml-' + 'b'.repeat(32);
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
@@ -16,8 +18,12 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
const slug = 'ml-' + 'a'.repeat(32);
const configuration = { model: 'test-model', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [], tool_approval_mode: 'default', max_execution_steps: 40, max_output_tokens: 4096, max_run_seconds: 600 };
const draft = { slug, name: '我的创作助手', purpose: '整理选题与制作演示文稿', system_prompt: '使用简洁自然的中文', draft_revision: 2, updated_at: '2026-09-13T00:00:00Z', configuration, published_version: 1, enabled: true, archived: false };
const secondSlug = 'ml-' + 'b'.repeat(32);
const secondDraft = { ...draft, slug: secondSlug, name: '资料整理助手', draft_revision: 1, published_version: 1 };
const session = { session_id: 'self-session', caller_id: 'self-caller', agent_slug: slug, agent_name: draft.name, binding_id: 'binding-a', provider_generation: 'generation-a', channel_account_id: 'wechat-a', access_mode: 'self_only', grant_state: 'authorized', session_state: 'active', sequence: 1, thread_id: 'channel-thread', created_at: '2026-09-13T01:00:00Z' };
const channel = { id: 'wechat-a', address: 'wechat:personal', display_name: '创作微信', target_agent_address: 'yuxi:makelore:' + slug, route_revision: 3, provider_generation: 'generation-a', binding_id: 'binding-a', enabled: true, status: 'active', worker_online: true, agent_slug: slug, published_version: 1, desired_state: 'enabled', sync_state: 'ready', health: 'connected', last_confirmed_at: '2026-09-13T01:00:00Z', revision: 3, blockers: [], access_mode: 'self_only', policy_revision: 0 };
const channel = { id: 'wechat-a', address: 'wechat:personal', display_name: '创作微信', target_agent_address: 'yuxi:makelore:' + slug, route_revision: 3, provider_generation: 'generation-a', binding_id: 'binding-a', enabled: true, status: 'active', worker_online: true, agent_slug: slug, published_version: 1, desired_state: 'enabled', sync_state: 'ready', health: 'connected', last_confirmed_at: '2026-09-13T01:00:00Z', revision: 3, blockers: [], access_mode: 'self_only', policy_revision: 0, target_agent_slug: slug, target_agent_name: draft.name, target_agent_published_version: 1 };
const spare = { id: 'wechat-b', address: 'wechat:spare', display_name: '备用微信', target_agent_address: null, route_revision: 1, provider_generation: 'generation-b', binding_id: 'binding-b', enabled: false, status: 'paused', worker_online: true, agent_slug: null, published_version: null, desired_state: 'disabled', sync_state: 'ready', health: 'login_required', last_confirmed_at: null, revision: 1, blockers: ['target_required'], access_mode: 'self_only', policy_revision: 0, target_agent_slug: null, target_agent_name: null, target_agent_published_version: null };
let channelAccounts = [channel, spare];
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string; body?: string }) => {
@@ -30,7 +36,7 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
if (path.endsWith('/recovery/recent')) return result({ saved: true });
if (path.endsWith('/files/save-channel')) { calls.push({ operation: 'downloadChannelArtifact', input: body }); return result({ saved: true }); }
if (!path.endsWith('/actions')) return result(path.endsWith('/agents') || path.endsWith('/bootstrap') ? { agents: [draft], next_cursor: null } : draft);
if (!path.endsWith('/actions')) return result(path.endsWith('/agents') || path.endsWith('/bootstrap') ? { agents: [draft, secondDraft], next_cursor: null } : path.endsWith(secondSlug) ? secondDraft : draft);
const { operation, input = {} } = body;
calls.push({ operation, input });
if (operation === 'threads') return result({ threads: [], next_offset: null });
@@ -40,12 +46,27 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
if (operation === 'access') return result({ published_version: 1, enabled: true, versions: [], grants: [], applications: [], share_url: 'niancode://agents/' + slug });
if (operation === 'budget') return result({ request_limit_points: '2.50', daily_limit_points: '10.00', daily_committed_points: '0.20' });
if (operation === 'costs') return result({ items: [], next_offset: null, summary: { count: 0, settled_points: '0', pending_points: '0' } });
if (operation === 'channelBindings') return result({ items: [channel], available_accounts: [] });
if (operation === 'channelAccounts') return result({ items: channelAccounts });
if (operation === 'routeChannelAccount') {
const target = input.target_agent_slug === secondSlug ? secondDraft : input.target_agent_slug === slug ? draft : null;
channelAccounts = channelAccounts.map(item => item.id !== input.channel_account_id ? item : { ...item, revision: item.revision + 1, enabled: input.enabled,
target_agent_slug: target?.slug ?? null, target_agent_name: target?.name ?? null, target_agent_published_version: target?.published_version ?? null,
agent_slug: target?.slug ?? null, published_version: target?.published_version ?? null, target_agent_address: target ? 'yuxi:makelore:' + target.slug : null,
blockers: target ? [] : ['target_required'] });
return result({ channel: channelAccounts.find(item => item.id === input.channel_account_id), target_agent: target?.slug ?? null });
}
if (operation === 'enableChannelAccount' || operation === 'pauseChannelAccount') {
const enabled = operation === 'enableChannelAccount';
channelAccounts = channelAccounts.map(item => item.id !== input.channel_account_id ? item : { ...item, revision: item.revision + 1, enabled, desired_state: enabled ? 'enabled' : 'disabled' });
return result({ channel: channelAccounts.find(item => item.id === input.channel_account_id) });
}
if (operation === 'disconnectChannelAccount') return result({ channel: { ...channelAccounts.find(item => item.id === input.channel_account_id), health: 'login_required', enabled: false }, disconnected: true });
if (operation === 'channelBindings') return result({ items: channelAccounts.filter(item => item.target_agent_slug === input.slug), available_accounts: [] });
if (operation === 'channelConversations') return result({ items: [session], next_offset: null });
if (operation === 'channelCallers' || operation === 'channelSelfCallers') return result({ items: [{ ...session, paired_ws_account_id: 'creator', revoked_at: null }] });
if (operation === 'channelActivity') return result({ items: [], next_offset: null });
if (operation === 'wechatBindStart' || operation === 'wechatBindStatus') return result({ session_key: 'qr-session', status: verified ? 'confirmed' : 'verification_required', qrcode_url: 'https://login.example.test/confirm?token=fixture', message: '请确认微信验证码' });
if (operation === 'wechatBindVerification') { verified = true; return result({ session_key: 'qr-session', status: 'confirmed', message: '连接已确认' }); }
if (operation === 'wechatBindStart' || operation === 'wechatBindStatus' || operation === 'channelAccountWechatBindStart' || operation === 'channelAccountWechatBindStatus') return result({ session_key: 'qr-session', status: verified ? 'confirmed' : 'verification_required', qrcode_url: 'https://login.example.test/confirm?token=fixture', message: '请确认微信验证码' });
if (operation === 'wechatBindVerification' || operation === 'channelAccountWechatBindVerification') { verified = true; channelAccounts = channelAccounts.map(item => item.id === input.channel_account_id ? { ...item, health: 'connected' } : item); return result({ session_key: 'qr-session', status: 'confirmed', message: '连接已确认' }); }
if (operation === 'createChannelPairing') return result({ invitation_id: 'pairing-a', binding_id: 'binding-a', provider_generation: 'generation-a', agent_slug: slug, kind: input.kind, expires_at: '2026-09-13T23:00:00Z', consumed: false, code: 'TEST1234' });
if (operation === 'channelConversation') return result({ ...session, messages: [{ id: 1, role: 'user', content: '把今天的创作建议整理成演示文稿。' }, ...(approved ? [{ id: 2, role: 'assistant', content: '## 演示文稿已完成\n共 7 页,文件已保存在本次会话中。' }] : [])], queued_requests: [], next_offset: null,
run: { agent_run_id: 'channel-run', request_id: 'channel-request', thread_id: 'channel-thread', agent_slug: slug, status: approved ? 'completed' : 'interrupted', output: '', version: '1',
@@ -74,34 +95,46 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
expect(calls.find(call => call.operation === 'downloadChannelArtifact')?.input).toEqual({ session_id: 'self-session', path: '/outputs/创作建议.pptx' });
expect(calls.some(call => call.operation === 'resume' || call.operation === 'submit')).toBe(false);
await page.getByRole('button', { name: '发布与访问', exact: true }).click();
await expect(page.getByText('创作微信', { exact: true }).first()).toBeVisible();
await page.getByRole('region', { name: '个人微信渠道', exact: true }).scrollIntoViewIfNeeded();
await page.getByRole('button', { name: '重新连接', exact: true }).click();
const linked = page.getByRole('region', { name: '已关联渠道', exact: true });
await expect(linked.getByText('创作微信', { exact: true })).toBeVisible();
await expect(page.getByRole('region', { name: '个人微信渠道', exact: true })).toHaveCount(0);
await linked.scrollIntoViewIfNeeded();
await page.screenshot({ path: testInfo.outputPath('agent-linked-channels-shortcut.png') });
await linked.getByRole('button', { name: /创作微信/ }).click();
await expect(page.getByRole('heading', { name: '微信', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: /创作微信.*我的创作助手/ })).toBeVisible();
await expect(page.getByRole('button', { name: /备用微信.*未分配智能体/ })).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('wechat-account-list.png'), fullPage: true });
await page.getByRole('button', { name: /备用微信.*未分配智能体/ }).click();
const spareDetail = page.getByRole('article', { name: '备用微信账号详情', exact: true });
await spareDetail.getByRole('button', { name: '扫码连接', exact: true }).click();
await expect(page.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveAttribute('src', /^data:image\//);
await page.getByLabel('微信验证码', { exact: true }).scrollIntoViewIfNeeded();
await page.screenshot({ path: testInfo.outputPath('wechat-qr-verification.png') });
await page.screenshot({ path: testInfo.outputPath('wechat-unassigned-qr-and-target.png'), fullPage: true });
await page.getByLabel('微信验证码', { exact: true }).fill('123456');
await page.getByRole('button', { name: '提交验证码', exact: true }).click();
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: unknown }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'wechatBindVerification'))).toBe(true);
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: unknown }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'channelAccountWechatBindVerification'))).toBe(true);
if (await page.getByRole('button', { name: '关闭二维码', exact: true }).count()) await page.getByRole('button', { name: '关闭二维码', exact: true }).click();
await page.getByRole('button', { name: '使用权限', exact: true }).click();
await expect(page.getByText('使用绑定时扫码的微信,直接发送消息即可。', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '生成本人配对码', exact: true })).toHaveCount(0);
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'createChannelPairing'))).toBe(false);
await page.screenshot({ path: testInfo.outputPath('wechat-access-without-pairing.png') });
await page.getByRole('button', { name: '连接微信', exact: true }).click();
await page.screenshot({ path: testInfo.outputPath('wechat-publication.png') });
await spareDetail.getByLabel('目标智能体', { exact: true }).selectOption(agentSlug);
await spareDetail.getByRole('button', { name: '保存目标', exact: true }).click();
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[] }).cloudChannelFixtureCalls
.filter(call => call.operation === 'routeChannelAccount').some(call => call.input.channel_account_id === 'wechat-b' && call.input.target_agent_slug === 'ml-' + 'a'.repeat(32)))).toBe(true);
await expect(page.getByRole('button', { name: /备用微信.*我的创作助手/ })).toBeVisible();
await page.getByRole('button', { name: /创作微信.*我的创作助手/ }).click();
const legacyDetail = page.getByRole('article', { name: '创作微信账号详情', exact: true });
const qrStartsBeforeSwitch = await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.filter(call => call.operation === 'channelAccountWechatBindStart').length);
await legacyDetail.getByLabel('目标智能体', { exact: true }).selectOption(secondAgentSlug);
await legacyDetail.getByRole('button', { name: '保存目标', exact: true }).click();
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[] }).cloudChannelFixtureCalls
.filter(call => call.operation === 'routeChannelAccount').some(call => call.input.channel_account_id === 'wechat-a' && call.input.target_agent_slug === 'ml-' + 'b'.repeat(32) && call.input.enabled === true))).toBe(true);
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.filter(call => call.operation === 'channelAccountWechatBindStart').length)).toBe(qrStartsBeforeSwitch);
await expect(page.getByRole('button', { name: '使用权限', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '活动记录', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '本人微信对话', exact: true })).toBeVisible();
await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; win.setMinimumSize(800, 600); win.setSize(980, 680); });
await expect.poll(() => page.getByTestId('main-content').evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
await page.getByRole('heading', { name: '发布到个人微信', exact: true }).scrollIntoViewIfNeeded();
await page.screenshot({ path: testInfo.outputPath('wechat-publication-compact.png') });
await page.getByRole('button', { name: '自动任务', exact: true }).click();
await page.getByRole('button', { name: '添加任务', exact: true }).click();
await expect(page.getByLabel('结果发送到')).toHaveValue('');
await expect(page.getByRole('option', { name: '创作微信 · 本人对话 1' })).toBeAttached();
await page.getByLabel('结果发送到').selectOption('self-caller');
await page.getByLabel('结果发送到').scrollIntoViewIfNeeded();
await page.screenshot({ path: testInfo.outputPath('wechat-schedule-target.png') });
await page.screenshot({ path: testInfo.outputPath('wechat-account-detail-compact.png'), fullPage: true });
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'createChannelPairing'))).toBe(false);
} finally {
await app.evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }).catch(() => undefined);
await closeElectronApp(app);

View File

@@ -0,0 +1,68 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { 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', access_mode: 'self_only', policy_revision: 1,
};
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', access_mode: 'self_only', policy_revision: 0,
};
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 === 'channelBindings') return { items: [{ ...legacy, address: 'wechat:legacy', target_agent_address: 'agent:legacy', route_revision: 7, worker_online: true, agent_slug: agentA.slug, published_version: 2, desired_state: 'enabled', sync_state: 'ready' }], available_accounts: [] };
if (operation === 'channelCallers') return { items: [] };
throw new Error(`Unexpected operation ${operation}`);
});
});
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);
});

View File

@@ -201,6 +201,24 @@ describe('Main cloud Agents boundary', () => {
});
});
it('routes user-level channel accounts without requiring an Agent slug', () => {
const list = operationPlan({ operation: 'channelAccounts', input: {} });
expect(list.path).toBe('/api/makelore/channel-accounts');
expect(list.project({ items: [{ id: 'wechat-1', display_name: '工作微信', revision: 2, enabled: false, status: 'paused', health: 'connected',
blockers: ['target_required'], target_agent_slug: null, target_agent_name: null, target_agent_published_version: null, access_token: 'private' }] })).toEqual({
items: [{ id: 'wechat-1', display_name: '工作微信', revision: 2, enabled: false, status: 'paused', health: 'connected',
blockers: ['target_required'], target_agent_slug: null, target_agent_name: null, target_agent_published_version: null }],
});
const route = operationPlan({ operation: 'routeChannelAccount', input: { channel_account_id: 'wechat-1', operation_id: 'op-route',
target_agent_slug: draft.slug, expected_revision: 2, enabled: true } });
expect(route.path).toBe('/api/makelore/channel-accounts/wechat-1/route');
expect(route.body).toEqual({ operation_id: 'op-route', target_agent_slug: draft.slug, expected_revision: 2, enabled: true });
expect(() => operationPlan({ operation: 'routeChannelAccount', input: { channel_account_id: 'wechat-1', operation_id: 'op-route',
target_agent_slug: null, expected_revision: 2, enabled: true } })).toThrow('invalid_input');
expect(operationPlan({ operation: 'channelAccountOperation', input: { operation_id: 'op-route' } }).path)
.toBe('/api/makelore/channel-operations/op-route');
});
it('recovers a completed WeChat verification from its receipt without replaying the code', async () => {
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost response'))
.mockResolvedValueOnce(json({ operation_id: input.operation_id, status: 'completed', result: { channel: { id: 'channel-1' } } }));
@@ -216,6 +234,19 @@ describe('Main cloud Agents boundary', () => {
expect(fetchImpl.mock.calls[2][0]).toContain(`/api/makelore/channel-operations/${input.operation_id}?agent_slug=${draft.slug}`);
});
it('recovers user-level WeChat verification without an Agent slug', async () => {
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost response'))
.mockResolvedValueOnce(json({ operation_id: input.operation_id, status: 'completed', result: { channel: { id: 'channel-1' } } }));
const module = moduleFor(fetchImpl);
const verification = { channel_account_id: 'channel-1', operation_id: input.operation_id, session_key: 'session-1', verify_code: 'secret-code-123' };
await expect(module.execute({ operation: 'channelAccountWechatBindVerification', input: verification })).rejects.toMatchObject({ code: 'cloud_service_unavailable' });
const pending = (await module.recovery()).pending[0];
expect(pending.input).not.toHaveProperty('verify_code');
expect(await module.resolvePending({ id: pending.id })).toMatchObject({ operation: 'channelAccountWechatBindVerification', result: { status: 'completed' } });
expect(fetchImpl.mock.calls[2][0]).toContain(`/api/makelore/channel-operations/${input.operation_id}`);
expect(fetchImpl.mock.calls[2][0]).not.toContain('agent_slug');
});
it('asks for a new WeChat verification code only when its receipt is absent', async () => {
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost response'))
.mockResolvedValueOnce(json({ detail: { code: 'operation_not_found' } }, 404));