feat: add user-level WeChat channel accounts
This commit is contained in:
@@ -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);
|
||||
|
||||
68
tests/unit/channel-accounts-page.test.tsx
Normal file
68
tests/unit/channel-accounts-page.test.tsx
Normal 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);
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user