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

546 lines
35 KiB
TypeScript
Raw Permalink 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 } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import CloudAgents from '@/pages/CloudAgents';
import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents';
const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn(), save: vi.fn(), call: vi.fn(), events: vi.fn(), recovery: vi.fn(), remember: vi.fn(), 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 or archived assistants as ready to use', 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.getByText('已归档')).toBeVisible();
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('button', { name: '编辑', exact: true })).toHaveAttribute('aria-current', 'page');
});
it('uses the published draft revision, requires saving edits, and keeps trial inputs when applying', async () => {
publishedAssistant(4);
open();
await screen.findByTestId('cloud-agent-editor');
await waitFor(() => expect(screen.getByRole('button', { name: '已应用', exact: true })).toBeDisabled());
fireEvent.change(screen.getByLabelText('消息'), { target: { value: '保留试用问题' } });
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.getByLabelText('消息')).toHaveValue('保留试用问题');
expect(api.call.mock.calls.filter(([operation]) => operation === 'publish')[0][1]).toMatchObject({ expected_revision: 5 });
});
it('shares the original failed publication between the header and access page', 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: '使用与接入' }));
fireEvent.click(await screen.findByRole('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 a custom requirement before a name or purpose has been entered', async () => {
const { router } = open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '我自己想一个' }));
fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '请先听我说完' } });
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
expect(screen.getByLabelText('特别要求')).toHaveValue('请先听我说完');
});
it('creates from an editable starter, preserves chosen requirements and defaults only a new assistant model', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'catalog' ? {
models: [{ id: 'model-first', name: '可用模型甲' }, { id: 'model-second', name: '可用模型乙' }], resources: {}, pricing: null,
} : base(operation, input));
api.create.mockImplementation(async input => ({ ...draft, ...input, system_prompt: input.purpose }));
api.save.mockImplementation(async (_slug, input) => ({ ...draft, ...input, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
fireEvent.click(screen.getByRole('button', { name: /科学问答助手/ }));
fireEvent.click(screen.getByRole('button', { name: '先给我提示' }));
fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '一次只解释一个新词。' } });
fireEvent.click(screen.getByRole('button', { name: '下一步:试一试' }));
await screen.findByTestId('cloud-agent-editor');
await waitFor(() => expect(screen.getByLabelText('模型')).toHaveValue('model-first'));
expect(screen.getByLabelText('特别要求')).toHaveValue('一次只解释一个新词。');
expect(screen.getByRole('button', { name: '先给我提示' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.queryByRole('button', { name: '能力' })).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText('模型'), { target: { value: '' } });
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '我的科学助手' } });
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(screen.getByRole('button', { name: '保存并开始试用' })).toBeDisabled();
fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'model-second' } });
fireEvent.click(screen.getByRole('button', { name: '保存并开始试用' }));
await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled());
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({
name: '我的科学助手', system_prompt: '一次只解释一个新词。\n先给我提示和思考问题让我尝试后再补充答案。',
configuration: expect.objectContaining({ model: 'model-second' }),
}));
fireEvent.click(screen.getByRole('button', { name: '为什么雨后会出现彩虹?' }));
expect(screen.getByLabelText('消息')).toHaveValue('为什么雨后会出现彩虹?');
expect(api.call.mock.calls.some(([operation]) => operation === 'preview' || operation === 'submit')).toBe(false);
});
it('saves renamed existing assistants without rewriting arbitrary instructions or selecting a model', async () => {
const original = '自定义标题\n\n 保留缩进。\n简单易懂只是一个例子。\n';
const existing = { ...draft, system_prompt: original };
api.list.mockResolvedValue({ 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, ...input, draft_revision: 2 }));
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
expect(screen.getByLabelText('模型')).toHaveValue('');
expect(screen.getByLabelText('特别要求')).toHaveValue(original);
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({ system_prompt: original, configuration: expect.objectContaining({ model: '' }) }));
});
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: /写作搭档/ }));
await screen.findByText('目录暂不可用');
fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '保留我的想法' } });
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: /写作搭档/ });
fireEvent.click(screen.getByRole('button', { name: '创建智能体' }));
fireEvent.click(screen.getByRole('button', { name: '我自己想一个' }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '新用途' } });
fireEvent.click(existing);
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
expect(screen.getByLabelText('名称')).toHaveValue('新搭档');
fireEvent.click(screen.getByRole('button', { name: '下一步:试一试' }));
await screen.findByRole('alert');
fireEvent.click(existing);
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await screen.findByTestId('cloud-agent-editor');
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});
it('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('keeps the same creation operation after a lost response', async () => {
api.list.mockResolvedValue({ agents: [], next_cursor: null });
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(draft);
open();
fireEvent.click(await screen.findByText('创建第一个智能体'));
fireEvent.click(screen.getByRole('button', { name: '我自己想一个' }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
fireEvent.click(screen.getByRole('button', { name: '下一步:试一试' }));
await screen.findByRole('alert');
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await screen.findByTestId('cloud-agent-editor');
expect(api.create).toHaveBeenCalledTimes(2);
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});
it('preserves local input on conflicts and requires an explicit choice before retry', async () => {
api.save.mockRejectedValueOnce(new Error('草稿已更新'));
api.get.mockResolvedValue({ ...draft, draft_revision: 2, system_prompt: '其他设备内容' });
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '我的修改' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('云端已有修订 2本地输入仍保留');
expect(screen.getByLabelText('特别要求')).toHaveValue('我的修改');
expect(screen.getByRole('button', { name: '保存草稿' })).toBeDisabled();
fireEvent.click(screen.getByText('保留我的内容,继续编辑'));
api.save.mockResolvedValue({ ...draft, draft_revision: 3, system_prompt: '我的修改' });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save.mock.calls[1][1]).toMatchObject({ expected_revision: 2, system_prompt: '我的修改' });
});
it('blocks module navigation with unsaved edits, cancels leaving, then saves before proceeding', async () => {
const { router } = open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '未保存的修改' })).toBeVisible();
expect(router.state.location.pathname).toBe('/cloud-agents');
fireEvent.click(screen.getByText('继续编辑'));
expect(screen.getByLabelText('名称')).toHaveValue('新的名称');
await act(async () => { await router.navigate('/module-select'); });
api.save.mockResolvedValue({ ...draft, name: '新的名称', draft_revision: 2 });
fireEvent.click(screen.getByText('保存并离开'));
await screen.findByText('模块首页');
expect(api.save).toHaveBeenCalledTimes(1);
});
it('clears the previous account workspace and ignores its delayed list', async () => {
let finish!: (value: unknown) => void;
api.list.mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; }))
.mockResolvedValue({ agents: [], next_cursor: null });
const view = open();
auth.user.userId = 'other';
view.rerender(<RouterProvider key="other-account" router={view.router} />);
await screen.findByText('做一个懂你的小助手');
await act(async () => finish({ agents: [draft], next_cursor: null }));
await waitFor(() => expect(screen.queryByText('写作搭档')).not.toBeInTheDocument());
});
it('warns before leaving an uncertain creation and keeps its retry intent', async () => {
api.list.mockResolvedValue({ agents: [], next_cursor: null });
api.create.mockRejectedValue(new Error('连接中断'));
const { router } = open();
fireEvent.click(await screen.findByText('创建第一个智能体'));
fireEvent.click(screen.getByRole('button', { name: '我自己想一个' }));
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
fireEvent.click(screen.getByRole('button', { name: '下一步:试一试' }));
await screen.findByRole('alert');
await act(async () => { await router.navigate('/module-select'); });
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
await waitFor(() => expect(api.create).toHaveBeenCalledTimes(2));
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
});
it('keeps unsent preview text when configuration changes and waits before switching saved revision', 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: /写作搭档/ }));
const prompt = await screen.findByPlaceholderText('输入消息或任务目标…');
fireEvent.change(prompt, { target: { value: '还未发送的问题' } });
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
expect(prompt).toHaveValue('还未发送的问题');
expect(screen.getByText(/要求改好了/)).toBeVisible();
await waitFor(() => expect(screen.getByRole('button', { name: '保存草稿' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByRole('button', { name: '已保存' });
expect(api.save).toHaveBeenCalledTimes(1);
expect(screen.getByLabelText('消息')).toBe(prompt);
expect(prompt).toHaveValue('还未发送的问题');
expect(screen.getByText('当前试用 · 修订 1')).toBeVisible();
expect(screen.getByRole('button', { name: '发送', exact: true })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '用最新草稿试用' }));
await waitFor(() => expect(screen.getByText('当前试用 · 修订 2')).toBeVisible());
expect(prompt).toHaveValue('还未发送的问题');
expect(screen.getByRole('button', { name: '发送', exact: true })).toBeEnabled();
});
it('switches peer editor tabs while retaining the advanced category and unfinished 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: /写作搭档/ }));
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('特别要求');
const preview = screen.getByLabelText('消息');
fireEvent.change(instructions, { target: { value: '没有保存的要求' } });
fireEvent.change(preview, { 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.getByRole('button', { name: '能力', exact: true })).toHaveAttribute('aria-current', 'page');
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
const budgetInput = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budgetInput).toBeEnabled());
fireEvent.change(budgetInput, { target: { value: '12.50' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
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.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
expect(screen.getByLabelText('每日上限(词元点数)')).toBe(budgetInput);
expect(budgetInput).toHaveValue(12.5);
expect(screen.getByLabelText('消息')).toBe(preview);
expect(preview).toHaveValue('没有发送的问题');
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('keeps preview text across mode switches and saves 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: /写作搭档/ }));
await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled());
fireEvent.change(screen.getByLabelText('消息'), { target: { value: '待发送的问题' } });
fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '未保存的指令' } });
fireEvent.click(screen.getByRole('button', { name: '对话', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '编辑', exact: true }));
expect(screen.getByLabelText('消息')).toHaveValue('待发送的问题');
expect(screen.getByLabelText('特别要求')).toHaveValue('未保存的指令');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
fireEvent.change(screen.getByLabelText('每日上限(词元点数)'), { 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.getByLabelText('消息')).toHaveValue('待发送的问题');
});
it('keeps an existing preview when the saved draft clears its model selection', async () => {
const configured = { ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } };
api.list.mockResolvedValue({ agents: [configured], next_cursor: null });
api.save.mockResolvedValue({ ...draft, draft_revision: 2 });
open();
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled());
const prompt = screen.getByLabelText('消息');
fireEvent.change(prompt, { target: { value: '还没有发送' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '能力', exact: true }));
fireEvent.change(screen.getByLabelText('模型'), { target: { value: '' } });
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
await screen.findByText('云端已保存');
expect(screen.getByLabelText('消息')).toBe(prompt);
expect(prompt).toHaveValue('还没有发送');
expect(screen.getByRole('button', { name: '用最新草稿试用' })).toBeDisabled();
});
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: /写作搭档/ }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '能力', exact: true }));
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'] }) })));
});