Files
makelore/tests/unit/cloud-agents-page.test.tsx
T

837 lines
55 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { act, cleanup, fireEvent, render, screen, waitFor, within } 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(), uploadAvatar: vi.fn(), get: vi.fn(), save: vi.fn(), call: vi.fn(), events: vi.fn(), recovery: vi.fn(), remember: vi.fn(), resolvePending: 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',
configuration: EMPTY_CLOUD_CONFIGURATION, published_version: null, enabled: true,
};
function open() {
const router = createMemoryRouter([
{ path: '/cloud-agents', element: <CloudAgents /> },
{ path: '/module-select', element: <div>模块首页</div> },
], { initialEntries: ['/cloud-agents'] });
const view = render(<RouterProvider router={router} />);
return { router, ...view };
}
beforeEach(() => {
vi.resetAllMocks(); auth.user.userId = 'creator';
api.recovery.mockResolvedValue({ recent: null, pending: [] });
api.remember.mockResolvedValue({ saved: true });
api.list.mockResolvedValue({ agents: [draft], next_cursor: null });
api.call.mockImplementation(async (operation: string) => {
if (operation === 'knowledge') return { databases: [], models: [] };
if (operation === 'resources') return { mcps: [], skills: [], subagents: [] };
if (operation === 'threads') return { threads: [], next_offset: null };
if (operation === 'budget') return { request_limit_points: null, daily_limit_points: null, daily_committed_points: '0.00' };
return { models: [], resources: {}, pricing: null };
});
});
afterEach(cleanup);
it('does not label disabled assistants as ready to use or show archived assistants', async () => {
api.list.mockResolvedValue({ agents: [
{ ...draft, published_version: 1, enabled: false },
{ ...draft, slug: 'ml-' + 'b'.repeat(32), name: '归档助手', published_version: 1, archived: true },
], next_cursor: null });
open();
expect(await screen.findByText('已停用')).toBeVisible();
expect(screen.queryByText('已归档')).not.toBeInTheDocument();
expect(screen.queryByRole('article', { name: '归档助手' })).not.toBeInTheDocument();
expect(screen.queryByText('可以使用')).not.toBeInTheDocument();
});
function publishedAssistant(appliedRevision = 3) {
let current = { ...draft, draft_revision: 4, published_version: 8, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'model-a' } };
let applied = appliedRevision;
const base = api.call.getMockImplementation()!;
api.list.mockImplementation(async () => ({ agents: [current], next_cursor: null }));
api.recovery.mockResolvedValue({ recent: { slug: draft.slug, mode: 'preview', draft_revision: 4 }, pending: [] });
api.get.mockImplementation(async () => current);
api.save.mockImplementation(async (_slug, input) => (current = { ...current, ...input, draft_revision: current.draft_revision + 1 }));
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access') return { published_version: current.published_version, enabled: true,
versions: [{ version: 1, draft_revision: 1, created_at: '' }, { version: current.published_version, draft_revision: applied, created_at: '' }], grants: [], applications: [] };
if (operation === 'publish') {
applied = input.expected_revision;
current = { ...current, published_version: current.published_version + 1 };
return { version: current.published_version, draft_revision: applied };
}
if (operation === 'catalog') return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null };
if (operation === 'channelBindings') return { items: [], available_accounts: [] };
if (operation === 'costs') return { items: [], summary: {}, next_offset: null };
return base(operation, input);
});
}
it('shows saved but unapplied changes and applies directly from the editor header', async () => {
publishedAssistant();
open();
await screen.findByTestId('cloud-agent-editor');
await screen.findByText('修改已保存,尚未应用');
expect(screen.getByText(/保存只更新草稿/)).toBeVisible();
expect(api.call.mock.calls.some(([operation]) => operation === 'publish')).toBe(false);
fireEvent.click(screen.getByRole('button', { name: '应用最新修改', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')).toEqual([
['publish', expect.objectContaining({ slug: draft.slug, expected_revision: 4 })],
]);
expect(screen.queryByText('修改已保存,尚未应用')).not.toBeInTheDocument();
expect(screen.getByRole('tab', { name: '介绍与提示词' })).toHaveAttribute('aria-selected', 'true');
});
it('keeps unsent input accessible before adjusting and applying configuration', async () => {
publishedAssistant(4);
api.recovery.mockResolvedValue({ recent: null, pending: [] });
open();
fireEvent.click(await screen.findByRole('button', { name: '和它聊聊', exact: true }));
await screen.findByTestId('cloud-agent-editor');
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
const message = await screen.findByLabelText('消息');
await waitFor(() => expect(message).toBeEnabled());
fireEvent.change(message, { target: { value: '保留正式聊天的问题' } });
fireEvent.click(screen.getByRole('button', { name: '调整智能体', exact: true }));
expect(message).toBeVisible();
expect(message).toHaveValue('保留正式聊天的问题');
expect(screen.getByText('对话中还有未完成的内容,请先发送或处理后再调整智能体。')).toBeVisible();
fireEvent.change(message, { target: { value: '' } });
fireEvent.click(screen.getByRole('button', { name: '调整智能体', exact: true }));
expect(message).not.toBeVisible();
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '新的要求' } });
expect(screen.getByText('修改尚未保存和应用')).toBeVisible();
expect(screen.getByRole('button', { name: '应用最新修改', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('修改已保存,尚未应用');
expect(api.call.mock.calls.some(([operation]) => operation === 'publish')).toBe(false);
fireEvent.click(screen.getByRole('button', { name: '应用最新修改', exact: true }));
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
expect(screen.getByRole('tab', { name: '介绍与提示词' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByLabelText('消息')).toBe(message);
expect(message).not.toBeVisible();
expect(message).toHaveValue('');
expect(screen.queryByRole('button', { name: '聊天', exact: true })).not.toBeInTheDocument();
expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')[0][1]).toMatchObject({ expected_revision: 5 });
expect(api.call.mock.calls.some(([operation]) => operation === 'preview' || operation === 'submit')).toBe(false);
});
it('retries a failed application from the header using the original publication intent', async () => {
publishedAssistant();
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);
});
open();
await screen.findByTestId('cloud-agent-editor');
await screen.findByText('修改已保存,尚未应用');
fireEvent.click(screen.getByRole('button', { name: '应用最新修改', exact: true }));
await screen.findByText(/连接中断/);
expect(screen.getByRole('button', { name: '重试本次应用' })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '重试本次应用' }));
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
const calls = api.call.mock.calls.filter(([operation]) => operation === 'publish');
expect(calls).toHaveLength(2);
expect(calls[0][1]).toEqual(calls[1][1]);
});
it('does not call an unread publication status up to date and offers a read retry', async () => {
publishedAssistant(4);
const base = api.call.getMockImplementation()!;
let fail = true;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access' && fail) throw new Error('状态读取失败');
return base(operation, input);
});
open();
await screen.findByTestId('cloud-agent-editor');
await screen.findByText(/状态读取失败/);
expect(screen.getByText('应用状态待确认')).toBeVisible();
expect(screen.queryByRole('button', { name: '已应用', exact: true })).not.toBeInTheDocument();
fail = false;
fireEvent.click(screen.getByRole('button', { name: '刷新应用状态' }));
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
expect(api.call.mock.calls.some(([operation]) => operation === 'publish')).toBe(false);
});
it('keeps a successful application confirmed when its following status refresh fails', async () => {
publishedAssistant();
const base = api.call.getMockImplementation()!;
let published = false;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'access' && published) throw new Error('状态刷新失败');
const result = await base(operation, input);
if (operation === 'publish') published = true;
return result;
});
open();
await screen.findByText('修改已保存,尚未应用');
fireEvent.click(screen.getByRole('button', { name: '应用最新修改', exact: true }));
await screen.findByText(/状态刷新失败/);
expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled();
expect(screen.queryByRole('button', { name: '重试本次应用' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '刷新应用状态' }));
await waitFor(() => expect(api.call.mock.calls.filter(([operation]) => operation === 'access')).toHaveLength(3));
expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')).toHaveLength(1);
});
it('protects an uploaded avatar before a name has been entered', async () => {
const avatar = { icon: '/api/storage/public/images/avatar.png', icon_url: 'https://images.test/avatar.png' };
api.uploadAvatar.mockResolvedValue(avatar);
const { router } = open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
await waitFor(() => expect(screen.getByRole('img', { name: '智能体头像' })).toHaveAttribute('src', avatar.icon_url));
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeDisabled();
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
expect(screen.getByRole('img', { name: '智能体头像' })).toHaveAttribute('src', avatar.icon_url);
expect(screen.getByLabelText('名称')).toHaveValue('');
expect(api.create).not.toHaveBeenCalled();
});
it('creates from name and avatar, then configures an optional introduction, advanced model and raw prompt', async () => {
const avatar = { icon: '/api/storage/public/images/avatar.png', icon_url: 'https://images.test/avatar.png' };
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
models: [{ id: 'model-first', name: '可用模型甲' }, { id: 'model-second', name: '可用模型乙' }], resources: {},
pricing: { unit_size: 1000, points_per_unit: '1.23' },
} : base(operation, input));
api.uploadAvatar.mockResolvedValue(avatar);
api.create.mockImplementation(async input => ({ ...draft, ...input, icon_url: avatar.icon_url, system_prompt: input.purpose }));
api.save.mockImplementation(async (_slug, input) => ({ ...draft, ...input, icon_url: avatar.icon_url, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
expect(screen.getByRole('heading', { name: '创建智能体', exact: true })).toBeVisible();
expect(screen.getByLabelText('名称')).toHaveValue('');
expect(screen.getByRole('group', { name: '智能体头像' })).toBeVisible();
expect(screen.queryByLabelText('用途')).not.toBeInTheDocument();
expect(screen.queryByLabelText('提示词')).not.toBeInTheDocument();
expect(screen.queryByLabelText('模型')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /科学问答助手|故事搭档|英语陪练|整理小帮手|先给我提示/ })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeDisabled();
expect(api.create).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => operation === 'catalog')).toBe(false);
fireEvent.change(screen.getByLabelText('名称'), { target: { value: ' 我的写作助手 ' } });
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
await screen.findByRole('button', { name: '更换头像' });
fireEvent.click(screen.getByRole('button', { name: '下一步', exact: true }));
await screen.findByTestId('cloud-agent-editor');
expect(screen.getByLabelText('名称')).toHaveValue('我的写作助手');
expect(screen.getByRole('img', { name: '我的写作助手头像' })).toHaveAttribute('src', avatar.icon_url);
expect(api.create).toHaveBeenCalledWith({ operation_id: expect.any(String), name: '我的写作助手', purpose: '', icon: avatar.icon });
expect(api.create.mock.calls[0][0]).not.toHaveProperty('icon_url');
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.getByLabelText('一句话介绍')).toHaveValue('');
expect(screen.getByLabelText('一句话介绍')).toBeVisible();
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(screen.getByLabelText('模型')).not.toBeVisible();
expect(screen.getByRole('tab', { name: '介绍与提示词' })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByLabelText('提示词')).toHaveValue('');
expect(screen.queryByLabelText('用途')).not.toBeInTheDocument();
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(screen.queryByRole('separator', { name: '调整配置与试用宽度' })).not.toBeInTheDocument();
expect(screen.queryByText(/这是帮助它思考和回答的 AI|新助手从目录中的第一个可用模型开始|选择模型后,保存即可试用/)).not.toBeInTheDocument();
expect(screen.queryByText(/每 .*个词元约 .*词元点数/)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /简单易懂|一步一步来|先给我提示/ })).not.toBeInTheDocument();
for (const name of ['聊天', '自动任务', '使用与接入']) {
expect(screen.queryByRole('button', { name, exact: true })).not.toBeInTheDocument();
}
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '我的文章搭档' } });
expect(screen.getByLabelText('模型')).toHaveValue('');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.getByLabelText('模型')).toBeRequired();
fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'model-second' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
const prompt = '一次只解释一个新词。\n\n 保留缩进。\n使用简单易懂的语言,遇到新词先解释。\n';
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: prompt } });
expect(api.save).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({
name: '我的文章搭档', purpose: '', system_prompt: prompt, icon: avatar.icon,
configuration: expect.objectContaining({ model: 'model-second' }),
}));
expect(api.save.mock.calls[0][1]).not.toHaveProperty('icon_url');
expect(screen.getByLabelText('提示词')).toHaveValue(prompt);
expect(api.call.mock.calls.some(([operation]) => ['threads', 'preview', 'submit', 'publish'].includes(operation))).toBe(false);
});
it('allows name-only creation after avatar cancellation or failure', async () => {
api.uploadAvatar.mockResolvedValueOnce(null).mockRejectedValueOnce(new Error('头像上传失败'));
api.create.mockImplementation(async input => ({ ...draft, ...input }));
open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
await waitFor(() => expect(screen.getByRole('button', { name: '上传头像' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
expect(await screen.findByRole('alert')).toHaveTextContent('头像上传失败');
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeEnabled();
fireEvent.click(screen.getByRole('button', { name: '下一步', exact: true }));
await screen.findByTestId('cloud-agent-editor');
expect(api.create).toHaveBeenCalledWith(expect.objectContaining({ name: '写作搭档', icon: null }));
expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible();
});
it('blocks creation and leaving while an avatar upload is still pending', async () => {
let finish!: (value: null) => void;
api.uploadAvatar.mockReturnValue(new Promise(resolve => { finish = resolve; }));
const { router } = open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
expect(screen.getByRole('button', { name: '正在上传…' })).toBeDisabled();
expect(screen.getByLabelText('名称')).toBeDisabled();
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeDisabled();
expect(screen.getByRole('button', { name: '取消', exact: true })).toBeDisabled();
fireEvent.submit(screen.getByRole('button', { name: '下一步', exact: true }).closest('form')!);
expect(api.create).not.toHaveBeenCalled();
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
expect(screen.getByRole('button', { name: '离开', exact: true })).toBeDisabled();
await act(async () => finish(null));
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
expect(screen.getByRole('button', { name: '下一步', exact: true })).toBeEnabled();
expect(screen.getByLabelText('名称')).toHaveValue('写作搭档');
});
it('ignores an avatar upload completed after an account switch', async () => {
let finish!: (value: { icon: string; icon_url: string }) => void;
api.uploadAvatar.mockReturnValue(new Promise(resolve => { finish = resolve; }));
const view = open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
auth.user.userId = 'other';
api.list.mockResolvedValue({ agents: [], next_cursor: null });
view.rerender(<RouterProvider key="other-avatar-account" router={view.router} />);
await screen.findByText('做一个属于你的 AI 助手吧');
await act(async () => finish({ icon: '/old-account.png', icon_url: 'https://images.test/old-account.png' }));
fireEvent.click(screen.getByRole('button', { name: '创建我的第一个智能体' }));
expect(screen.getByRole('img', { name: '智能体默认头像' })).toBeVisible();
expect(screen.queryByRole('button', { name: '更换头像' })).not.toBeInTheDocument();
expect(api.create).not.toHaveBeenCalled();
});
it('treats an avatar change as a manual draft edit and blocks saving during its upload', async () => {
publishedAssistant();
let finish!: (value: { icon: string; icon_url: string }) => void;
const avatar = { icon: '/api/storage/public/images/edit.png', icon_url: 'https://images.test/edit.png' };
api.uploadAvatar.mockReturnValue(new Promise(resolve => { finish = resolve; }));
open();
await screen.findByText('修改已保存,尚未应用');
fireEvent.click(screen.getByRole('button', { name: '修改头像', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
const editor = within(screen.getByTestId('cloud-agent-editor'));
expect(editor.getByRole('button', { name: '我的智能体', hidden: true })).toBeDisabled();
expect(editor.getByRole('button', { name: '应用最新修改', hidden: true })).toBeDisabled();
expect(editor.getByRole('button', { name: '已保存', hidden: true })).toBeDisabled();
expect(screen.getByRole('button', { name: '完成', exact: true })).toBeDisabled();
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
await act(async () => finish(avatar));
fireEvent.click(screen.getByRole('button', { name: '完成', exact: true }));
expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', avatar.icon_url);
expect(screen.getByRole('button', { name: '保存草稿' })).toBeEnabled();
expect(screen.getByRole('button', { name: '应用最新修改' })).toBeDisabled();
expect(api.save).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('修改已保存,尚未应用');
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ icon: avatar.icon }));
expect(api.save.mock.calls[0][1]).not.toHaveProperty('icon_url');
expect(api.call.mock.calls.some(([operation]) => operation === 'publish')).toBe(false);
});
it('saves renamed existing assistants without rewriting arbitrary instructions or selecting a model', async () => {
const original = '自定义标题\n\n 保留缩进。\n使用简单易懂的语言,遇到新词先解释。\n把任务拆成小步骤,一次引导我完成一步。\n先给我提示和思考问题,让我尝试后再补充答案。\n';
let existing = { ...draft, system_prompt: original };
api.list.mockImplementation(async () => ({ agents: [existing], next_cursor: null }));
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null,
} : base(operation, input));
api.save.mockImplementation(async (_slug, input) => (existing = { ...existing, ...input, draft_revision: 2 }));
open();
const cardAvatar = await screen.findByRole('img', { name: '写作搭档默认头像' });
const portrait = cardAvatar.getAttribute('src');
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(screen.getByLabelText('模型')).not.toBeVisible();
expect(screen.getByLabelText('提示词')).toHaveValue(original);
fireEvent.click(screen.getByRole('button', { name: '修改名称' }));
expect(screen.getByLabelText('名称')).toHaveFocus();
for (const name of ['新', '新名', '新名字']) {
fireEvent.change(screen.getByLabelText('名称'), { target: { value: name } });
expect(screen.getByRole('img', { name: `${name}默认头像` })).toHaveAttribute('src', portrait);
}
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ system_prompt: original, configuration: expect.objectContaining({ model: '' }) }));
fireEvent.click(screen.getByRole('button', { name: '我的智能体', exact: true }));
expect(await screen.findByRole('article', { name: '新名字' })).toBeVisible();
expect(screen.getByRole('img', { name: '新名字默认头像' })).toHaveAttribute('src', portrait);
fireEvent.click(screen.getByRole('button', { name: '继续制作', exact: true }));
expect(screen.getByRole('img', { name: '新名字默认头像' })).toHaveAttribute('src', portrait);
expect(screen.getByLabelText('提示词')).toHaveValue(original);
});
it('uses the saved introduction on the assistant card and permits clearing it on reopening', async () => {
let current = { ...draft };
api.list.mockImplementation(async () => ({ agents: [current], next_cursor: null }));
api.save.mockImplementation(async (_slug, input) => (current = { ...current, ...input, draft_revision: current.draft_revision + 1 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
const introduction = '和我一起把小想法变成故事';
fireEvent.change(screen.getByLabelText('一句话介绍'), { target: { value: introduction } });
expect(api.save).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ purpose: introduction, system_prompt: '原始指令' }));
fireEvent.click(screen.getByRole('button', { name: '我的智能体', exact: true }));
expect(await screen.findByText(introduction)).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续制作', exact: true }));
expect(screen.getByLabelText('一句话介绍')).toHaveValue(introduction);
fireEvent.change(screen.getByLabelText('一句话介绍'), { target: { value: '' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save.mock.calls[1][1]).toMatchObject({ purpose: '', system_prompt: '原始指令' });
fireEvent.click(screen.getByRole('button', { name: '我的智能体', exact: true }));
const card = await screen.findByRole('article', { name: '写作搭档' });
expect(within(card).queryByText(introduction)).not.toBeInTheDocument();
expect(card.querySelector('.agent-home-purpose')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '继续制作', exact: true }));
expect(screen.getByLabelText('一句话介绍')).toHaveValue('');
});
it('requires an explicit advanced model choice before starting use but permits an unfinished draft save', async () => {
let current = { ...draft };
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null };
if (operation === 'access') return { published_version: null, enabled: true, versions: [], grants: [], applications: [] };
return base(operation, input);
});
api.save.mockImplementation(async (_slug, input) => (current = { ...current, ...input, draft_revision: current.draft_revision + 1 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(screen.getByLabelText('模型')).not.toBeVisible();
expect(api.save).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '还没配置模型的想法' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ model: '' }) }));
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(screen.getByRole('tab', { name: '高级设置' })).toHaveAttribute('aria-selected', 'true');
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.getByLabelText('模型')).toBeRequired();
expect(screen.getByRole('alert')).toHaveTextContent('请先在高级设置中选择模型。');
expect(screen.queryByRole('button', { name: '确认开始使用' })).not.toBeInTheDocument();
expect(api.call.mock.calls.some(([operation]) => operation === 'publish')).toBe(false);
fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'model-a' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(await screen.findByRole('button', { name: '确认开始使用' })).toBeEnabled();
expect(api.call.mock.calls.some(([operation]) => ['publish', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('shows all advanced settings together and preserves unfinished selections, search and limits when switching pages', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') return {
models: [{ id: 'model-a', name: '模型甲' }],
resources: {
tools: [{ key: 'writing-tool', name: '文章整理工具' }],
skills: [{ key: 'writing-guide', name: '写作方法' }],
}, pricing: null,
};
return base(operation, input);
});
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
const model = screen.getByLabelText('模型');
const toolbox = screen.getByRole('group', { name: /^工具箱(Tool)/ });
const handbook = screen.getByRole('group', { name: /^做事手册(Skill)/ });
expect(model).toBeVisible();
expect(model).toBeRequired();
expect(toolbox).toBeVisible();
expect(handbook).toBeVisible();
expect(model.compareDocumentPosition(toolbox) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
expect(screen.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
expect(screen.getByRole('group', { name: /^知识库/ })).toBeVisible();
expect(screen.getByRole('heading', { name: '费用上限', exact: true })).toBeVisible();
expect(screen.getByRole('heading', { name: '运行限制', exact: true })).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['介绍与提示词', '高级设置']);
fireEvent.change(model, { target: { value: 'model-a' } });
fireEvent.click(screen.getByRole('button', { name: '选择做事手册', exact: true }));
fireEvent.click(screen.getByRole('checkbox', { name: '写作方法', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true }));
const search = screen.getByLabelText('搜索工具箱');
fireEvent.change(search, { target: { value: '文章' } });
fireEvent.click(screen.getByRole('checkbox', { name: '文章整理工具', exact: true }));
const steps = screen.getByLabelText('最多执行步数');
fireEvent.change(steps, { target: { value: '70' } });
const budget = screen.getByLabelText('每次任务上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '2.50' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
const prompt = ' 保留我的原始写作要求。\n一次讨论一个想法。\n';
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: prompt } });
expect(model).not.toBeVisible();
expect(search).not.toBeVisible();
expect(budget).not.toBeVisible();
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('模型')).toBe(model);
expect(model).toHaveValue('model-a');
expect(screen.getByLabelText('搜索工具箱')).toBe(search);
expect(search).toHaveValue('文章');
expect(screen.getByRole('checkbox', { name: '文章整理工具', exact: true })).toBeChecked();
expect(screen.getByRole('checkbox', { name: '写作方法', exact: true })).toBeChecked();
expect(steps).toHaveValue(70);
expect(budget).toHaveValue(2.5);
expect(screen.getByRole('button', { name: '保存费用上限', exact: true })).toBeEnabled();
expect(screen.getByLabelText('提示词')).toHaveValue(prompt);
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'publish', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('retains requirements when model loading fails and offers a usable retry', async () => {
const base = api.call.getMockImplementation()!;
let fail = true;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') {
if (fail) throw new Error('目录暂不可用');
return { models: [{ id: 'model-a', name: '模型甲' }], resources: {}, pricing: null };
}
return base(operation, input);
});
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await screen.findByText('目录暂不可用');
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '保留我的想法' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByText('目录暂不可用')).toBeVisible();
fail = false;
fireEvent.click(screen.getByRole('button', { name: '重新读取模型' }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.getByLabelText('提示词')).toHaveValue('保留我的想法');
expect(screen.getByLabelText('模型')).toHaveValue('');
});
it('restores the exact previous thread and presents an uncertain operation without automatically replaying it', async () => {
const published = { ...draft, published_version: 1 };
const pending = { id: 'submit:pending', operation: 'submit', input: { slug: draft.slug, request_id: 'same-request', thread_id: 'older-thread', query: '上次未确认输入' }, created_at: '2026-09-10T01:00:00Z' };
api.recovery.mockResolvedValue({ recent: { slug: draft.slug, thread_id: 'older-thread', mode: 'published' }, pending: [pending] });
api.get.mockResolvedValue(published);
api.call.mockImplementation(async (operation: string, input) => {
if (operation === 'threads') return { threads: ['newer-thread', 'older-thread'].map(thread_id => ({ thread_id, client_thread_id: thread_id, mode: 'published', title: thread_id })), next_offset: null };
if (operation === 'history') return { thread_id: input.thread_id, messages: [{ id: 1, role: 'assistant', content: '恢复了旧对话' }], run: null, next_offset: null };
if (operation === 'attachments') return { attachments: [] };
if (operation === 'files') return { files: [] };
throw new Error(operation);
});
open();
await screen.findByText('恢复了旧对话');
fireEvent.click(screen.getByRole('button', { name: '查看历史对话' }));
expect(screen.getByLabelText('历史对话')).toHaveValue('older-thread');
expect(api.resolvePending).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(call => call[0] === 'submit')).toBe(false);
api.resolvePending.mockResolvedValue({ result: { request_id: 'same-request', status: 'queued' } });
api.recovery.mockResolvedValue({ recent: null, pending: [] });
fireEvent.click(screen.getByRole('button', { name: '重试并确认结果' }));
await screen.findByText('发送消息的结果已确认。');
expect(api.resolvePending).toHaveBeenCalledWith('submit:pending', false);
});
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: '继续制作', exact: true });
fireEvent.click(screen.getByRole('button', { name: '创建智能体' }));
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('guides an unconfirmed WeChat verification back to its code entry without claiming completion', async () => {
api.recovery.mockResolvedValue({ recent: null, pending: [{ id: 'wechat-code', operation: 'wechatBindVerification',
input: { slug: draft.slug, session_key: 'qr-session', operation_id: 'verify-operation' }, created_at: '2026-09-13T01:00:00Z' }] });
api.resolvePending.mockResolvedValue({ requires_input: true, operation: 'wechatBindVerification' });
open();
fireEvent.click(await screen.findByRole('button', { name: '重试并确认结果' }));
await screen.findByText(/这次微信验证还需要验证码/);
expect(screen.queryByText('微信连接验证的结果已确认。')).not.toBeInTheDocument();
expect(api.resolvePending).toHaveBeenCalledWith('wechat-code', false);
});
it('offers only explicit discard for a recovery record whose channel operation was removed', async () => {
api.recovery.mockResolvedValueOnce({ recent: null, pending: [{ id: 'channel-policy-old', operation: 'channelPolicy',
input: { slug: draft.slug, channel_account_id: 'wechat-1' }, created_at: '2026-09-13T01:00:00Z' }] })
.mockResolvedValue({ recent: null, pending: [] });
api.resolvePending.mockResolvedValue({ operation: 'channelPolicy', discarded: true, retired: true });
open();
expect(await screen.findByText(/旧版联系人权限操作(已移除)/)).toBeVisible();
expect(screen.queryByRole('button', { name: '重试并确认结果' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '丢弃旧记录' }));
await screen.findByText(/旧版联系人权限操作已移除/);
expect(api.resolvePending).toHaveBeenCalledWith('channel-policy-old', false);
});
it('freezes the uploaded avatar and creation identity when retrying a lost response', async () => {
const avatar = { icon: '/api/storage/public/images/create.png', icon_url: 'https://images.test/create.png' };
api.list.mockResolvedValue({ agents: [], next_cursor: null });
api.uploadAvatar.mockResolvedValue(avatar);
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce({ ...draft, ...avatar });
open();
fireEvent.click(await screen.findByText('创建我的第一个智能体'));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
await screen.findByRole('button', { name: '更换头像' });
fireEvent.click(screen.getByRole('button', { name: '下一步', exact: true }));
await screen.findByRole('alert');
expect(api.create).toHaveBeenCalledTimes(1);
expect(screen.getByLabelText('名称')).toBeDisabled();
expect(screen.getByRole('button', { name: '更换头像' })).toBeDisabled();
expect(screen.getByRole('button', { name: '移除头像' })).toBeDisabled();
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]);
expect(api.create.mock.calls[0][0]).toEqual({ operation_id: expect.any(String), name: '写作搭档', purpose: '', icon: avatar.icon });
expect(api.uploadAvatar).toHaveBeenCalledTimes(1);
expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', avatar.icon_url);
});
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: '继续制作', exact: true }));
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: '继续制作', exact: true }));
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(<RouterProvider key="other-account" router={view.router} />);
await screen.findByText('做一个属于你的 AI 助手吧');
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.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]);
});
it('saves an existing configured draft without creating or loading a configuration chat', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
api.save.mockResolvedValue({ ...configured, name: '新的名称', draft_revision: 2 });
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(screen.queryByRole('region', { name: '智能体试用' })).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledTimes(1);
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(api.call.mock.calls.some(([operation]) => ['threads', 'history', 'preview', 'submit', 'publish'].includes(operation))).toBe(false);
});
it('switches editor pages while retaining unfinished knowledge and budget form inputs', async () => {
api.list.mockResolvedValue({ agents: [{ ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } }], next_cursor: null });
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
const requirements = screen.getByRole('tab', { name: '介绍与提示词', exact: true });
const advanced = screen.getByRole('tab', { name: '高级设置', exact: true });
expect(requirements).toHaveAttribute('aria-selected', 'true');
expect(advanced).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tabpanel', { name: '介绍与提示词' })).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
const instructions = screen.getByLabelText('提示词');
fireEvent.change(instructions, { target: { value: '没有保存的要求' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(advanced).toHaveAttribute('aria-selected', 'true');
expect(requirements).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tabpanel', { name: '高级设置' })).toBeVisible();
expect(instructions).not.toBeVisible();
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
fireEvent.click(screen.getByText('管理我的知识文档'));
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
const knowledgeName = screen.getByLabelText('知识库名称');
fireEvent.change(knowledgeName, { target: { value: '还没创建的资料' } });
const budgetInput = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budgetInput).toBeEnabled());
fireEvent.change(budgetInput, { target: { value: '12.50' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(knowledgeName).toBeVisible();
expect(budgetInput).toBeVisible();
fireEvent.mouseDown(requirements, { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(budgetInput).not.toBeVisible();
expect(screen.getByLabelText('提示词')).toBe(instructions);
expect(instructions).toHaveValue('没有保存的要求');
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('每日上限(词元点数)')).toBe(budgetInput);
expect(budgetInput).toHaveValue(12.5);
expect(screen.getByLabelText('知识库名称')).toBe(knowledgeName);
expect(knowledgeName).toHaveValue('还没创建的资料');
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('retains unsaved instructions while saving a budget independently of the draft', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'saveBudget' ? { ...input, daily_committed_points: '0.00' } : base(operation, input));
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '未保存的指令' } });
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
const budget = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '12' } });
fireEvent.click(screen.getByRole('button', { name: '保存费用上限' }));
await waitFor(() => expect(api.call).toHaveBeenCalledWith('saveBudget', expect.objectContaining({ daily_limit_points: '12' })));
expect(api.save).not.toHaveBeenCalled();
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词', exact: true }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令');
expect(screen.getByRole('button', { name: '保存草稿' })).toBeEnabled();
expect(api.call.mock.calls.some(([operation]) => ['threads', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('saves a cleared model selection without choosing another model or starting a preview', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'catalog'
? { models: [{ id: 'test-model', name: '创作模型' }, { id: 'another-model', name: '其他模型' }], resources: {}, pricing: null }
: base(operation, input));
api.save.mockResolvedValue({ ...draft, draft_revision: 2 });
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
fireEvent.change(screen.getByLabelText('模型'), { target: { value: '' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('云端已保存');
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ model: '' }) }));
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(api.call.mock.calls.some(([operation]) => ['threads', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('searches capabilities while retaining existing selections outside the results', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model', tools: ['missing-tool'] } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
models: [{ id: 'test-model', name: '写作模型' }], resources: { tools: [{ key: 'search', name: '资料搜索', description: '搜索素材' }, { key: 'clock', name: '当前时间' }] }, pricing: null,
} : base(operation, input));
api.save.mockImplementation(async (_slug, input) => ({ ...configured, ...input, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(await screen.findByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true }));
fireEvent.change(screen.getByLabelText('搜索工具箱'), { target: { value: '资料' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('搜索工具箱')).toHaveValue('资料');
expect(screen.queryByRole('checkbox', { name: '当前时间' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('checkbox', { name: /资料搜索/ }));
fireEvent.click(screen.getByRole('button', { name: '完成选择' }));
expect(screen.getByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await waitFor(() => expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ tools: ['missing-tool', 'search'] }) })));
});