feat(trial): assemble completed Agent workspace updates
This commit is contained in:
1 parent
8289f27f3e
commit
7c51bb4a00
38 files changed
+2081
-276
No files matched your search
@@ -2,6 +2,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testi
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import CloudAgents from '@/pages/CloudAgents';
|
||||
import { AppError } from '@/lib/error-model';
|
||||
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() }));
|
||||
@@ -610,6 +611,37 @@ it('guides an unconfirmed WeChat verification back to its code entry without cla
|
||||
expect(api.resolvePending).toHaveBeenCalledWith('wechat-code', false);
|
||||
});
|
||||
|
||||
it('refreshes recovery after a definitive rejection without presenting a confirmed result', async () => {
|
||||
const pending = { id: 'create:pending', operation: 'createAgent', input: { operation_id: 'same-operation', name: '旧搭档', purpose: '' }, created_at: '2026-09-13T01:00:00Z' };
|
||||
api.recovery.mockResolvedValueOnce({ recent: null, pending: [pending] }).mockResolvedValue({ recent: null, pending: [] });
|
||||
api.resolvePending.mockRejectedValue(new AppError('CONFIG', '名称不可用', undefined, { commandOutcome: 'definitive_failure' }));
|
||||
open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '重试并确认结果' }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('创建智能体失败:名称不可用');
|
||||
await waitFor(() => expect(api.recovery).toHaveBeenCalledTimes(2));
|
||||
expect(screen.queryByRole('button', { name: '重试并确认结果' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('创建智能体的结果已确认。')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('查看结果')).not.toBeInTheDocument();
|
||||
expect(api.resolvePending).toHaveBeenCalledWith(pending.id, false);
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an old recovery record retryable when a parameter response cannot determine the original outcome', async () => {
|
||||
const pending = { id: 'create:old', operation: 'createAgent', input: { operation_id: 'original-operation', name: '旧搭档', purpose: '' }, created_at: '2026-09-13T01:00:00Z' };
|
||||
api.recovery.mockResolvedValue({ recent: null, pending: [pending] });
|
||||
api.resolvePending.mockRejectedValue(new AppError('CONFIG', '请检查名称', undefined, { commandOutcome: 'unknown', status: 422, code: 'invalid_input' }));
|
||||
open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '重试并确认结果' }));
|
||||
await screen.findByRole('alert');
|
||||
expect(screen.getByRole('button', { name: '重试并确认结果' })).toBeEnabled();
|
||||
expect(screen.queryByText('创建智能体的结果已确认。')).not.toBeInTheDocument();
|
||||
expect(api.recovery).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试并确认结果' }));
|
||||
await waitFor(() => expect(api.resolvePending).toHaveBeenCalledTimes(2));
|
||||
expect(api.resolvePending.mock.calls).toEqual([[pending.id, false], [pending.id, false]]);
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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' }] })
|
||||
@@ -648,6 +680,108 @@ it('freezes the uploaded avatar and creation identity when retrying a lost respo
|
||||
expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', avatar.icon_url);
|
||||
});
|
||||
|
||||
it('unlocks rejected creation input and uses a new operation identity after editing', async () => {
|
||||
const avatar = { icon: '/api/storage/public/images/rejected.png', icon_url: 'https://images.test/rejected.png' };
|
||||
api.uploadAvatar.mockResolvedValue(avatar);
|
||||
api.create.mockRejectedValueOnce(new AppError('CONFIG', '名称不可用', undefined, { commandOutcome: 'definitive_failure' }))
|
||||
.mockImplementationOnce(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 screen.findByRole('button', { name: '更换头像' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '下一步', exact: true }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('创建失败:名称不可用');
|
||||
await waitFor(() => expect(api.recovery).toHaveBeenCalledTimes(2));
|
||||
expect(screen.getByLabelText('名称')).toBeEnabled();
|
||||
expect(screen.getByLabelText('名称')).toHaveValue('原名称');
|
||||
expect(screen.getByRole('button', { name: '更换头像' })).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: '移除头像' })).toBeEnabled();
|
||||
expect(screen.queryByText(/创建结果暂未确认/)).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除头像' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '下一步', exact: true }));
|
||||
await screen.findByTestId('cloud-agent-editor');
|
||||
expect(api.create).toHaveBeenCalledTimes(2);
|
||||
const original = api.create.mock.calls[0][0];
|
||||
const corrected = api.create.mock.calls[1][0];
|
||||
expect(original).toMatchObject({ name: '原名称', purpose: '', icon: avatar.icon });
|
||||
expect(corrected).toMatchObject({ name: '新的名称', purpose: '', icon: null });
|
||||
expect(corrected.operation_id).not.toEqual(original.operation_id);
|
||||
expect(api.recovery).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('preserves the creation identity for unknown command outcomes even when the error says invalid input', async () => {
|
||||
const avatar = { icon: '/api/storage/public/images/unknown.png', icon_url: 'https://images.test/unknown.png' };
|
||||
let retainedPending: unknown[] = [];
|
||||
api.recovery.mockImplementation(async () => ({ recent: null, pending: retainedPending }));
|
||||
api.uploadAvatar.mockResolvedValue(avatar);
|
||||
api.create.mockImplementationOnce(async input => {
|
||||
retainedPending = [{ id: 'create:unknown', operation: 'createAgent', input, created_at: '2026-09-13T01:00:00Z' }];
|
||||
throw new AppError('CONFIG', '请检查名称', undefined, { commandOutcome: 'unknown', status: 422, code: 'invalid_input' });
|
||||
}).mockImplementationOnce(async input => {
|
||||
retainedPending = [];
|
||||
return { ...draft, ...input, icon_url: avatar.icon_url };
|
||||
});
|
||||
open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
|
||||
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 }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('创建结果暂未确认,可重试确认结果');
|
||||
await screen.findByText('有 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.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
await waitFor(() => expect(screen.queryByText('有 1 个操作的结果尚未确认')).not.toBeInTheDocument());
|
||||
expect(api.recovery).toHaveBeenCalledTimes(3);
|
||||
expect(api.uploadAvatar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps an uncertain creation identity after its reminder is removed and a retry is rejected', async () => {
|
||||
const avatar = { icon: '/api/storage/public/images/discarded.png', icon_url: 'https://images.test/discarded.png' };
|
||||
let retainedPending: unknown[] = [];
|
||||
api.recovery.mockImplementation(async () => ({ recent: null, pending: retainedPending }));
|
||||
api.uploadAvatar.mockResolvedValue(avatar);
|
||||
api.resolvePending.mockImplementation(async (_id, discard) => {
|
||||
expect(discard).toBe(true);
|
||||
retainedPending = [];
|
||||
return { discarded: true };
|
||||
});
|
||||
api.create.mockImplementationOnce(async input => {
|
||||
retainedPending = [{ id: 'create:discarded', operation: 'createAgent', input, created_at: '2026-09-13T01:00:00Z' }];
|
||||
throw new AppError('NETWORK', '连接中断', undefined, { commandOutcome: 'unknown' });
|
||||
}).mockRejectedValueOnce(new AppError('CONFIG', '参数被拒绝', undefined, { commandOutcome: 'definitive_failure' }))
|
||||
.mockImplementationOnce(async input => ({ ...draft, ...input, icon_url: avatar.icon_url }));
|
||||
open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: '创建智能体', exact: true }));
|
||||
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.findByText('有 1 个操作的结果尚未确认');
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除提醒', exact: true }));
|
||||
await waitFor(() => expect(screen.queryByText('有 1 个操作的结果尚未确认')).not.toBeInTheDocument());
|
||||
expect(api.resolvePending).toHaveBeenCalledWith('create:discarded', true);
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建', exact: true }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('创建结果暂未确认,可重试确认结果。参数被拒绝');
|
||||
expect(screen.getByLabelText('名称')).toBeDisabled();
|
||||
expect(screen.getByLabelText('名称')).toHaveValue('原搭档');
|
||||
expect(screen.getByRole('button', { name: '更换头像' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '移除头像' })).toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: '下一步', exact: true })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建', exact: true }));
|
||||
await screen.findByTestId('cloud-agent-editor');
|
||||
expect(api.create).toHaveBeenCalledTimes(3);
|
||||
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
expect(api.create.mock.calls[2][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
expect(api.uploadAvatar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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: '其他设备内容' });
|
||||
@@ -829,7 +963,10 @@ it('searches capabilities while retaining existing selections outside the result
|
||||
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,
|
||||
models: [{ id: 'test-model', name: '写作模型' }], resources: {
|
||||
tools: [{ key: 'web_search', name: '上网查资料', description: '在网上查找相关信息。' }, { key: 'clock', name: '当前时间' }],
|
||||
skills: [{ key: 'deep-research', name: '查资料,做研究', description: '比较不同说法,再整理出结论和出处。' }],
|
||||
}, pricing: null,
|
||||
} : base(operation, input));
|
||||
api.save.mockImplementation(async (_slug, input) => ({ ...configured, ...input, draft_revision: 2 }));
|
||||
open();
|
||||
@@ -842,9 +979,15 @@ it('searches capabilities while retaining existing selections outside the result
|
||||
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('checkbox', { name: /上网查资料/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '完成选择' }));
|
||||
expect(screen.getByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择做事手册', exact: true }));
|
||||
fireEvent.change(screen.getByLabelText('搜索做事手册'), { target: { value: '出处' } });
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /查资料,做研究/ }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: '开始对话前,先读好已选做事手册的使用方法' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
|
||||
await waitFor(() => expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ tools: ['missing-tool', 'search'] }) })));
|
||||
await waitFor(() => expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({
|
||||
configuration: expect.objectContaining({ tools: ['missing-tool', 'web_search'], skills: ['deep-research'], preload_skills: ['deep-research'] }),
|
||||
})));
|
||||
});
|
||||
Reference in new issue
Block a user