rollback optimistic message on send failure

This commit is contained in:
2026-07-15 19:24:47 +08:00
parent 9856acb8c3
commit 3a30198fef
3 changed files with 78 additions and 7 deletions
@@ -446,6 +446,69 @@ describe('EditorAgentConversationPanelView', () => {
});
});
it('restores the draft and selected attachments when sending fails', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockRejectedValueOnce(
new Error('Network error'),
);
render(
<EditorAgentConversationPanelView
open
onToggleOpen={vi.fn()}
client={client}
layers={[
{
id: 'layer-1',
resourceId: 'resource-1',
title: '角色图层',
src: '/generated/role.png',
x: 0,
y: 0,
width: 512,
height: 512,
originalWidth: 512,
originalHeight: 512,
zIndex: 1,
sourceType: 'generated',
},
]}
/>,
);
await waitFor(() => {
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
const attachmentDialog = screen.getByRole('dialog', {
name: '选择图片附件',
});
fireEvent.click(
within(attachmentDialog).getByRole('checkbox', {
name: '选择画布图片 角色图层',
}),
);
fireEvent.click(
within(attachmentDialog).getByRole('button', { name: '应用' }),
);
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
target: { value: '失败后恢复这条草稿' },
});
fireEvent.click(screen.getByRole('button', { name: '发送' }));
expect(await screen.findByText('Network error')).toBeTruthy();
expect(
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
).toBe('失败后恢复这条草稿');
expect(screen.getByText('角色图层')).toBeTruthy();
expect(
within(screen.getByRole('log', { name: '画布 Agent 消息流' })).queryByText(
'失败后恢复这条草稿',
),
).toBeNull();
});
it('preserves messages when the panel is collapsed and reopened', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockResolvedValue({
@@ -553,7 +553,7 @@ describe('useEditorAgentConversation', () => {
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
});
it('handles fetch error as failed stage', async () => {
it('rethrows fetch errors and rolls back the optimistic message', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() =>
@@ -567,10 +567,13 @@ describe('useEditorAgentConversation', () => {
});
await act(async () => {
await expect(result.current.sendMessage('test')).resolves.toBeUndefined();
await expect(result.current.sendMessage('test')).rejects.toThrow(
'Network error',
);
});
expect(result.current.errorMessage).toBe('Network error');
expect(result.current.messages).toHaveLength(0);
});
it('aborts the active request and resets state on stopCurrentTurn', async () => {
@@ -355,13 +355,14 @@ export function useEditorAgentConversation({
activeRequestAbortControllerRef.current = abortController;
setErrorMessage(null);
setIsWaiting(true);
setMessages((currentMessages) => [
...currentMessages,
createLocalUserMessage({
id: -1, // does not care
const optimisticMessage = createLocalUserMessage({
id: -1,
text,
attachments,
}),
});
setMessages((currentMessages) => [
...currentMessages,
optimisticMessage,
]);
try {
@@ -394,6 +395,10 @@ export function useEditorAgentConversation({
const message =
error instanceof Error ? error.message : '发送画布 Agent 消息失败';
setErrorMessage(message);
setMessages((currentMessages) =>
currentMessages.filter((message) => message !== optimisticMessage),
);
throw error;
} finally {
if (activeRequestAbortControllerRef.current === abortController) {
activeRequestAbortControllerRef.current = null;