fix(agents): explain missing channel targets
This commit is contained in:
1 parent
13336cfe12
commit
8289f27f3e
5 files changed
+222
-9
No files matched your search
@@ -0,0 +1,65 @@
|
||||
# Task: Explain unavailable WeChat channel binding without an Agent
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20261013-channel-empty-hint-4e8a2b6c
|
||||
- Mode: Feature
|
||||
- Branch: codex/20261013-channel-empty-hint-4e8a2b6c-channel-empty-hint
|
||||
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261013-channel-empty-hint-4e8a2b6c
|
||||
- Base commit: 13336cfe12d241a098cd7f0f250c2fcc787ca760
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Show a visible explanation on the Agent channel page when there is no available published/enabled Agent, with a route to the existing My Agents list.
|
||||
- Clarify the same prerequisite in the Add/Manage WeChat dialog. Preserve existing channel cards, management and eligibility rules.
|
||||
- Synchronize README and focused unit/Electron UI coverage.
|
||||
|
||||
## Project Context Loaded
|
||||
|
||||
- Concurrent Task Gate: Passed. `task_context.py status --json` matches this task ID, worktree, branch, feature mode and base commit; primary checkout belongs to a separate integration task and remains untouched.
|
||||
- Planning Gate: Passed. Read the entry instructions and active record; retained already-loaded memory-index, project-positioning, current-state, decision-index, system-overview, business-rules and success-criteria from the same canonical snapshot at `23ddce11`. Base `13336cf` adds only this chat's already-verified homepage feature and its own task record.
|
||||
- Accepted authority: ADR-2026-09-11-personal-cloud-agents; user-level channel accounts require an available published Agent, and enabling/connecting remains explicit through the Main-owned API.
|
||||
- Peers: knowledge workspace changes concern knowledge/files; create diagnosis has no declared source dependency; Canvas diagnosis is separate. This task edits ChannelAccounts and its tests only, plus README and this record. No cross-task dependency or canonical documentation change.
|
||||
- Source evidence: sidebar Channel button already navigates normally; the disabled Next step requires `targetAgent`. Successful Agent reads filter out unpublished and disabled Agents; the current modal has a vague prerequisite but the channel page does not explain it.
|
||||
- Existing create flow uses name/avatar then configuration and “开始使用”; its former “使用与接入” navigation is absent, so do not refer users to that stale screen.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- User asks for a helpful explanation when no Agent is available to connect. Use “还没有可连接的智能体” and explain creating/configuring and clicking “开始使用”, with “去我的智能体”.
|
||||
- Show the explanation only after successful loading, and never replace existing WeChat management cards.
|
||||
- Do not change any Agent/channel API, publish/enable automatically, send a message, invoke paid services, update/restart the current temporary application, merge primary, or push.
|
||||
- The earlier Robot homepage feature is already present in this base; preserve it for later combined integration.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add the shared explanatory panel to the successful empty-target state and existing dialog.
|
||||
2. Add behavior coverage for absence, draft-only eligibility, navigation without mutation, load failure and existing account access.
|
||||
3. Run focused tests, typecheck, scoped lint, build and fixture-only Electron UI validation; record and commit a clean handoff.
|
||||
|
||||
## Outcome
|
||||
|
||||
- ChannelAccounts shows “还没有可连接的智能体” after a successful read with no available target, followed by “先创建一个智能体,配置完成后点击‘开始使用’,就能连接到微信。” (UI uses double curly quotes).
|
||||
- “去我的智能体” navigates to `/cloud-agents?view=mine`. The same panel explains the disabled Next step in the Add/Manage dialog.
|
||||
- With no accounts or targets, the explanatory panel replaces the premature “添加第一个微信” empty state; existing accounts retain their cards and management actions.
|
||||
- No backend, eligibility, publication, enablement, paid service or current temporary-application changes. README synchronized.
|
||||
- Read-only independent review found no blocking issue.
|
||||
|
||||
## Verification
|
||||
|
||||
- Focused `channel-accounts-page.test.tsx`: 34/34 passed. Covers no Agents/draft-only/disabled-only, CTA navigation with no binding writes, modal hint, successful-load gating, failed initial read and preserved existing-account management.
|
||||
- Exact pinned pnpm 10.33.4: typecheck passed; scoped lint of component and both test files passed; build:vite passed.
|
||||
- Electron `cloud-agent-channels.spec.ts`: 2/2 passed with fixture-only API responses. New empty-channel test enters from the sidebar, checks both explanations, checks disabled Next, then returns to My Agents without writes. Existing connect/switch behavior remains passing.
|
||||
- Visually checked captured screenshots at 1366×900 and 980×680; text/button fit and main content has no horizontal overflow. Local evidence: `test-results/cloud-agent-channels-guide-e93c9-e-sidebar-back-to-My-Agents/wechat-no-agents-desktop.png` and `wechat-no-agents-compact.png`.
|
||||
- `git diff --check` passed; project-doc ownership drift check passed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Integrate this branch when combining changes for the user's new temporary application. Its base includes prior homepage Robot-hiding commit `13336cf`; do not update the existing temporary application.
|
||||
- Published-but-disabled Agents remain excluded by the existing contract; this task does not restore the removed use/access controls or change Agent lifecycle behavior.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Current state / success criteria: channel page explains the available-Agent prerequisite before the user enters a blocked binding step, and links to My Agents. Promote only at integration.
|
||||
- No new architectural decision or backend contract.
|
||||
@@ -18,7 +18,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
|
||||
云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
Agents 侧栏的「渠道」用于把自己的智能体接到微信。每个微信一张卡,展示备注、绑定的智能体头像/名称和是否可用;一个账号同时只有一个目标,多个账号可以使用同一智能体。添加流程为选择自己已开始使用的智能体、扫码连接、明确完成绑定并启用;备注可选,扫码确认后收起二维码与验证码,未确认或启用失败不显示完成。管理已有账号不会重新创建账号;更换目标保留微信登录与原启停状态,解除智能体绑定清空目标并停用但保留微信登录,暂停保留绑定/登录并停止该渠道当前任务,断开微信连接则需要重新扫码。更换、解除和断开有明确确认,历史保留。渠道配置页不展示微信对话或活动,不读取这些列表;聊天在微信中进行,既有云端历史、文件、审批、费用和账号隔离合同不变。页面保留加载失败重试、未确认操作恢复,创建后绑定失败可继续使用原账号,重试沿用原操作身份;仅添加/扫码不自动启用。实际微信身份与重复扫码识别仍由云端负责,客户端不以备注推测同一微信。渠道触发的应计费模型调用仍由智能体创建者支付,Main 持有认证与恢复;用户无需填写服务凭据。应用 API 管理独立于微信渠道。
|
||||
Agents 侧栏的「渠道」用于把自己的智能体接到微信。每个微信一张卡,展示备注、绑定的智能体头像/名称和是否可用;一个账号同时只有一个目标,多个账号可以使用同一智能体。没有可连接的智能体时,渠道页和添加窗口说明需先创建、配置并点击“开始使用”,提供“去我的智能体”入口;加载失败不显示该空态,已有微信卡片仍可管理。添加流程为选择自己已开始使用的智能体、扫码连接、明确完成绑定并启用;备注可选,扫码确认后收起二维码与验证码,未确认或启用失败不显示完成。管理已有账号不会重新创建账号;更换目标保留微信登录与原启停状态,解除智能体绑定清空目标并停用但保留微信登录,暂停保留绑定/登录并停止该渠道当前任务,断开微信连接则需要重新扫码。更换、解除和断开有明确确认,历史保留。渠道配置页不展示微信对话或活动,不读取这些列表;聊天在微信中进行,既有云端历史、文件、审批、费用和账号隔离合同不变。页面保留加载失败重试、未确认操作恢复,创建后绑定失败可继续使用原账号,重试沿用原操作身份;仅添加/扫码不自动启用。实际微信身份与重复扫码识别仍由云端负责,客户端不以备注推测同一微信。渠道触发的应计费模型调用仍由智能体创建者支付,Main 持有认证与恢复;用户无需填写服务凭据。应用 API 管理独立于微信渠道。
|
||||
|
||||
- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。任务中的已生成图片可通过“放大查看图片”按钮直接打开大图,支持适应窗口、原始尺寸及 Esc 关闭,无需先下载。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
|
||||
- `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
|
||||
|
||||
@@ -251,6 +251,11 @@ export function ChannelAccounts() {
|
||||
{flow?.target && !targetAgent && selected?.target_agent_slug === flow.target && <option value={flow.target} disabled>{selected.target_agent_name ?? '原智能体'}(暂不可用)</option>}
|
||||
{agents.map(agent => <option key={agent.slug} value={agent.slug}>{agent.name}</option>)}
|
||||
</>;
|
||||
const agentRequiredHint = <div className="rounded-2xl border border-[#e5def0] bg-[#f6f2fb] p-5 text-sm leading-7 text-[#75618e]">
|
||||
<h2 className="font-semibold text-[#3c3549]">还没有可连接的智能体</h2>
|
||||
<p className="mt-2">先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。</p>
|
||||
<Button variant="outline" className="mt-4 rounded-xl" onClick={() => navigate('/cloud-agents?view=mine')}>去我的智能体<ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
</div>;
|
||||
|
||||
return <section className="h-full overflow-auto bg-[#fffcf7] p-4 sm:p-7 lg:p-9" data-testid="channel-accounts-page">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
@@ -266,12 +271,13 @@ export function ChannelAccounts() {
|
||||
</div>
|
||||
</header>
|
||||
{!flow && messages}
|
||||
{!loading && loaded && !agents.length && !flow && agentRequiredHint}
|
||||
{loading ? <div role="status" className="flex items-center justify-center gap-3 rounded-3xl bg-[#f4f1fb] p-12 text-[#75618e]"><Loader2 className="h-5 w-5 animate-spin" />正在查看你的微信绑定…</div>
|
||||
: !loaded ? <div className="rounded-3xl border border-[#e4e2de] bg-white p-10 text-center text-sm text-[#677286]"><p>暂时没能读到微信绑定,请刷新后再试。</p><Button variant="outline" className="mt-5 rounded-xl" onClick={() => void refresh().catch(failure => setError(errorText(failure)))}>重新加载</Button></div>
|
||||
: !accounts.length ? <div className="rounded-[28px] border border-[#dfe9e2] bg-[#f1f8f2] px-6 py-14 text-center">
|
||||
: !accounts.length ? (agents.length > 0 && <div className="rounded-[28px] border border-[#dfe9e2] bg-[#f1f8f2] px-6 py-14 text-center">
|
||||
<Link2 className="mx-auto h-10 w-10 text-[#678b70]" /><h2 className="mt-5 text-xl font-bold text-[#303747]">把你的智能体带到微信吧</h2><p className="mx-auto mt-3 max-w-md text-sm leading-7 text-[#677286]">绑定后,就可以回到微信和它聊聊。每个微信绑定一个智能体,也可以把多个微信交给同一个智能体。</p>
|
||||
<Button className="mt-7 h-11 rounded-2xl px-6" onClick={() => { setError(''); setFlow({ mode: 'new', step: 'choose', accountId: '', target: '', remark: '' }); }}>添加第一个微信<ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
</div> : <div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" aria-label="我的微信绑定">
|
||||
</div>) : <div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" aria-label="我的微信绑定">
|
||||
{accounts.map(account => {
|
||||
const agent = agents.find(item => item.slug === account.target_agent_slug);
|
||||
const state = accountState(account);
|
||||
@@ -298,7 +304,7 @@ export function ChannelAccounts() {
|
||||
{recoveryBlocked && <p className="rounded-2xl bg-[#fff0d8] p-3 text-sm text-[#876033]">有一次绑定操作的结果需要确认,请先关闭此窗口并处理页面上的恢复提醒。</p>}
|
||||
{flow.step === 'choose' && <>
|
||||
{flow.mode === 'manage' && selectedState && <div className="flex items-center justify-between rounded-2xl bg-[#eaf5ec] p-4 text-sm"><span>微信连接与绑定</span><span className={`rounded-full px-3 py-1.5 text-xs font-semibold ${selectedState.tone}`}>{selectedState.text}</span></div>}
|
||||
{!agents.length ? <div className="rounded-2xl border border-[#e5def0] bg-[#f6f2fb] p-5 text-sm leading-7 text-[#75618e]"><p>先准备一个可以使用的智能体,再把它带到微信里。</p><Button variant="outline" className="mt-4 rounded-xl" onClick={() => navigate('/cloud-agents?view=mine')}>去我的智能体<ArrowRight className="ml-2 h-4 w-4" /></Button></div>
|
||||
{!agents.length ? agentRequiredHint
|
||||
: <label className="space-y-2 text-sm font-medium text-[#3c3549]"><span>{flow.mode === 'new' ? '选择智能体' : '绑定的智能体'}</span><Select aria-label={flow.mode === 'new' ? '选择智能体' : '绑定的智能体'} className="h-12 rounded-xl bg-white" style={{ backgroundPosition: 'right 12px center', backgroundSize: '16px 16px', backgroundRepeat: 'no-repeat' }} value={flow.target} disabled={disabled} onChange={event => setFlow({ ...flow, target: event.target.value })}>{options}</Select></label>}
|
||||
{flow.mode === 'new' && !flow.accountId && <label className="space-y-2 text-sm font-medium text-[#3c3549]"><span>微信备注(可选)</span><Input aria-label="微信备注(可选)" className="h-12 rounded-xl bg-white" disabled={disabled} value={flow.remark} maxLength={100} onChange={event => setFlow({ ...flow, remark: event.target.value })} placeholder="给这个微信起个方便辨认的名字" /></label>}
|
||||
{flow.mode === 'manage' && selected?.blockers.length ? <p className="rounded-2xl bg-[#fff0d8] p-3 text-sm leading-6 text-[#876033]">{selected.blockers.map(item => blockerText[item] ?? '连接暂时不可用,请刷新后重试').join(';')}</p> : null}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user