修复: 打通设计回复流式投递与结构化确认

问题:Run 完成会抢在 SSE 流式事件之前清空 pending,导致回复整块出现;无 Quote 的确认按钮又会降级为普通聊天,因此无法创建任务。

实现:为 Main 事件队列增加有界投递屏障,严格识别生成确认短语并只调用结构化 Quote Action,接入 generation capability 提示,同时补齐竞态、误触和任务对账测试。
This commit is contained in:
2026-08-03 17:19:53 +08:00
parent fab0e4034c
commit e697853d66
4 changed files with 408 additions and 51 deletions

View File

@@ -348,6 +348,14 @@ describe('ImageCanvas Workspace-first design experience', () => {
chunkIndex: 0,
delta: '可以先增加',
} satisfies DesignAssistantDeltaEvent);
});
expect(screen.getByTestId('image-workspace-conversation'))
.toHaveTextContent('可以先增加');
expect(screen.getByTestId('image-workspace-conversation'))
.not.toHaveTextContent('留白和水流节奏');
act(() => {
taskEventSource.emit('design.assistant.delta', {
id: 'session-one:3',
type: 'design.assistant.delta',
@@ -369,34 +377,34 @@ describe('ImageCanvas Workspace-first design experience', () => {
await waitFor(() => expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull());
});
it('refreshes tasks after an Agent turn creates a generation task', async () => {
const queuedTask = {
...taskFixture,
taskId: 'task-two',
status: 'queued' as const,
resultAssets: [],
it('never downgrades confirmation to normal chat when no active Quote exists', async () => {
const workspaceWithoutQuote = workspaceFixture();
workspaceWithoutQuote.messages[1] = {
...workspaceWithoutQuote.messages[1],
generationQuote: null,
};
fetchImageWorkspaceTasksMock
.mockResolvedValueOnce([])
.mockResolvedValueOnce([queuedTask]);
fetchImageWorkspaceMock.mockResolvedValueOnce({
...bootstrapFixture,
capabilities: {
...bootstrapFixture.capabilities,
generation: false,
image: false,
video: false,
},
});
fetchImageWorkspaceProjectMock.mockResolvedValueOnce(workspaceWithoutQuote);
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
await screen.findByTestId('design-quote-quote-one');
const confirmReply = await screen.findByRole('button', { name: '确认生成' });
const confirmationReply = workspaceFixture().messages[1].quickReplies[0];
fireEvent.change(screen.getByLabelText('设计需求'), {
target: { value: confirmationReply },
});
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
fireEvent.click(confirmReply);
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
'workspace-cloud',
1,
confirmationReply,
expect.stringMatching(/^turn-/),
));
await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
expect(await screen.findByTestId('design-task-task-two')).toBeInTheDocument();
expect(screen.getByRole('alert')).toHaveTextContent('当前没有可确认的生成报价');
expect(screen.getByLabelText('设计需求')).toHaveValue('');
expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
expect(screen.getByTestId('image-workspace-composer'))
.toHaveTextContent('当前环境仅支持设计沟通');
});
it('creates a generation task only through explicit Quote confirmation', async () => {
@@ -425,6 +433,55 @@ describe('ImageCanvas Workspace-first design experience', () => {
expect(await screen.findByTestId('design-task-task-two')).toBeInTheDocument();
});
it('converts an exact composer confirmation into the structured Quote action', async () => {
fetchImageWorkspaceTasksMock
.mockResolvedValueOnce([taskFixture])
.mockResolvedValueOnce([{
...taskFixture,
taskId: 'task-confirmed-from-composer',
quoteId: 'quote-one',
status: 'queued',
resultAssets: [],
}]);
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
await screen.findByTestId('design-quote-quote-one');
fireEvent.change(screen.getByLabelText('设计需求'), {
target: { value: '确认生成' },
});
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
await waitFor(() => expect(confirmImageWorkspaceGenerationMock)
.toHaveBeenCalledWith(
'workspace-cloud',
1,
'quote-one',
expect.stringMatching(/^turn-/),
));
expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
expect(await screen.findByTestId('design-task-task-confirmed-from-composer'))
.toBeInTheDocument();
});
it('keeps longer design questions containing confirmation words as normal chat', async () => {
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
await screen.findByTestId('design-quote-quote-one');
const question = '请确认这个生成图的构图是不是还需要调整';
fireEvent.change(screen.getByLabelText('设计需求'), {
target: { value: question },
});
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
'workspace-cloud',
1,
question,
expect.stringMatching(/^turn-/),
));
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
});
it('renders a new task pushed by the design event stream without repeated polling', async () => {
const queuedTask = {
...taskFixture,