546 lines
35 KiB
TypeScript
546 lines
35 KiB
TypeScript
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'] }) })));
|
||
});
|