Files
makelore/tests/e2e/cloud-agent-channels.spec.ts
T

712 lines
30 KiB
TypeScript

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<string, unknown> }[];
}
).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<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);
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<string, unknown> }[];
}
).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<string, unknown> }[];
}
).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);
}
});