import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import CloudAgents from '@/pages/CloudAgents'; import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents'; const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn(), save: vi.fn(), call: vi.fn(), events: vi.fn(), recovery: vi.fn(), remember: vi.fn() })); const auth = vi.hoisted(() => ({ user: { userId: 'creator' } })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: typeof auth) => unknown) => selector(auth) })); const draft = { slug: 'ml-' + 'a'.repeat(32), name: '写作搭档', purpose: '', system_prompt: '原始指令', draft_revision: 3, updated_at: '2026-09-10T00:00:00Z', configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'model-a' }, published_version: null, enabled: true, }; function open() { const router = createMemoryRouter([ { path: '/cloud-agents', element: }, { path: '/module-select', element:
模块首页
}, ], { initialEntries: ['/cloud-agents'] }); return { router, ...render() }; } async function openConfiguration() { const view = open(); fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true })); await waitFor(() => expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeEnabled()); return view; } const publications = () => api.call.mock.calls.filter(([operation]) => operation === 'publish'); const messageOperations = () => api.call.mock.calls.filter(([operation]) => ['preview', 'submit', 'resume'].includes(operation)); beforeEach(() => { vi.resetAllMocks(); auth.user.userId = 'creator'; api.list.mockResolvedValue({ agents: [draft], next_cursor: null }); api.get.mockResolvedValue(draft); api.recovery.mockResolvedValue({ recent: null, pending: [] }); api.remember.mockResolvedValue({ saved: true }); api.save.mockImplementation(async (_slug, input) => ({ ...draft, ...input, draft_revision: 4 })); api.call.mockImplementation(async (operation, input) => { if (operation === 'access') return { published_version: null, enabled: true, versions: [], grants: [], applications: [] }; if (operation === 'catalog') return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null }; if (operation === 'knowledge') return { databases: [], models: [] }; if (operation === 'resources') return { mcps: [], skills: [], subagents: [] }; if (operation === 'threads') return { threads: [], next_offset: null }; if (operation === 'publish') return { version: 1, draft_revision: input.expected_revision }; return {}; }); }); afterEach(cleanup); it('starts once from the saved configuration and opens conversation only after activation succeeds', async () => { let finish!: (result: { version: number; draft_revision: number }) => void; const base = api.call.getMockImplementation()!; api.call.mockImplementation((operation, input) => operation === 'publish' ? new Promise(resolve => { finish = resolve; }) : base(operation, input)); const { router } = await openConfiguration(); expect(publications()).toHaveLength(0); const start = screen.getByRole('button', { name: '开始使用', exact: true }); fireEvent.click(start); fireEvent.click(start); expect(publications()).toHaveLength(1); expect(publications()[0][1]).toMatchObject({ slug: draft.slug, expected_revision: 3, operation_id: expect.any(String) }); expect(screen.getByRole('button', { name: '正在开始…' })).toBeDisabled(); expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument(); expect(screen.queryByText('准备好使用自己的助手了吗?')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: '确认开始使用' })).not.toBeInTheDocument(); expect(screen.getByLabelText('提示词')).toHaveValue('原始指令'); await act(async () => { await router.navigate('/module-select'); }); expect(screen.getByRole('dialog', { name: '未保存的修改' })).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: '继续编辑' })); await act(async () => finish({ version: 1, draft_revision: 3 })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); expect(screen.getByRole('button', { name: '调整智能体', exact: true })).toBeVisible(); expect(api.save).not.toHaveBeenCalled(); expect(messageOperations()).toHaveLength(0); }); it('keeps unsaved input in configuration until the user saves, then starts the saved revision directly', async () => { await openConfiguration(); fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '我的新要求' } }); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); expect(screen.getByRole('alert')).toHaveTextContent('请先保存草稿,再开始使用。'); expect(screen.getByLabelText('提示词')).toBeVisible(); expect(screen.getByLabelText('提示词')).toHaveValue('我的新要求'); expect(publications()).toHaveLength(0); expect(api.save).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true })); await waitFor(() => expect(screen.getByRole('button', { name: '已保存', exact: true })).toBeDisabled()); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); expect(publications()[0][1]).toMatchObject({ expected_revision: 4 }); expect(messageOperations()).toHaveLength(0); }); it('returns to the model choice without activating a draft that has no model', async () => { api.list.mockResolvedValue({ agents: [{ ...draft, configuration: EMPTY_CLOUD_CONFIGURATION }], next_cursor: null }); api.get.mockResolvedValue({ ...draft, configuration: EMPTY_CLOUD_CONFIGURATION }); await openConfiguration(); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); expect(screen.getByRole('tab', { name: '高级设置' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByLabelText('模型')).toBeVisible(); expect(screen.getByRole('alert')).toHaveTextContent('请先在高级设置中选择模型。'); expect(publications()).toHaveLength(0); expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument(); }); it('does not activate while a cloud save conflict needs to be resolved', async () => { await openConfiguration(); api.save.mockRejectedValue(new Error('保存冲突')); api.get.mockResolvedValue({ ...draft, draft_revision: 4, system_prompt: '云端修改' }); fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '保留本地修改' } }); fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true })); await screen.findByText('云端已有修订 4,本地输入仍保留'); const start = screen.getByRole('button', { name: '开始使用', exact: true }); expect(start).toBeDisabled(); fireEvent.click(start); expect(publications()).toHaveLength(0); expect(screen.getByLabelText('提示词')).toHaveValue('保留本地修改'); }); it('waits for a readable activation status and permits a read retry before starting', async () => { const base = api.call.getMockImplementation()!; let unavailable = true; api.call.mockImplementation(async (operation, input) => { if (operation === 'access' && unavailable) throw new Error('状态读取失败'); return base(operation, input); }); open(); fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true })); await screen.findByText(/状态读取失败/); expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeDisabled(); expect(publications()).toHaveLength(0); unavailable = false; fireEvent.click(screen.getByRole('button', { name: '刷新应用状态', exact: true })); await waitFor(() => expect(screen.getByRole('button', { name: '开始使用', exact: true })).toBeEnabled()); expect(publications()).toHaveLength(0); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); expect(publications()).toHaveLength(1); expect(messageOperations()).toHaveLength(0); }); it('retries an uncertain activation with the original operation and keeps pending-leave protection', async () => { const base = api.call.getMockImplementation()!; let attempts = 0; api.call.mockImplementation(async (operation, input) => { if (operation === 'publish' && ++attempts === 1) throw new Error('连接中断'); return base(operation, input); }); await openConfiguration(); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); await screen.findByText(/连接中断/); expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument(); const unload = new Event('beforeunload', { cancelable: true }); window.dispatchEvent(unload); expect(unload.defaultPrevented).toBe(true); fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); expect(publications()).toHaveLength(2); expect(publications()[1][1]).toEqual(publications()[0][1]); expect(messageOperations()).toHaveLength(0); }); it('keeps a newer saved revision unapplied when retry resolves the original uncertain activation', async () => { const base = api.call.getMockImplementation()!; let attempts = 0; let activated = false; api.call.mockImplementation(async (operation, input) => { if (operation === 'access') return { published_version: activated ? 1 : null, enabled: true, grants: [], applications: [], versions: activated ? [{ version: 1, draft_revision: 3, created_at: '' }] : [], }; if (operation === 'publish') { if (++attempts === 1) throw new Error('连接中断'); activated = true; return { version: 1, draft_revision: input.expected_revision }; } return base(operation, input); }); await openConfiguration(); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); await screen.findByText(/连接中断/); fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '这次新保存的要求' } }); fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true })); await waitFor(() => expect(screen.getByRole('button', { name: '已保存', exact: true })).toBeDisabled()); fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); await screen.findByText('修改已保存,尚未应用'); expect(screen.getByText('这次修改还没有应用到正在使用的助手')).toBeVisible(); expect(screen.getByRole('button', { name: '应用最新修改', exact: true })).toBeEnabled(); expect(screen.getByLabelText('提示词')).toHaveValue('这次新保存的要求'); expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ expected_revision: 3, system_prompt: '这次新保存的要求' })); expect(publications()).toHaveLength(2); expect(publications()[1][1]).toEqual(publications()[0][1]); expect(publications()[1][1]).toMatchObject({ expected_revision: 3 }); expect(messageOperations()).toHaveLength(0); }); it('keeps successful activation and conversation when the following status read fails', async () => { const base = api.call.getMockImplementation()!; let activated = false; api.call.mockImplementation(async (operation, input) => { if (operation === 'access' && activated) throw new Error('状态刷新失败'); const result = await base(operation, input); if (operation === 'publish') activated = true; return result; }); await openConfiguration(); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible(); await screen.findByText(/状态刷新失败/); expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled(); fireEvent.click(screen.getByRole('button', { name: '刷新应用状态', exact: true })); await waitFor(() => expect(api.call.mock.calls.filter(([operation]) => operation === 'access')).toHaveLength(3)); expect(publications()).toHaveLength(1); expect(messageOperations()).toHaveLength(0); }); it('does not carry a late activation result into another signed-in account', async () => { let finish!: (result: { version: number; draft_revision: number }) => void; const base = api.call.getMockImplementation()!; api.call.mockImplementation((operation, input) => operation === 'publish' ? new Promise(resolve => { finish = resolve; }) : base(operation, input)); const view = await openConfiguration(); fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true })); auth.user.userId = 'other'; api.list.mockResolvedValue({ agents: [], next_cursor: null }); view.rerender(); await screen.findByText('做一个属于你的 AI 助手吧'); await act(async () => finish({ version: 1, draft_revision: 3 })); expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument(); expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument(); expect(publications()).toHaveLength(1); expect(messageOperations()).toHaveLength(0); });