fix: clarify application retry prompts
This commit is contained in:
1 parent
1a496dd4fb
commit
5a8b8d75e5
12 files changed
+150
-29
No files matched your search
@@ -83,7 +83,7 @@ type FixtureCall = { path: string; method: string; input: Record<string, unknown
|
||||
type RecoveryFixture = { agents: CloudAgentDraft[]; pending: CloudPendingOperation[]; retry: 'fail' | 'success'; calls: FixtureCall[] };
|
||||
type FixtureWindow = Window & { __recoveryFixture: RecoveryFixture };
|
||||
|
||||
async function installFixture(page: Page, count = 2) {
|
||||
async function installFixture(page: Page, count = 2, publication = false) {
|
||||
page.on('pageerror', error => { throw error; });
|
||||
await page.addInitScript(({ agent, records, retryError }) => {
|
||||
const scope = window as unknown as FixtureWindow;
|
||||
@@ -104,7 +104,12 @@ async function installFixture(page: Page, count = 2) {
|
||||
const record = state.pending.find(item => item.id === input?.id);
|
||||
if (!record) throw new Error('Recovery must keep the original pending id');
|
||||
if (input?.discard) { state.pending = state.pending.filter(item => item.id !== record.id); return result({ discarded: true }); }
|
||||
if (state.retry === 'fail') return { ok: true, data: { status: 503, ok: false, json: { error: retryError } } };
|
||||
if (state.retry === 'fail') return { ok: true, data: { status: 503, ok: false, json: { error: retryError,
|
||||
...(record.operation === 'publish' ? { code: 'cloud_service_unavailable', commandOutcome: 'unknown' } : {}) } } };
|
||||
if (record.operation === 'publish') {
|
||||
state.pending = state.pending.filter(item => item.id !== record.id);
|
||||
return result({ operation: 'publish', result: { version: 1, draft_revision: record.input.expected_revision } });
|
||||
}
|
||||
const created = { ...agent, slug: 'ml-' + 'b'.repeat(32), name: String(record.input.name), published_version: null };
|
||||
state.agents.push(created); state.pending = state.pending.filter(item => item.id !== record.id);
|
||||
return result({ operation: 'createAgent', result: created });
|
||||
@@ -124,7 +129,9 @@ async function installFixture(page: Page, count = 2) {
|
||||
}
|
||||
},
|
||||
} } });
|
||||
}, { agent, records: records.slice(0, count), retryError });
|
||||
}, { agent, records: publication ? [{ id: 'e2e-publish-original', operation: 'publish',
|
||||
input: { slug: agent.slug, operation_id: 'e2e-publish-original', expected_revision: 1 }, created_at: records[0].created_at }]
|
||||
: records.slice(0, count), retryError });
|
||||
}
|
||||
|
||||
async function recoveryCalls(page: Page) {
|
||||
@@ -192,6 +199,27 @@ test('retry keeps the same pending identity and errors remain inside the modal',
|
||||
expect(await recoveryCalls(page)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('an application reminder gives a simple action and reuses its record after a network failure', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 850 }); await installFixture(page, 1, true); await page.goto(baseUrl);
|
||||
await page.getByRole('button', { name: /^待处理记录/ }).click();
|
||||
const dialog = page.getByRole('dialog', { name: '待处理记录', exact: true });
|
||||
await expect(dialog.getByRole('heading', { name: '应用设置', exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText('网络可能不稳定,请再点一次「应用」。', { exact: true })).toBeVisible();
|
||||
expect(await recoveryCalls(page)).toEqual([]);
|
||||
const apply = dialog.getByRole('button', { name: '应用', exact: true });
|
||||
await apply.click();
|
||||
await expect(dialog.getByRole('alert')).toHaveText('网络可能不稳定,请再点一次「应用」。');
|
||||
await expect(apply).toBeEnabled(); await expectModalFits(dialog);
|
||||
await page.screenshot({ path: test.info().outputPath('application-message-375.png'), animations: 'disabled' });
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__recoveryFixture.retry = 'success'; });
|
||||
await apply.click();
|
||||
await expect(dialog.getByText('设置已应用。', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText('查看结果', { exact: true })).toHaveCount(0);
|
||||
expect((await recoveryCalls(page)).map(call => call.input)).toEqual([
|
||||
{ id: 'e2e-publish-original', discard: false }, { id: 'e2e-publish-original', discard: false },
|
||||
]);
|
||||
});
|
||||
|
||||
test('discard is explicit and reminders do not follow into configuration, chat or creation', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 850 }); await installFixture(page); await page.goto(baseUrl);
|
||||
const trigger = page.getByRole('button', { name: /^创建记录/ });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { AppError } from '@/lib/error-model';
|
||||
import CloudAgents from '@/pages/CloudAgents';
|
||||
import { EMPTY_CLOUD_CONFIGURATION } from '../../shared/cloud-agents';
|
||||
|
||||
@@ -149,17 +150,17 @@ it('retries an uncertain activation with the original operation and keeps pendin
|
||||
const base = api.call.getMockImplementation()!;
|
||||
let attempts = 0;
|
||||
api.call.mockImplementation(async (operation, input) => {
|
||||
if (operation === 'publish' && ++attempts === 1) throw new Error('连接中断');
|
||||
if (operation === 'publish' && ++attempts === 1) throw new AppError('NETWORK', '连接中断', undefined, { commandOutcome: 'unknown' });
|
||||
return base(operation, input);
|
||||
});
|
||||
await openConfiguration();
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
|
||||
await screen.findByText(/连接中断/);
|
||||
await screen.findByText(/网络可能不稳定,请再点一次「应用」。/);
|
||||
expect(screen.queryByRole('region', { name: '智能体对话' })).not.toBeInTheDocument();
|
||||
const unload = new Event('beforeunload', { cancelable: true });
|
||||
window.dispatchEvent(unload);
|
||||
expect(unload.defaultPrevented).toBe(true);
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '应用', exact: true }));
|
||||
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
|
||||
expect(publications()).toHaveLength(2);
|
||||
expect(publications()[1][1]).toEqual(publications()[0][1]);
|
||||
@@ -180,7 +181,7 @@ it('keeps a newer saved revision unapplied when retry resolves the original unce
|
||||
versions: activated ? [{ version: 1, draft_revision: 3, created_at: '' }] : [],
|
||||
};
|
||||
if (operation === 'publish') {
|
||||
if (++attempts === 1) throw new Error('连接中断');
|
||||
if (++attempts === 1) throw new AppError('NETWORK', '连接中断', undefined, { commandOutcome: 'unknown' });
|
||||
activated = true;
|
||||
current = { ...current, published_version: 1 };
|
||||
return { version: 1, draft_revision: input.expected_revision };
|
||||
@@ -189,11 +190,11 @@ it('keeps a newer saved revision unapplied when retry resolves the original unce
|
||||
});
|
||||
await openConfiguration();
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
|
||||
await screen.findByText(/连接中断/);
|
||||
await screen.findByText(/网络可能不稳定,请再点一次「应用」。/);
|
||||
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: '这次新保存的要求' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存草稿', exact: true }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '已保存', exact: true })).toBeDisabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试开始使用', exact: true }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '应用', exact: true }));
|
||||
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: '应用最新修改', exact: true })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回智能体列表', exact: true }));
|
||||
@@ -225,7 +226,7 @@ it('keeps successful activation and conversation when the following status read
|
||||
expect(await screen.findByRole('region', { name: '智能体对话' })).toBeVisible();
|
||||
await waitFor(() => expect(api.call.mock.calls.filter(([operation]) => operation === 'access')).toHaveLength(2));
|
||||
expect(screen.queryByRole('button', { name: '已应用', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '重试开始使用', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '应用', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/状态刷新失败/)).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回智能体列表', exact: true }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '调整', exact: true }));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CloudApproval, CloudChat } from '@/pages/CloudAgents/CloudChat';
|
||||
import { AgentRequirements } from '@/pages/CloudAgents/AgentRequirements';
|
||||
import { CloudAccessPanel as AccessPanel } from '@/pages/CloudAgents/CloudAccess';
|
||||
|
||||
import { AppError } from '@/lib/error-model';
|
||||
import { useCloudPublication } from '@/pages/CloudAgents/useCloudPublication';
|
||||
|
||||
function CloudAccessPanel(props: { slug: string; revision: number; onPublished: () => void }) {
|
||||
@@ -43,7 +44,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, draft_revision: input.expected_revision }; }
|
||||
if (operation === 'publish') { if (++attempts === 1) throw new AppError('NETWORK', '连接中断', undefined, { commandOutcome: 'unknown' }); 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);
|
||||
@@ -53,9 +54,9 @@ it('starts private use through the existing publication action and retains its i
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '确认开始使用' })).toBeEnabled());
|
||||
expect(screen.queryByLabelText('应用名称')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认开始使用' }));
|
||||
await screen.findByText(/连接中断/);
|
||||
await screen.findByText(/网络可能不稳定,请再点一次「应用」。/);
|
||||
view.rerender(<CloudAccessPanel slug={slug} revision={3} onPublished={onPublished} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试本次发布' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '应用' }));
|
||||
await screen.findByText('助手已准备好');
|
||||
const publications = api.call.mock.calls.filter(([operation]) => operation === 'publish');
|
||||
expect(publications).toHaveLength(2);
|
||||
@@ -65,6 +66,24 @@ it('starts private use through the existing publication action and retains its i
|
||||
expect(api.call.mock.calls.some(([operation]) => operation === 'share' || operation === 'createApplication')).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ status: 401, code: 'session_expired', message: '请重新登录' },
|
||||
{ status: 403, code: 'access_revoked', message: '无权使用这个智能体' },
|
||||
{ status: 409, code: 'draft_revision_conflict', message: '云端草稿已更新' },
|
||||
{ status: 422, code: 'invalid_input', message: '请选择模型' },
|
||||
{ status: 429, code: 'cloud_service_unavailable', message: '请求过于频繁,请稍后再试' },
|
||||
])('keeps the actual application rejection for HTTP $status', async ({ status, code, message }) => {
|
||||
api.call.mockImplementation(async operation => {
|
||||
if (operation === 'access') return { published_version: null, enabled: true, versions: [], grants: [], applications: [] };
|
||||
throw new AppError('UNKNOWN', message, undefined, { status, backendCode: code, commandOutcome: 'unknown' });
|
||||
});
|
||||
render(<CloudAccessPanel slug={slug} revision={2} onPublished={vi.fn()} />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '确认开始使用' })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认开始使用' }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(message);
|
||||
expect(screen.getByRole('alert')).not.toHaveTextContent('网络可能不稳定');
|
||||
});
|
||||
|
||||
it('preserves the entire prompt when displaying and editing former style instruction lines', () => {
|
||||
const initial = '我的规则\n\n 保留缩进和解释。\n使用简单易懂的语言,遇到新词先解释。\n把任务拆成小步骤,一次引导我完成一步。\n先给我提示和思考问题,让我尝试后再补充答案。\n';
|
||||
const edited = '先给我提示和思考问题,让我尝试后再补充答案。\n\n 我的新规则\n使用简单易懂的语言,遇到新词先解释。\n把任务拆成小步骤,一次引导我完成一步。\n\n';
|
||||
|
||||
@@ -145,8 +145,8 @@ it('retries a failed application from the header using the original publication
|
||||
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: '重试本次应用' }));
|
||||
expect(screen.getByRole('button', { name: '应用' })).toBeEnabled();
|
||||
fireEvent.click(screen.getByRole('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);
|
||||
@@ -187,7 +187,7 @@ it('keeps a successful application confirmed when its following status refresh f
|
||||
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();
|
||||
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);
|
||||
|
||||
@@ -47,6 +47,39 @@ it('uses a general records entry for mixed operations and keeps the original mes
|
||||
expect(api.resolvePending).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('explains a pending application in simple language and retries only its original record', async () => {
|
||||
const publication: CloudPendingOperation = { ...first, id: 'publish:original', operation: 'publish',
|
||||
input: { slug: 'ml-' + 'a'.repeat(32), operation_id: 'original-publication', expected_revision: 3 } };
|
||||
api.resolvePending.mockRejectedValueOnce(new AppError('UNKNOWN', '智能体服务暂时不可用,请重试', undefined,
|
||||
{ status: 502, backendCode: 'cloud_service_unavailable', commandOutcome: 'unknown' }))
|
||||
.mockResolvedValueOnce({ result: { version: 2, draft_revision: 3 } });
|
||||
render(<CloudRecovery pending={[publication]} onResolved={resolved} />);
|
||||
await openRecords();
|
||||
const row = screen.getByRole('article', { name: '应用设置' });
|
||||
expect(within(row).getByText('网络可能不稳定,请再点一次「应用」。')).toBeVisible();
|
||||
expect(screen.queryByText('发布版本')).not.toBeInTheDocument();
|
||||
expect(api.resolvePending).not.toHaveBeenCalled();
|
||||
fireEvent.click(within(row).getByRole('button', { name: '应用', exact: true }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('网络可能不稳定,请再点一次「应用」。');
|
||||
expect(resolved).not.toHaveBeenCalled();
|
||||
fireEvent.click(within(row).getByRole('button', { name: '应用', exact: true }));
|
||||
expect(await screen.findByText('设置已应用。')).toBeVisible();
|
||||
expect(api.resolvePending.mock.calls).toEqual([[publication.id, false], [publication.id, false]]);
|
||||
expect(screen.queryByText('查看结果')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps a publication conflict explicit instead of calling it a network failure', async () => {
|
||||
const publication: CloudPendingOperation = { ...first, id: 'publish:conflict', operation: 'publish' };
|
||||
api.resolvePending.mockRejectedValue(new AppError('UNKNOWN', '云端草稿已更新,请重新读取后再保存', undefined,
|
||||
{ status: 409, backendCode: 'draft_revision_conflict', commandOutcome: 'unknown' }));
|
||||
render(<CloudRecovery pending={[publication]} onResolved={resolved} />);
|
||||
await openRecords(); fireEvent.click(screen.getByRole('button', { name: '应用', exact: true }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('云端草稿已更新,请重新读取后再保存');
|
||||
expect(screen.getByRole('alert')).not.toHaveTextContent('网络可能不稳定');
|
||||
expect(screen.queryByText('网络可能不稳定,请再点一次「应用」。')).not.toBeInTheDocument();
|
||||
expect(api.resolvePending).toHaveBeenCalledWith(publication.id, false);
|
||||
});
|
||||
|
||||
it('keeps failed records and retries the same record ID after closing and reopening the dialog', async () => {
|
||||
api.resolvePending.mockRejectedValueOnce(new Error('原操作仍未确认')).mockResolvedValueOnce({ result: { slug: 'original-agent' } });
|
||||
render(<CloudRecovery pending={[first]} onResolved={resolved} />);
|
||||
|
||||
Reference in new issue
Block a user