feat(trial): assemble completed Agent workspace updates

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 12:11:56 +08:00
1 parent 8289f27f3e
commit 7c51bb4a00
38 files changed
+2081 -276

No files matched your search

+146 -3
View File
@@ -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'] }),
})));
});