fix: stabilize ai design history switching

This commit is contained in:
2026-08-19 10:31:36 +08:00
parent abece81fdb
commit bf0b805d9f
14 changed files with 573 additions and 48 deletions

View File

@@ -114,6 +114,31 @@ function conversation(
};
}
function conversationMessages(revisions: number[]): DesignConversation['messages'] {
return revisions.flatMap((turnRevision) => ([
{
id: `turn-${turnRevision}:user`,
role: 'user' as const,
kind: 'user' as const,
text: `用户消息 ${turnRevision}`,
quickReplies: [],
generationQuote: null,
turnRevision,
createdAt: `2026-08-02T10:${String(turnRevision).padStart(2, '0')}:00Z`,
},
{
id: `turn-${turnRevision}:assistant`,
role: 'assistant' as const,
kind: 'reply' as const,
text: `助手消息 ${turnRevision}`,
quickReplies: [],
generationQuote: null,
turnRevision,
createdAt: `2026-08-02T10:${String(turnRevision).padStart(2, '0')}:00Z`,
},
]));
}
function workspace(workspaceId = 'workspace-one', viewRevision = 1): DesignWorkspace {
const { messages: _messages, ...conversationSummary } = conversation(workspaceId);
return {
@@ -400,6 +425,91 @@ describe('AI design task event store', () => {
expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce();
});
it('reuses an unresolved Conversation stream when switching A to B to A', async () => {
const pendingA = deferred<EventSource>();
const sourceB = new MockEventSource();
const project = workspace();
const { messages: _messages, ...secondSummary } = conversation(
'workspace-one',
0,
'conversation-two',
);
fetchImageWorkspaceProjectMock.mockResolvedValue({
...project,
conversationCount: 2,
conversations: [...project.conversations, secondSummary],
});
openImageWorkspaceTaskEventsMock
.mockReturnValueOnce(pendingA.promise)
.mockResolvedValueOnce(sourceB as unknown as EventSource);
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock)
.toHaveBeenCalledWith('workspace-one', 'conversation-one'));
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
await useImageWorkspaceStore.getState().selectConversation('conversation-one');
expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledTimes(2);
expect(openImageWorkspaceTaskEventsMock).toHaveBeenLastCalledWith(
'workspace-one',
'conversation-two',
);
const sourceA = new MockEventSource();
pendingA.resolve(sourceA as unknown as EventSource);
await vi.waitFor(() => expect(sourceA.onopen).not.toBeNull());
});
it('shows the Conversation before a delayed task refresh settles', async () => {
const source = new MockEventSource();
const delayedTasks = deferred<DesignGenerationTask[]>();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceTasksMock.mockReturnValueOnce(delayedTasks.promise);
await useImageWorkspaceStore.getState().load();
expect(useImageWorkspaceStore.getState()).toMatchObject({
status: 'ready',
conversation: { conversationId: 'conversation-one' },
tasks: [],
});
delayedTasks.resolve([task]);
await vi.waitFor(() => expect(useImageWorkspaceStore.getState().tasks).toEqual([task]));
});
it('prepends the next older page without losing the latest messages', async () => {
const source = new MockEventSource();
const latest = {
...conversation('workspace-one', 6),
messages: conversationMessages([2, 3, 4, 5, 6]),
messagePage: { hasOlder: true, nextBefore: 'cursor-before-turn-2' },
};
const older = {
...conversation('workspace-one', 6),
messages: conversationMessages([1]),
messagePage: { hasOlder: false, nextBefore: null },
};
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceConversationMock
.mockResolvedValueOnce(latest)
.mockResolvedValueOnce(older);
await useImageWorkspaceStore.getState().load();
await useImageWorkspaceStore.getState().loadOlderMessages();
expect(fetchImageWorkspaceConversationMock).toHaveBeenLastCalledWith(
'workspace-one',
'conversation-one',
'cursor-before-turn-2',
);
expect(useImageWorkspaceStore.getState().conversation).toMatchObject({
messagePage: { hasOlder: false, nextBefore: null },
});
expect(useImageWorkspaceStore.getState().conversation?.messages.map((message) => (
message.turnRevision
))).toEqual([1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]);
});
it('recovers a committed generation task when the Agent Run fails afterward', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);