fix(agents): surface unapplied draft changes and publish action
This commit is contained in:
@@ -12,6 +12,7 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
BrowserWindow.getAllWindows()[0].setSize(1366, 768);
|
||||
let draft: Record<string, unknown> | null = null;
|
||||
let recent: unknown = null;
|
||||
const versions: { version: number; draft_revision: number; created_at: string }[] = [];
|
||||
let knowledgeCatalogLoads = 0;
|
||||
let knowledgeCreated = false;
|
||||
let knowledgeUploaded = false;
|
||||
@@ -68,8 +69,8 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
if (body.operation === 'channelSelfCallers') return result({ items: [] });
|
||||
if (body.operation === 'channelBindings') return result({ items: [], available_accounts: [] });
|
||||
if (body.operation === 'threads') return result({ threads: [], next_offset: null });
|
||||
if (body.operation === 'publish') { if (draft) draft.published_version = 1; return result({ version: 1, draft_revision: 2 }); }
|
||||
if (body.operation === 'access') return result({ published_version: draft?.published_version, enabled: true, share_url: 'niancode://agents/' + draft?.slug, versions: [], grants: [], applications: [] });
|
||||
if (body.operation === 'publish') { const version = versions.length + 1; if (draft) draft.published_version = version; versions.push({ version, draft_revision: input.expected_revision, created_at: new Date().toISOString() }); return result({ version, draft_revision: input.expected_revision }); }
|
||||
if (body.operation === 'access') return result({ published_version: draft?.published_version, enabled: true, share_url: 'niancode://agents/' + draft?.slug, versions, grants: [], applications: [] });
|
||||
if (body.operation === 'costs') return result({ items: [], next_offset: null, unit: '词元点数', summary: { count: 0, settled_points: '0.00', pending_points: '0.00' } });
|
||||
if (body.operation === 'schedules') return result({ jobs });
|
||||
if (body.operation === 'createSchedule') { const job = { ...input, id: 'job-1', agent_slug: draft?.slug, next_run_at: '2026-09-11T01:00:00Z', runs: [] }; jobs.push(job); return result(job); }
|
||||
@@ -272,6 +273,21 @@ test('personal cloud Agent creation, draft save and leave protection in Electron
|
||||
await page.getByRole('button', { name: '确认开始使用' }).click();
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true }).getByRole('heading', { name: '创作建议' })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '编辑', exact: true }).click();
|
||||
await page.getByLabel('名称', { exact: true }).fill('我的新版写作搭档');
|
||||
await expect(page.getByRole('button', { name: '应用最新修改', exact: true })).toBeDisabled();
|
||||
await page.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
await expect(page.getByText('修改已保存,尚未应用', { exact: true })).toBeVisible();
|
||||
const applyButton = page.getByRole('button', { name: '应用最新修改', exact: true });
|
||||
await expect(applyButton).toBeInViewport();
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-apply-visible.png') });
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1024, 768));
|
||||
await expect(applyButton).toBeInViewport();
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-apply-compact.png') });
|
||||
await applyButton.click();
|
||||
await expect(page.getByRole('button', { name: '已应用', exact: true })).toBeDisabled();
|
||||
await expect(page.getByText('修改已保存,尚未应用', { exact: true })).toHaveCount(0);
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1366, 768));
|
||||
await page.getByRole('button', { name: '对话', exact: true }).click();
|
||||
await page.getByRole('region', { name: '智能体对话', exact: true }).getByLabel('消息', { exact: true }).fill('每天早上八点半给我三个创作方向');
|
||||
await page.getByRole('region', { name: '智能体对话', exact: true }).getByRole('button', { name: '发送', exact: true }).click();
|
||||
|
||||
@@ -3,7 +3,14 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-libra
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { CloudApproval, CloudChat } from '@/pages/CloudAgents/CloudChat';
|
||||
import { AgentRequirements } from '@/pages/CloudAgents/AgentRequirements';
|
||||
import { CloudAccessPanel } from '@/pages/CloudAgents/CloudAccess';
|
||||
import { CloudAccessPanel as AccessPanel } from '@/pages/CloudAgents/CloudAccess';
|
||||
|
||||
import { useCloudPublication } from '@/pages/CloudAgents/useCloudPublication';
|
||||
|
||||
function CloudAccessPanel(props: { slug: string; revision: number; onPublished: () => void }) {
|
||||
const publication = useCloudPublication(props.slug, props.revision, props.onPublished);
|
||||
return <AccessPanel slug={props.slug} revision={props.revision} publication={publication} />;
|
||||
}
|
||||
|
||||
const api = vi.hoisted(() => ({ call: vi.fn(), events: vi.fn(), download: vi.fn(), recovery: vi.fn(), remember: vi.fn() }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
@@ -36,7 +43,7 @@ it('starts private use through the existing publication action and retains its i
|
||||
let attempts = 0;
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'access') return { published_version: published ? 1 : null, enabled: true, versions: [], grants: [], applications: [] };
|
||||
if (operation === 'publish') { if (++attempts === 1) throw new Error('连接中断'); published = true; return { version: 1 }; }
|
||||
if (operation === 'publish') { if (++attempts === 1) throw new Error('连接中断'); published = true; return { version: 1, draft_revision: input.expected_revision }; }
|
||||
if (operation === 'costs') return { items: [], next_offset: null, summary: {} };
|
||||
if (operation === 'channelBindings') return { items: [] };
|
||||
return base(operation, input);
|
||||
|
||||
@@ -47,6 +47,125 @@ it('does not label disabled or archived assistants as ready to use', async () =>
|
||||
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 }));
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { CloudChat } from '@/pages/CloudAgents/CloudChat';
|
||||
import { CloudAccessPanel } from '@/pages/CloudAgents/CloudAccess';
|
||||
import { CloudAccessPanel as AccessPanel } from '@/pages/CloudAgents/CloudAccess';
|
||||
import { CloudSchedules } from '@/pages/CloudAgents/CloudSchedules';
|
||||
import { CloudBudgetEditor, CloudCosts } from '@/pages/CloudAgents/CloudCosts';
|
||||
import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents';
|
||||
|
||||
import { useCloudPublication } from '@/pages/CloudAgents/useCloudPublication';
|
||||
|
||||
function CloudAccessPanel(props: { slug: string; revision: number; onPublished: () => void }) {
|
||||
const publication = useCloudPublication(props.slug, props.revision, props.onPublished);
|
||||
return <AccessPanel slug={props.slug} revision={props.revision} publication={publication} />;
|
||||
}
|
||||
|
||||
const api = vi.hoisted(() => ({ call: vi.fn(), events: vi.fn(), upload: vi.fn(), download: vi.fn(), remember: vi.fn(), recovery: vi.fn() }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
class Stream extends EventTarget { close = vi.fn(); onopen = null; onerror = null; }
|
||||
|
||||
Reference in New Issue
Block a user