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';
const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn(), save: 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: 1, updated_at: '2026-09-10T00:00:00Z',
};
function open() {
const router = createMemoryRouter([
{ path: '/cloud-agents', element: },
{ path: '/module-select', element:
模块首页
},
], { initialEntries: ['/cloud-agents'] });
const view = render();
return { router, ...view };
}
beforeEach(() => {
vi.resetAllMocks(); auth.user.userId = 'creator';
api.list.mockResolvedValue({ agents: [draft], next_cursor: null });
});
afterEach(cleanup);
it('keeps creation input and its uncertain operation when an existing card is clicked', async () => {
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(draft);
open();
const existing = await screen.findByRole('button', { name: /写作搭档/ });
fireEvent.click(screen.getByRole('button', { name: '创建智能体' }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '新用途' } });
fireEvent.click(existing);
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
expect(screen.getByLabelText('名称')).toHaveValue('新搭档');
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
await screen.findByRole('alert');
fireEvent.click(existing);
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await screen.findByTestId('cloud-agent-editor');
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});
it('keeps the same creation operation after a lost response', async () => {
api.list.mockResolvedValue({ agents: [], next_cursor: null });
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(draft);
open();
fireEvent.click(await screen.findByText('创建第一个智能体'));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
await screen.findByRole('alert');
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await screen.findByTestId('cloud-agent-editor');
expect(api.create).toHaveBeenCalledTimes(2);
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});
it('preserves local input on conflicts and requires an explicit choice before retry', async () => {
api.save.mockRejectedValueOnce(new Error('草稿已更新'));
api.get.mockResolvedValue({ ...draft, draft_revision: 2, system_prompt: '其他设备内容' });
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
fireEvent.change(screen.getByLabelText('角色与指令'), { target: { value: '我的修改' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('云端已有修订 2,本地输入仍保留');
expect(screen.getByLabelText('角色与指令')).toHaveValue('我的修改');
expect(screen.getByRole('button', { name: '保存草稿' })).toBeDisabled();
fireEvent.click(screen.getByText('保留我的内容,继续编辑'));
api.save.mockResolvedValue({ ...draft, draft_revision: 3, system_prompt: '我的修改' });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save.mock.calls[1][1]).toMatchObject({ expected_revision: 2, system_prompt: '我的修改' });
});
it('blocks module navigation with unsaved edits, cancels leaving, then saves before proceeding', async () => {
const { router } = open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '未保存的修改' })).toBeVisible();
expect(router.state.location.pathname).toBe('/cloud-agents');
fireEvent.click(screen.getByText('继续编辑'));
expect(screen.getByLabelText('名称')).toHaveValue('新的名称');
await act(async () => { await router.navigate('/module-select'); });
api.save.mockResolvedValue({ ...draft, name: '新的名称', draft_revision: 2 });
fireEvent.click(screen.getByText('保存并离开'));
await screen.findByText('模块首页');
expect(api.save).toHaveBeenCalledTimes(1);
});
it('clears the previous account workspace and ignores its delayed list', async () => {
let finish!: (value: unknown) => void;
api.list.mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; }))
.mockResolvedValue({ agents: [], next_cursor: null });
const view = open();
auth.user.userId = 'other';
view.rerender();
await screen.findByText('你的第一个智能体,从这里开始');
await act(async () => finish({ agents: [draft], next_cursor: null }));
await waitFor(() => expect(screen.queryByText('写作搭档')).not.toBeInTheDocument());
});
it('warns before leaving an uncertain creation and keeps its retry intent', async () => {
api.list.mockResolvedValue({ agents: [], next_cursor: null });
api.create.mockRejectedValue(new Error('连接中断'));
const { router } = open();
fireEvent.click(await screen.findByText('创建第一个智能体'));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
await screen.findByRole('alert');
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await waitFor(() => expect(api.create).toHaveBeenCalledTimes(2));
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});