fix: stabilize ai design history switching
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user