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: }, { path: '/module-select', element:
模块首页
}, ], { initialEntries: ['/cloud-agents'] }); const view = render(); 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 chat read-only and protects unsent input before returning to the card to configure', 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'); const message = await screen.findByLabelText('消息'); await waitFor(() => expect(message).toBeEnabled()); expect(screen.getByRole('heading', { name: '写作搭档', exact: true })).toBeVisible(); expect(screen.queryByLabelText('名称')).not.toBeInTheDocument(); expect(screen.queryByRole('navigation', { name: '对话来源' })).not.toBeInTheDocument(); for (const name of ['修改名称', '修改头像', '已保存', '保存草稿', '已应用', '应用最新修改', '调整智能体', '查看历史对话', '新对话']) { expect(screen.queryByRole('button', { name, exact: true })).not.toBeInTheDocument(); } fireEvent.change(message, { target: { value: '保留正式聊天的问题' } }); fireEvent.keyDown(window, { key: 's', ctrlKey: true }); expect(api.save).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '返回智能体列表', exact: true })); expect(screen.getByRole('dialog', { name: '未保存的修改' })).toHaveTextContent('还有未发送的内容或未确认的操作'); fireEvent.click(screen.getByRole('button', { name: '继续编辑', exact: true })); expect(message).toBeVisible(); expect(message).toHaveValue('保留正式聊天的问题'); fireEvent.change(message, { target: { value: '' } }); fireEvent.click(screen.getByRole('button', { name: '返回智能体列表', exact: true })); fireEvent.click(await screen.findByRole('button', { name: '调整', exact: true })); 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.queryByLabelText('消息')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '我的智能体', exact: true })); fireEvent.click(await screen.findByRole('button', { name: '和它聊聊', exact: true })); await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled()); expect(screen.getByLabelText('消息')).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(); 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: [] }; if (operation === 'publish') return { version: 1, draft_revision: input.expected_revision }; 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('region', { name: '智能体对话' })).toBeVisible(); expect(screen.queryByRole('button', { name: '确认开始使用' })).not.toBeInTheDocument(); expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')).toHaveLength(1); expect(api.call.mock.calls.some(([operation]) => ['preview', 'submit'].includes(operation))).toBe(false); }); it('shows only model and capability settings together and preserves unfinished selections and search 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.queryByText('选它做事时能用的工具。', { exact: true })).not.toBeInTheDocument(); expect(screen.queryByText('给它一本手册,照着方法做。', { exact: true })).not.toBeInTheDocument(); expect(screen.getByRole('group', { name: /^知识库/ })).toBeVisible(); expect(screen.queryByText('管理我的云端能力', { exact: true })).not.toBeInTheDocument(); expect(screen.queryByRole('heading', { name: '费用上限', exact: true })).not.toBeInTheDocument(); expect(screen.queryByRole('heading', { name: '运行限制', exact: true })).not.toBeInTheDocument(); expect(screen.queryByLabelText('工具审批')).not.toBeInTheDocument(); 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 })); 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(); 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(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]) => ['budget', 'saveBudget', 'resources', '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('恢复了旧对话'); expect(api.call).toHaveBeenCalledWith('history', { thread_id: 'older-thread' }); expect(api.call.mock.calls.some(call => call[0] === 'history' && call[1].thread_id === 'newer-thread')).toBe(false); expect(screen.queryByRole('button', { name: '查看历史对话' })).not.toBeInTheDocument(); expect(screen.queryByLabelText('历史对话')).not.toBeInTheDocument(); 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(); 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 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: '还没创建的资料' } }); fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false }); expect(knowledgeName).toBeVisible(); fireEvent.mouseDown(requirements, { button: 0, ctrlKey: false }); expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument(); expect(knowledgeName).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(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('preserves hidden runtime policies while saving model, prompt and capability changes without touching the budget', async () => { const configuration = { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model', tool_approval_mode: 'always_trust' as const, max_execution_steps: 70, max_output_tokens: 7200, max_run_seconds: 180 }; const configured = { ...draft, configuration }; 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: { tools: [{ key: 'writing-tool', 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.change(screen.getByLabelText('提示词'), { target: { value: '未保存的指令' } }); expect(screen.queryByLabelText('消息')).not.toBeInTheDocument(); expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令'); fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false }); await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled()); fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'another-model' } }); fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true })); fireEvent.click(screen.getByRole('checkbox', { name: '文章整理工具', exact: true })); expect(screen.queryByLabelText('最多执行步数')).not.toBeInTheDocument(); expect(screen.queryByLabelText('每日上限(词元点数)')).not.toBeInTheDocument(); 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(); fireEvent.click(screen.getByRole('button', { name: '保存草稿' })); await screen.findByRole('button', { name: '已保存' }); expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ system_prompt: '未保存的指令', configuration: { ...configuration, model: 'another-model', tools: ['writing-tool'] }, })); expect(api.call.mock.calls.some(([operation]) => ['budget', 'saveBudget', 'resources', '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'] }) }))); });