import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; test('guides an empty channel workspace from the sidebar back to My Agents', async ({ launchElectronApp, }, testInfo) => { const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); const applicationUrl = page.url(); await page.goto('about:blank'); await app.evaluate(({ ipcMain, BrowserWindow }) => { BrowserWindow.getAllWindows()[0].setSize(1366, 900); const calls: { method: string; path: string; operation?: string }[] = []; Object.assign(globalThis, { cloudChannelEmptyFixtureCalls: calls }); 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 }) => { const path = request.path ?? ''; if (path === '/api/auth/session/sync') return result({ success: true, session: { accessToken: 'ui-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false, }, }); if (path === '/api/auth/me') return result({ success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [], }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true }, }); if (path === '/api/works/user/agent-profile') return result({ success: true, profile: { display_name: '创作者', age: null, gender: null, avatar_url: null, share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true, completed: true, version: 1, updated_at: '2026-10-13T00:00:00Z', }, }); if (!path.startsWith('/api/cloud-agents/')) return result({ success: true }); const body = typeof request.body === 'string' ? JSON.parse(request.body) : (request.body ?? {}); calls.push({ method: request.method ?? 'GET', path, operation: body.operation }); if (path.endsWith('/recovery')) return result({ recent: null, pending: [] }); if (path.endsWith('/agents') || path.endsWith('/bootstrap')) return result({ agents: [], next_cursor: null }); if (path.endsWith('/actions') && body.operation === 'channelAccounts') return result({ items: [] }); throw new Error('Unexpected empty channel fixture request ' + path); } ); }); await page.goto(applicationUrl); await page.getByTestId('ai-module-option-cloud_agents').click(); const sidebar = page.getByTestId('sidebar-cloud-agents-navigation'); await sidebar.getByRole('button', { name: '渠道', exact: true }).click(); await expect(page).toHaveURL(/\/cloud-agents\/channels$/); const hint = page.getByRole('heading', { name: '还没有可连接的智能体', exact: true }); await expect(hint).toBeVisible(); await expect( page.getByText('先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。', { exact: true }) ).toBeVisible(); await expect(page.getByRole('button', { name: '添加第一个微信', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled(); await page.screenshot({ path: testInfo.outputPath('wechat-no-agents-desktop.png'), fullPage: true }); await page.getByRole('button', { name: '添加微信', exact: true }).click(); const dialog = page.getByRole('dialog', { name: '添加微信', exact: true }); await expect( dialog.getByRole('heading', { name: '还没有可连接的智能体', exact: true }) ).toBeVisible(); await expect(dialog.getByRole('button', { name: '下一步:扫码连接', exact: true })).toBeDisabled(); await dialog.getByRole('button', { name: '关闭', exact: true }).click(); await app.evaluate(({ BrowserWindow }) => { const window = BrowserWindow.getAllWindows()[0]; window.setMinimumSize(800, 600); window.setSize(980, 680); }); await expect(hint).toBeVisible(); await expect .poll(() => page.getByTestId('main-content').evaluate((element) => element.scrollWidth - element.clientWidth) ) .toBeLessThanOrEqual(1); await page.screenshot({ path: testInfo.outputPath('wechat-no-agents-compact.png'), fullPage: true }); await page.getByRole('button', { name: '去我的智能体', exact: true }).click(); await expect(page).toHaveURL(/\/cloud-agents\?view=mine$/); await expect(page.getByTestId('cloud-agents-page')).toBeVisible(); await expect( page.getByRole('heading', { name: '做一个属于你的 AI 助手吧', exact: true }) ).toBeVisible(); await expect(sidebar.getByRole('button', { name: '我的智能体', exact: true })).toHaveAttribute( 'aria-current', 'page' ); const calls = await app.evaluate( () => ( globalThis as typeof globalThis & { cloudChannelEmptyFixtureCalls: { method: string; operation?: string }[]; } ).cloudChannelEmptyFixtureCalls ); expect(calls.filter((call) => call.operation).map((call) => call.operation)).toEqual([ 'channelAccounts', ]); expect(calls.every((call) => call.method === 'GET' || call.operation === 'channelAccounts')).toBe( true ); } finally { await app .evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }) .catch(() => undefined); await closeElectronApp(app); } }); test('WeChat binding cards connect an account and switch its Agent through the channel 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(); const applicationUrl = page.url(); await page.goto('about:blank'); await app.evaluate(({ ipcMain, BrowserWindow }) => { BrowserWindow.getAllWindows()[0].setSize(1366, 900); const calls: { operation: string; input: unknown }[] = []; Object.assign(globalThis, { cloudChannelFixtureCalls: calls }); let verified = false; let rejectUnbind = true; 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 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 }) => { const path = request.path ?? ''; if (path === '/api/auth/session/sync') return result({ success: true, session: { accessToken: 'ui-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false, }, }); if (path === '/api/auth/me') return result({ success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [], }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true }, }); if (path === '/api/works/user/agent-profile') return result({ success: true, profile: { display_name: '创作者', age: null, gender: null, avatar_url: null, share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true, completed: true, version: 1, updated_at: '2026-09-13T00:00:00Z', }, }); if (!path.startsWith('/api/cloud-agents/')) return result({ success: true }); const body = typeof request.body === 'string' ? JSON.parse(request.body) : (request.body ?? {}); if (path.endsWith('/recovery')) return result({ recent: null, pending: [] }); if (path.endsWith('/recovery/recent')) return result({ saved: true }); 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 }); if (operation === 'history') return result({ thread_id: 'desktop-thread', messages: [], run: null, next_offset: null }); if (operation === 'attachments' || operation === 'files') return result({ attachments: [], files: [] }); if (operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {}, pricing: null, }); 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 === 'channelAccounts') return result({ items: channelAccounts }); if (operation === 'createChannelAccount') { const created = { ...spare, id: 'wechat-created', display_name: input.display_name, binding_id: 'binding-created', }; channelAccounts.push(created); return result({ channel: created }); } if (operation === 'routeChannelAccount') { const stale = { ...channelAccounts.find((item) => item.id === input.channel_account_id) }; 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: stale, 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 === 'unbindChannelAccount') { if (rejectUnbind) { rejectUnbind = false; return { ok: true, data: { status: 409, ok: false, json: { error: '解除暂时失败,请重试', code: 'channel_operation_conflict', commandOutcome: 'definitive_failure', }, }, }; } channelAccounts = channelAccounts.map((item) => item.id === input.channel_account_id ? { ...item, health: 'login_required', enabled: false, desired_state: 'paused', sync_state: 'ready', target_agent_slug: null, target_agent_name: null, target_agent_address: '', target_agent_published_version: null, agent_slug: null, published_version: null, provider_generation: 'logged-out-generation', revision: item.revision + 1, } : item ); return result({ channel: channelAccounts.find((item) => item.id === input.channel_account_id), disconnected: true, unbound: true, }); } if (operation === 'channelBindings') return result({ items: channelAccounts.filter((item) => item.target_agent_slug === input.slug), available_accounts: [], }); if ( 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 === '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 === 'schedules') return result({ jobs: [] }); if (operation === 'scheduleContext') return result({ version: 1, enabled: true, configuration, result_destination: '任务历史', payer: 'creator', }); throw new Error('Unexpected channel UI fixture operation ' + operation); } ); }); await page.goto(applicationUrl); await page.getByTestId('ai-module-option-cloud_agents').click(); await page .getByRole('article', { name: '我的创作助手', exact: true }) .getByRole('button', { name: '和它聊聊', exact: true }) .click(); await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: '我的创作助手', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0); await expect(page.getByRole('navigation', { name: '对话来源', exact: true })).toHaveCount(0); await expect(page.getByRole('region', { name: '微信会话', exact: true })).toHaveCount(0); await page.getByRole('button', { name: '返回智能体列表', exact: true }).click(); await page.getByRole('button', { name: '渠道', exact: true }).click(); await expect(page.getByRole('heading', { name: '连接到微信', exact: true })).toBeVisible(); const legacyCard = page.getByRole('article', { name: '创作微信微信', exact: true }); const spareCard = page.getByRole('article', { name: '备用微信微信', exact: true }); await expect(legacyCard.getByText('我的创作助手', { exact: true })).toBeVisible(); await expect(spareCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible(); await expect(page.getByRole('dialog')).toHaveCount(0); await expect(page.getByText('活动记录', { exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0); await page.screenshot({ path: testInfo.outputPath('wechat-binding-cards.png'), fullPage: true }); await page.getByRole('button', { name: '添加微信', exact: true }).click(); const adding = page.getByRole('dialog', { name: '添加微信', exact: true }); await adding .getByRole('radiogroup', { name: '选择智能体' }) .getByText('我的创作助手', { exact: true }) .click(); await expect(adding.getByRole('radio', { name: '我的创作助手', exact: true })).toBeChecked(); await adding.getByRole('button', { name: '下一步:扫码连接', exact: true }).click(); await expect(adding.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveAttribute( 'src', /^data:image\// ); await expect .poll(async () => app.evaluate(() => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record }[]; } ).cloudChannelFixtureCalls .filter((call) => call.operation === 'routeChannelAccount') .some( (call) => call.input.channel_account_id === 'wechat-created' && call.input.target_agent_slug === 'ml-' + 'a'.repeat(32) && call.input.enabled === false ) ) ) .toBe(true); await expect(adding.getByRole('button', { name: '完成绑定并启用', exact: true })).toHaveCount(0); await adding.getByLabel('微信验证码', { exact: true }).fill('123456'); await adding.getByRole('button', { name: '提交验证码', exact: true }).click(); await expect(adding.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveCount(0); await expect(adding.getByLabel('微信验证码', { exact: true })).toHaveCount(0); await expect(adding.getByRole('heading', { name: '微信绑定成功', exact: true })).toBeVisible(); expect( await app.evaluate( () => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] } ).cloudChannelFixtureCalls.filter((call) => call.operation === 'enableChannelAccount').length ) ).toBe(1); await page.screenshot({ path: testInfo.outputPath('wechat-automatically-connected.png'), fullPage: true, }); await expect(adding.getByRole('button', { name: '完成', exact: true })).toBeVisible(); await adding.getByRole('button', { name: '完成', exact: true }).click(); await expect( page .getByRole('article', { name: '微信 3微信', exact: true }) .getByText('我的创作助手', { exact: true }) ).toBeVisible(); await legacyCard.getByRole('button', { name: '更换智能体', exact: true }).click(); const managing = page.getByRole('dialog', { name: '更换智能体', exact: true }); const qrStartsBeforeSwitch = await app.evaluate( () => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] } ).cloudChannelFixtureCalls.filter((call) => call.operation === 'channelAccountWechatBindStart') .length ); await managing .getByRole('radiogroup', { name: '选择智能体' }) .getByText('资料整理助手', { exact: true }) .click(); await expect(managing.getByRole('radio', { name: '资料整理助手', exact: true })).toBeChecked(); await expect(page.getByRole('dialog')).toHaveCount(1); await page.screenshot({ path: testInfo.outputPath('wechat-agent-choice.png'), fullPage: true }); await managing.getByRole('button', { name: '确认更换', exact: true }).click(); await expect .poll(async () => app.evaluate(() => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record }[]; } ).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); await expect(managing).toHaveCount(0); await expect(page.getByText('已更换智能体,微信连接和历史保留。', { exact: true })).toBeVisible(); expect( await app.evaluate( () => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] } ).cloudChannelFixtureCalls.filter((call) => call.operation === 'channelAccountWechatBindStart') .length ) ).toBe(qrStartsBeforeSwitch); await expect(page.getByText('活动记录', { exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0); await expect(page.getByText(/本人|受邀|邀请|使用范围|使用权限|授权调用者/)).toHaveCount(0); 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.screenshot({ path: testInfo.outputPath('wechat-binding-cards-compact.png'), fullPage: true, }); await expect(legacyCard.getByText('资料整理助手', { exact: true })).toBeVisible(); const finalCalls = await app.evaluate( () => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record }[]; } ).cloudChannelFixtureCalls ); expect( finalCalls.some((call) => [ 'channelActivity', 'channelConversations', 'channelConversation', 'channelConversationControl', 'channelFiles', 'downloadChannelArtifact', 'channelPolicy', 'createChannelPairing', 'channelCallers', 'channelSelfCallers', 'revokeChannelCaller', 'submit', 'preview', 'resume', ].includes(call.operation) ) ).toBe(false); expect(finalCalls.filter((call) => call.operation === 'createChannelAccount')).toHaveLength(1); expect(finalCalls.find((call) => call.operation === 'createChannelAccount')?.input).toEqual({ display_name: '微信 3', operation_id: expect.any(String), }); expect( finalCalls.find((call) => call.operation === 'channelAccountWechatBindVerification')?.input ).toEqual({ channel_account_id: 'wechat-created', session_key: 'qr-session', verify_code: '123456', operation_id: expect.any(String), }); expect(finalCalls.find((call) => call.operation === 'enableChannelAccount')?.input).toEqual({ channel_account_id: 'wechat-created', expected_revision: 2, operation_id: expect.any(String), }); expect( finalCalls.filter((call) => call.operation === 'routeChannelAccount').map((call) => call.input) ).toEqual([ { channel_account_id: 'wechat-created', target_agent_slug: agentSlug, expected_revision: 1, enabled: false, operation_id: expect.any(String), }, { channel_account_id: 'wechat-a', target_agent_slug: secondAgentSlug, expected_revision: 3, enabled: true, operation_id: expect.any(String), }, ]); await legacyCard.getByRole('button', { name: '更多操作', exact: true }).click(); await page.screenshot({ path: testInfo.outputPath('wechat-binding-more-compact.png'), fullPage: true }); await expect(legacyCard.getByRole('button', { name: '暂停自动回复', exact: true })).toBeVisible(); await legacyCard.getByRole('button', { name: '解除微信绑定', exact: true }).click(); const unbinding = page.getByRole('dialog', { name: '解除微信绑定', exact: true }); await unbinding.getByRole('button', { name: '确认解除微信绑定', exact: true }).click(); await expect(unbinding.getByRole('alert')).toHaveText(/解除暂时失败/); await expect(page.getByRole('dialog')).toHaveCount(1); await page.screenshot({ path: testInfo.outputPath('wechat-unbind-visible-failure.png'), fullPage: true, }); await unbinding.getByRole('button', { name: '确认解除微信绑定', exact: true }).click(); await expect(unbinding).toHaveCount(0); await expect(legacyCard.getByText('未绑定', { exact: true })).toBeVisible(); await expect(legacyCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible(); await expect(page.getByText('断开微信连接', { exact: true })).toHaveCount(0); await page.screenshot({ path: testInfo.outputPath('wechat-unbound-card-compact.png'), fullPage: true, }); const unbindCalls = await app.evaluate(() => ( globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record }[]; } ).cloudChannelFixtureCalls.filter((call) => call.operation === 'unbindChannelAccount') ); expect(unbindCalls).toHaveLength(2); expect(unbindCalls[0].input).toEqual(unbindCalls[1].input); } finally { await app .evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }) .catch(() => undefined); await closeElectronApp(app); } });