fix: 修复 AI 设计确认生成任务链路
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||
import type {
|
||||
DesignAssistantDeltaEvent,
|
||||
DesignConversation,
|
||||
@@ -302,6 +303,139 @@ describe('AI design task event store', () => {
|
||||
expect(useImageWorkspaceStore.getState().tasks).toEqual([task]);
|
||||
});
|
||||
|
||||
it('recovers a committed generation task when the Agent Run fails afterward', async () => {
|
||||
const source = new MockEventSource();
|
||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||
fetchImageWorkspaceTasksMock
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([task]);
|
||||
fetchImageWorkspaceConversationMock
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 1, 'conversation-one'))
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 2, 'conversation-one'));
|
||||
confirmImageWorkspaceGenerationMock.mockRejectedValueOnce(
|
||||
new ImageWorkspaceApiError(
|
||||
503,
|
||||
'agent_runtime_unavailable',
|
||||
'AI 设计服务暂时不可用,请稍后重试',
|
||||
),
|
||||
);
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
await expect(useImageWorkspaceStore.getState().confirmGeneration('quote-one'))
|
||||
.resolves.toMatchObject({ turnRevision: 2 });
|
||||
|
||||
expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledOnce();
|
||||
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2);
|
||||
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||
error: null,
|
||||
pendingTurn: null,
|
||||
conversation: { turnRevision: 2 },
|
||||
tasks: [{ taskId: 'task-one', quoteId: 'quote-one' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps reconciling Workspace tasks after switching Conversations during confirmation', async () => {
|
||||
const first = new MockEventSource();
|
||||
const second = new MockEventSource();
|
||||
const confirmation = deferred<DesignConversation>();
|
||||
const project = workspace();
|
||||
const { messages: _messages, ...secondSummary } = conversation(
|
||||
'workspace-one',
|
||||
1,
|
||||
'conversation-two',
|
||||
);
|
||||
openImageWorkspaceTaskEventsMock
|
||||
.mockResolvedValueOnce(first as unknown as EventSource)
|
||||
.mockResolvedValueOnce(second as unknown as EventSource);
|
||||
fetchImageWorkspaceProjectMock.mockResolvedValue({
|
||||
...project,
|
||||
conversationCount: 2,
|
||||
conversations: [...project.conversations, secondSummary],
|
||||
});
|
||||
fetchImageWorkspaceTasksMock
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([task]);
|
||||
fetchImageWorkspaceConversationMock
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 1, 'conversation-one'))
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 1, 'conversation-two'));
|
||||
confirmImageWorkspaceGenerationMock.mockReturnValueOnce(confirmation.promise);
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
const confirming = useImageWorkspaceStore.getState().confirmGeneration('quote-one');
|
||||
await vi.waitFor(() => expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledOnce());
|
||||
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
|
||||
confirmation.resolve(conversation('workspace-one', 2, 'conversation-one'));
|
||||
await confirming;
|
||||
|
||||
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2);
|
||||
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||
activeWorkspaceId: 'workspace-one',
|
||||
activeConversationId: 'conversation-two',
|
||||
conversation: { conversationId: 'conversation-two', turnRevision: 1 },
|
||||
pendingTurn: null,
|
||||
error: null,
|
||||
tasks: [{ taskId: 'task-one', quoteId: 'quote-one' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not let a failed ABA confirmation overwrite a newer turn in the same Workspace', async () => {
|
||||
const source = new MockEventSource();
|
||||
const latestResponse = deferred<DesignConversation>();
|
||||
const runtimeError = new ImageWorkspaceApiError(
|
||||
503,
|
||||
'agent_runtime_unavailable',
|
||||
'AI 设计服务暂时不可用,请稍后重试',
|
||||
);
|
||||
let rejectConfirmation!: (error: unknown) => void;
|
||||
const staleConfirmationResponse = new Promise<DesignConversation>((_resolve, reject) => {
|
||||
rejectConfirmation = reject;
|
||||
});
|
||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||
fetchImageWorkspaceMock.mockResolvedValue(bootstrap(['workspace-one', 'workspace-two']));
|
||||
fetchImageWorkspaceProjectMock.mockImplementation((workspaceId: string) => (
|
||||
Promise.resolve(workspace(workspaceId))
|
||||
));
|
||||
fetchImageWorkspaceTasksMock
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([task]);
|
||||
fetchImageWorkspaceConversationMock
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 1, 'conversation-one'))
|
||||
.mockResolvedValueOnce(conversation('workspace-two', 1, 'conversation-two'))
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 1, 'conversation-one'))
|
||||
.mockResolvedValueOnce(conversation('workspace-one', 2, 'conversation-one'));
|
||||
confirmImageWorkspaceGenerationMock.mockReturnValueOnce(staleConfirmationResponse);
|
||||
sendImageWorkspaceMessageMock.mockReturnValueOnce(latestResponse.promise);
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
const staleOutcome = useImageWorkspaceStore.getState().confirmGeneration('quote-one').then(
|
||||
(value) => ({ value, error: null }),
|
||||
(error: unknown) => ({ value: null, error }),
|
||||
);
|
||||
await vi.waitFor(() => expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledOnce());
|
||||
await useImageWorkspaceStore.getState().selectProject('workspace-two');
|
||||
await useImageWorkspaceStore.getState().selectProject('workspace-one');
|
||||
const latestTurn = useImageWorkspaceStore.getState().sendMessage('继续调整最新方案');
|
||||
|
||||
rejectConfirmation(runtimeError);
|
||||
|
||||
await expect(staleOutcome).resolves.toMatchObject({
|
||||
value: { conversationId: 'conversation-one', turnRevision: 1 },
|
||||
error: null,
|
||||
});
|
||||
expect(fetchImageWorkspaceConversationMock).toHaveBeenCalledTimes(3);
|
||||
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||
activeWorkspaceId: 'workspace-one',
|
||||
activeConversationId: 'conversation-one',
|
||||
conversation: { turnRevision: 1 },
|
||||
pendingTurn: { userText: '继续调整最新方案' },
|
||||
error: null,
|
||||
});
|
||||
|
||||
latestResponse.resolve(conversation('workspace-one', 2, 'conversation-one'));
|
||||
await latestTurn;
|
||||
});
|
||||
|
||||
it('does not silently complete confirmation when the quoted task stays missing', async () => {
|
||||
const source = new MockEventSource();
|
||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||
|
||||
Reference in New Issue
Block a user