fix: await concurrent attachment uploads
This commit is contained in:
1 parent
1ca0249e69
commit
1fe8863e69
3 files changed
+20
-9
No files matched your search
@@ -156,7 +156,8 @@
|
|||||||
acceptance settle, so removal or a next-message paste cannot mutate the
|
acceptance settle, so removal or a next-message paste cannot mutate the
|
||||||
captured image set. Attachment preparation failures now remain local to the
|
captured image set. Attachment preparation failures now remain local to the
|
||||||
Composer, restore the draft, permit direct retry without runtime recovery,
|
Composer, restore the draft, permit direct retry without runtime recovery,
|
||||||
and retain already uploaded variants for reuse. Attachment route failures use
|
retain already uploaded variants for reuse, and wait for every concurrent
|
||||||
|
upload flight to settle before releasing the edit lock. Attachment route failures use
|
||||||
only registered `CODING_ATTACHMENT_INVALID`, `CODING_ATTACHMENT_NOT_FOUND`,
|
only registered `CODING_ATTACHMENT_INVALID`, `CODING_ATTACHMENT_NOT_FOUND`,
|
||||||
and `CODING_STORAGE_WRITE_FAILED` codes while preserving 413/404 status.
|
and `CODING_STORAGE_WRITE_FAILED` codes while preserving 413/404 status.
|
||||||
- Synchronized `README.md` with the current Pi core Chat, first-Conversation,
|
- Synchronized `README.md` with the current Pi core Chat, first-Conversation,
|
||||||
|
|||||||
@@ -392,7 +392,10 @@ export function CodingChatPanel({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all(workers);
|
const results = await Promise.allSettled(workers);
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === 'rejected') throw result.reason;
|
||||||
|
}
|
||||||
return prepared;
|
return prepared;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ describe('CodingChatPanel first Conversation', () => {
|
|||||||
expect(maxActiveUploads).toBeLessThanOrEqual(4);
|
expect(maxActiveUploads).toBeLessThanOrEqual(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('locks attachment removal while an upload is still pending', async () => {
|
it('keeps attachment removal locked until every upload settles after one fails', async () => {
|
||||||
const upload = deferred<{ attachmentId: string; mime: string; byteLength: number }>();
|
const upload = deferred<{ attachmentId: string; mime: string; byteLength: number }>();
|
||||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||||
projectApi.config.mockResolvedValue({ project, config });
|
projectApi.config.mockResolvedValue({ project, config });
|
||||||
@@ -383,7 +383,11 @@ describe('CodingChatPanel first Conversation', () => {
|
|||||||
runId: 'run-1',
|
runId: 'run-1',
|
||||||
mode: input.mode,
|
mode: input.mode,
|
||||||
}));
|
}));
|
||||||
attachmentApi.upload.mockReturnValue(upload.promise);
|
attachmentApi.upload.mockImplementation((file: File) => (
|
||||||
|
file.name === 'failed.png'
|
||||||
|
? Promise.reject(new Error('图片附件无效。'))
|
||||||
|
: upload.promise
|
||||||
|
));
|
||||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||||
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
||||||
const { codingConversationStore } = await import('@/stores/coding-conversations');
|
const { codingConversationStore } = await import('@/stores/coding-conversations');
|
||||||
@@ -393,21 +397,24 @@ describe('CodingChatPanel first Conversation', () => {
|
|||||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||||
.toBe(conversation.id));
|
.toBe(conversation.id));
|
||||||
|
|
||||||
const file = new File(['image'], 'held.png', { type: 'image/png' });
|
const failed = new File(['failed'], 'failed.png', { type: 'image/png' });
|
||||||
|
const held = new File(['held'], 'held.png', { type: 'image/png' });
|
||||||
fireEvent.change(screen.getByTestId('coding-file-attachment-input'), {
|
fireEvent.change(screen.getByTestId('coding-file-attachment-input'), {
|
||||||
target: { files: [file] },
|
target: { files: [failed, held] },
|
||||||
});
|
});
|
||||||
fireEvent.click(await screen.findByRole('button', { name: '发送' }));
|
fireEvent.click(await screen.findByRole('button', { name: '发送' }));
|
||||||
await waitFor(() => expect(attachmentApi.upload).toHaveBeenCalledOnce());
|
await waitFor(() => expect(attachmentApi.upload).toHaveBeenCalledTimes(2));
|
||||||
|
|
||||||
const remove = screen.getByRole('button', { name: '移除图片 held.png' });
|
const remove = screen.getByRole('button', { name: '移除图片 held.png' });
|
||||||
expect(remove).toBeDisabled();
|
expect(remove).toBeDisabled();
|
||||||
fireEvent.click(remove);
|
fireEvent.click(remove);
|
||||||
expect(screen.getByAltText('held.png')).toBeInTheDocument();
|
expect(screen.getByAltText('held.png')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('图片附件无效。')).not.toBeInTheDocument();
|
||||||
|
|
||||||
upload.resolve({ attachmentId: 'attachment-held', mime: 'image/png', byteLength: 5 });
|
upload.resolve({ attachmentId: 'attachment-held', mime: 'image/png', byteLength: 5 });
|
||||||
await waitFor(() => expect(conversationApi.submit).toHaveBeenCalledOnce());
|
expect(await screen.findByText('图片附件无效。')).toBeInTheDocument();
|
||||||
await waitFor(() => expect(screen.queryByAltText('held.png')).not.toBeInTheDocument());
|
expect(remove).toBeEnabled();
|
||||||
|
expect(conversationApi.submit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps next-message images out of a submission while its 202 response is pending', async () => {
|
it('keeps next-message images out of a submission while its 202 response is pending', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user