fix(agents): explain missing channel targets

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 11:32:07 +08:00
1 parent 13336cfe12
commit 8289f27f3e
5 files changed
+222 -9

No files matched your search

+63
View File
@@ -1,5 +1,68 @@
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);
+84 -5
View File
@@ -1,5 +1,5 @@
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { ChannelAccounts } from '@/pages/CloudAgents/ChannelAccounts';
import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft, type CloudUserChannelAccount, type CloudWechatBindState } from '../../shared/cloud-agents';
@@ -23,7 +23,16 @@ let verifyStatus: CloudWechatBindState['status'];
const calls = (operation: string) => api.call.mock.calls.filter(([name]) => name === operation);
const writes = () => api.call.mock.calls.filter(([name]) => !['channelAccounts', 'channelAccountWechatBindStatus'].includes(name));
function renderPage(entry = '/cloud-agents/channels') { return render(<MemoryRouter initialEntries={[entry]}><ChannelAccounts /></MemoryRouter>); }
function LocationProbe() {
const location = useLocation();
return <output data-testid="location-path">{location.pathname + location.search}</output>;
}
function renderPage(entry = '/cloud-agents/channels') {
return render(<MemoryRouter initialEntries={[entry]}><Routes>
<Route path="/cloud-agents/channels" element={<ChannelAccounts />} />
<Route path="/cloud-agents" element={<div>智能体列表</div>} />
</Routes><LocationProbe /></MemoryRouter>);
}
async function addWechat(fakeTimers = false) {
fireEvent.click(await screen.findByRole('button', { name: '添加微信', exact: true }));
const dialog = screen.getByRole('dialog', { name: '添加微信', exact: true });
@@ -80,6 +89,69 @@ beforeEach(() => {
});
afterEach(() => { cleanup(); vi.useRealTimers(); });
it.each([
['no Agents', []],
['only drafts', [{ ...agentA, published_version: null }]],
['only disabled Agents', [{ ...agentA, enabled: false }]],
] satisfies [string, CloudAgentDraft[]][])('guides a workspace with %s to My Agents without starting a binding', async (_name, unavailable) => {
agents = unavailable; accounts = [];
renderPage();
expect(await screen.findByRole('heading', { name: '还没有可连接的智能体', exact: true })).toBeVisible();
expect(screen.getByText('先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。', { exact: true })).toBeVisible();
expect(screen.queryByRole('button', { name: '添加第一个微信', exact: true })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '去我的智能体', exact: true }));
expect(await screen.findByText('智能体列表', { exact: true })).toBeVisible();
expect(screen.getByTestId('location-path')).toHaveTextContent('/cloud-agents?view=mine');
expect(writes()).toEqual([]);
});
it('uses the same preparation hint in the add dialog without duplicating it on the page', async () => {
agents = []; accounts = [];
renderPage();
await screen.findByRole('heading', { name: '还没有可连接的智能体', exact: true });
fireEvent.click(screen.getByRole('button', { name: '添加微信', exact: true }));
const dialog = screen.getByRole('dialog', { name: '添加微信', exact: true });
expect(within(dialog).getByRole('heading', { name: '还没有可连接的智能体', exact: true })).toBeVisible();
expect(within(dialog).getByText('先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。', { exact: true })).toBeVisible();
expect(screen.getAllByRole('heading', { name: '还没有可连接的智能体', exact: true, hidden: true })).toHaveLength(1);
expect(within(dialog).getByRole('button', { name: '下一步:扫码连接', exact: true })).toBeDisabled();
fireEvent.click(within(dialog).getByRole('button', { name: '去我的智能体', exact: true }));
expect(await screen.findByText('智能体列表', { exact: true })).toBeVisible();
expect(screen.getByTestId('location-path')).toHaveTextContent('/cloud-agents?view=mine');
expect(writes()).toEqual([]);
});
it('waits for the Agent read before showing a preparation hint', async () => {
accounts = [];
let finish!: (value: { agents: CloudAgentDraft[]; next_cursor: null }) => void;
api.list.mockImplementation(() => new Promise(resolve => { finish = resolve; }));
renderPage();
expect(screen.getByText('正在查看你的微信绑定…', { exact: true })).toBeVisible();
expect(screen.queryByRole('heading', { name: '还没有可连接的智能体', exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '去我的智能体', exact: true })).not.toBeInTheDocument();
await act(async () => finish({ agents: [], next_cursor: null }));
expect(await screen.findByRole('heading', { name: '还没有可连接的智能体', exact: true })).toBeVisible();
expect(writes()).toEqual([]);
});
it('keeps existing WeChat cards manageable when no connectable Agent remains', async () => {
agents = [];
renderPage();
expect(await screen.findByRole('heading', { name: '还没有可连接的智能体', exact: true })).toBeVisible();
expect(screen.getByRole('article', { name: '原有微信微信', exact: true })).toBeVisible();
expect(screen.getByRole('article', { name: '备用微信微信', exact: true })).toBeVisible();
const dialog = await manage();
expect(within(dialog).getByRole('button', { name: '暂停使用', exact: true })).toBeEnabled();
expect(within(dialog).getByRole('button', { name: '解除绑定', exact: true })).toBeEnabled();
expect(within(dialog).getByRole('button', { name: '断开微信连接', exact: true })).toBeEnabled();
fireEvent.click(within(dialog).getByRole('button', { name: '暂停使用', exact: true }));
await waitFor(() => expect(calls('pauseChannelAccount')).toHaveLength(1));
expect(accounts[0]).toMatchObject({ enabled: false, health: 'connected', target_agent_slug: agentA.slug });
expect(calls('createChannelAccount')).toHaveLength(0);
expect(calls('routeChannelAccount')).toHaveLength(0);
});
it('shows binding cards and never fetches or embeds conversation or activity panels', async () => {
renderPage();
const card = await screen.findByRole('article', { name: '原有微信微信', exact: true });
@@ -89,6 +161,7 @@ it('shows binding cards and never fetches or embeds conversation or activity pan
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByText('微信对话', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('活动记录', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '还没有可连接的智能体', exact: true })).not.toBeInTheDocument();
expect(writes()).toEqual([]);
expect(api.call.mock.calls.every(([operation]) => operation === 'channelAccounts')).toBe(true);
});
@@ -295,13 +368,19 @@ it('does not call a blocked or unconfirmed existing account ready to use', async
});
it('keeps adding unavailable after a failed initial read instead of showing a false empty account list', async () => {
api.call.mockRejectedValue(new Error('微信列表暂时不可用'));
it.each(['accounts', 'agents'])('keeps adding unavailable after a failed initial %s read instead of showing a false empty list', async source => {
agents = [];
const message = source === 'accounts' ? '微信列表暂时不可用' : '智能体列表暂时不可用';
if (source === 'accounts') api.call.mockRejectedValue(new Error(message));
else api.list.mockRejectedValue(new Error(message));
renderPage();
await screen.findByText('微信列表暂时不可用', { exact: true });
await screen.findByText(message, { exact: true });
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeDisabled();
expect(screen.queryByText('还没有微信绑定', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '还没有可连接的智能体', exact: true })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '去我的智能体', exact: true })).not.toBeInTheDocument();
installMock();
api.list.mockImplementation(async () => ({ agents, next_cursor: null }));
fireEvent.click(screen.getByRole('button', { name: '刷新微信列表', exact: true }));
expect(await screen.findByRole('article', { name: '原有微信微信', exact: true })).toBeVisible();
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled();