Files
makelore/tests/unit/image-workspace-store.test.ts
brother7 03dae62cf3 feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。

实现:
- 增加会话选择、新建入口及本地数据迁移
- Conversation 独立消息、Brief、Quote 与流式状态
- 保留项目任务并修复 Task revision 与 ABA 异步竞态
- 保持 Enter 发送、Shift+Enter 换行及 IME 保护
2026-08-06 18:22:15 +08:00

646 lines
25 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import type {
DesignAssistantDeltaEvent,
DesignConversation,
DesignConversationSnapshotEvent,
DesignGenerationTask,
DesignGenerationTaskUpdatedEvent,
DesignWorkspace,
DesignWorkspaceBootstrap,
} from '../../shared/image-workspace';
const fetchImageWorkspaceMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceProjectMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceConversationMock = vi.hoisted(() => vi.fn());
const createImageWorkspaceConversationMock = vi.hoisted(() => vi.fn());
const fetchImageWorkspaceTasksMock = vi.hoisted(() => vi.fn());
const openImageWorkspaceTaskEventsMock = vi.hoisted(() => vi.fn());
const sendImageWorkspaceMessageMock = vi.hoisted(() => vi.fn());
const confirmImageWorkspaceGenerationMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/image-workspace', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/image-workspace')>();
return {
...actual,
fetchImageWorkspace: (...args: unknown[]) => fetchImageWorkspaceMock(...args),
fetchImageWorkspaceProject: (...args: unknown[]) => fetchImageWorkspaceProjectMock(...args),
fetchImageWorkspaceConversation: (...args: unknown[]) => (
fetchImageWorkspaceConversationMock(...args)
),
createImageWorkspaceConversation: (...args: unknown[]) => (
createImageWorkspaceConversationMock(...args)
),
fetchImageWorkspaceTasks: (...args: unknown[]) => fetchImageWorkspaceTasksMock(...args),
openImageWorkspaceTaskEvents: (...args: unknown[]) => openImageWorkspaceTaskEventsMock(...args),
sendImageWorkspaceMessage: (...args: unknown[]) => sendImageWorkspaceMessageMock(...args),
confirmImageWorkspaceGeneration: (...args: unknown[]) => (
confirmImageWorkspaceGenerationMock(...args)
),
};
});
type EventListener = (event: MessageEvent<string>) => void;
class MockEventSource {
onopen: ((event: Event) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
readonly close = vi.fn();
private readonly listeners = new Map<string, Set<EventListener>>();
addEventListener(type: string, listener: EventListener): void {
const listeners = this.listeners.get(type) ?? new Set<EventListener>();
listeners.add(listener);
this.listeners.set(type, listeners);
}
emit(type: string, payload: unknown): void {
const event = { data: JSON.stringify(payload) } as MessageEvent<string>;
for (const listener of this.listeners.get(type) ?? []) listener(event);
}
}
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
let resolve!: (value: T) => void;
const promise = new Promise<T>((accept) => {
resolve = accept;
});
return { promise, resolve };
}
const task: DesignGenerationTask = {
taskId: 'task-one',
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
medium: 'image',
status: 'queued',
briefVersion: 1,
briefSummary: '海洋公益海报',
quoteId: 'quote-one',
quotedDesignPoints: 1,
failureCode: null,
resultAssets: [],
createdAt: '2026-08-02T10:00:00Z',
updatedAt: '2026-08-02T10:00:00Z',
};
function conversation(
workspaceId = 'workspace-one',
turnRevision = 1,
conversationId = workspaceId.replace('workspace', 'conversation'),
): DesignConversation {
return {
conversationId,
workspaceId,
title: '主会话',
turnRevision,
phase: 'shaping',
brief: {
version: 1,
status: 'ready',
medium: 'image',
summary: '海洋公益海报',
ready: true,
missingDecision: null,
},
updatedAt: '2026-08-02T10:00:00Z',
messages: [],
createdAt: '2026-08-02T10:00:00Z',
};
}
function workspace(workspaceId = 'workspace-one', viewRevision = 1): DesignWorkspace {
const { messages: _messages, ...conversationSummary } = conversation(workspaceId);
return {
workspaceId,
title: workspaceId,
viewRevision,
conversationCount: 1,
phase: conversationSummary.phase,
updatedAt: '2026-08-02T10:00:00Z',
conversations: [conversationSummary],
};
}
function bootstrap(workspaceIds = ['workspace-one']): DesignWorkspaceBootstrap {
return {
capabilities: { conversation: true, generation: true, image: true, video: true },
workspaces: workspaceIds.map((workspaceId) => {
const { conversations: _conversations, ...summary } = workspace(workspaceId);
return summary;
}),
};
}
function taskEvent(
workspaceViewRevision: number,
status: DesignGenerationTask['status'],
): DesignGenerationTaskUpdatedEvent {
return {
id: `session-one:${workspaceViewRevision}`,
type: 'design.generation_task.updated',
workspaceId: 'workspace-one',
workspaceViewRevision,
generationTask: {
...task,
status,
updatedAt: `2026-08-02T10:0${workspaceViewRevision}:00Z`,
},
};
}
function taskSnapshotEvent(
workspaceViewRevision: number,
status: DesignGenerationTask['status'],
): DesignConversationSnapshotEvent {
return {
id: `session-one:${workspaceViewRevision}`,
type: 'design.conversation.snapshot',
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
workspaceViewRevision,
conversation: conversation('workspace-one', workspaceViewRevision),
generationTasks: [{
...task,
status,
updatedAt: `2026-08-02T10:0${workspaceViewRevision}:00Z`,
}],
};
}
describe('AI design task event store', () => {
beforeEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
useImageWorkspaceStore.getState().reset();
fetchImageWorkspaceMock.mockResolvedValue(bootstrap());
fetchImageWorkspaceProjectMock.mockResolvedValue(workspace());
fetchImageWorkspaceConversationMock.mockImplementation((workspaceId: string, conversationId: string) => (
Promise.resolve(conversation(workspaceId, 1, conversationId))
));
createImageWorkspaceConversationMock.mockResolvedValue(
conversation('workspace-one', 0, 'conversation-new'),
);
fetchImageWorkspaceTasksMock.mockResolvedValue([task]);
sendImageWorkspaceMessageMock.mockResolvedValue(conversation());
confirmImageWorkspaceGenerationMock.mockResolvedValue({
...conversation('workspace-one', 2),
turnRevision: 2,
});
});
afterEach(() => {
useImageWorkspaceStore.getState().reset();
vi.useRealTimers();
});
it('uses task event revisions instead of the project view revision as the task watermark', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceProjectMock.mockResolvedValue(workspace('workspace-one', 50));
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock)
.toHaveBeenCalledWith('workspace-one', 'conversation-one'));
source.onopen?.(new Event('open'));
source.emit('design.generation_task.updated', taskEvent(3, 'failed'));
expect(useImageWorkspaceStore.getState().tasks[0].status).toBe('failed');
source.emit('design.conversation.snapshot', taskSnapshotEvent(6, 'running'));
source.emit('design.generation_task.updated', taskEvent(5, 'failed'));
expect(useImageWorkspaceStore.getState()).toMatchObject({
taskStreamState: 'connected',
workspace: { viewRevision: 50 },
tasks: [{ taskId: 'task-one', status: 'running' }],
});
});
it('assembles assistant deltas once and replaces them with the canonical snapshot', async () => {
const source = new MockEventSource();
const response = deferred<DesignConversation>();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());
const sending = useImageWorkspaceStore.getState().sendMessage('把主视觉改成鲸鱼');
const pending = useImageWorkspaceStore.getState().pendingTurn;
expect(pending).toMatchObject({
userText: '把主视觉改成鲸鱼',
assistantText: '',
turnRevision: 2,
});
const firstDelta = {
id: 'session-one:2',
type: 'design.assistant.delta',
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
clientTurnId: pending!.clientTurnId,
turnRevision: 2,
chunkIndex: 0,
delta: '可以,先强化',
} satisfies DesignAssistantDeltaEvent;
source.emit('design.assistant.delta', firstDelta);
source.emit('design.assistant.delta', firstDelta);
source.emit('design.assistant.delta', {
...firstDelta,
id: 'session-one:3',
chunkIndex: 1,
delta: '鲸鱼的轮廓。',
} satisfies DesignAssistantDeltaEvent);
expect(useImageWorkspaceStore.getState().pendingTurn?.assistantText)
.toBe('可以,先强化鲸鱼的轮廓。');
const canonical = {
...conversation('workspace-one', 2),
turnRevision: 2,
messages: [{
id: 'workspace-one:2:assistant:0',
role: 'assistant' as const,
kind: 'reply' as const,
text: '可以,先强化鲸鱼的轮廓。',
quickReplies: [],
generationQuote: null,
turnRevision: 2,
createdAt: '2026-08-02T10:01:00Z',
}],
};
source.emit('design.conversation.snapshot', {
...taskSnapshotEvent(2, 'queued'),
id: 'session-one:4',
conversation: canonical,
} satisfies DesignConversationSnapshotEvent);
expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull();
expect(useImageWorkspaceStore.getState().conversation?.messages[0].text)
.toBe('可以,先强化鲸鱼的轮廓。');
response.resolve(canonical);
await sending;
});
it('reconciles the confirmed Quote until its generation task is visible', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceTasksMock
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([task]);
await useImageWorkspaceStore.getState().load();
await useImageWorkspaceStore.getState().confirmGeneration('quote-one');
expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledWith(
'workspace-one',
'conversation-one',
1,
'quote-one',
expect.stringMatching(/^turn-/),
);
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(3);
expect(useImageWorkspaceStore.getState().tasks).toEqual([task]);
});
it('does not silently complete confirmation when the quoted task stays missing', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceTasksMock.mockResolvedValue([]);
await useImageWorkspaceStore.getState().load();
vi.useFakeTimers();
const outcome = useImageWorkspaceStore.getState().confirmGeneration('quote-one').then(
() => null,
(error: unknown) => error,
);
await vi.runAllTimersAsync();
await expect(outcome).resolves.toMatchObject({ code: 'generation_task_not_visible' });
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(6);
expect(useImageWorkspaceStore.getState().error)
.toContain('任务列表同步超时');
});
it('closes the previous stream when switching workspaces and on reset', async () => {
const first = new MockEventSource();
const second = new MockEventSource();
openImageWorkspaceTaskEventsMock
.mockResolvedValueOnce(first as unknown as EventSource)
.mockResolvedValueOnce(second as unknown as EventSource);
fetchImageWorkspaceMock.mockResolvedValue(bootstrap(['workspace-one', 'workspace-two']));
fetchImageWorkspaceProjectMock.mockImplementation((workspaceId: string) => (
Promise.resolve(workspace(workspaceId))
));
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledTimes(1));
await useImageWorkspaceStore.getState().selectProject('workspace-two');
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledTimes(2));
expect(first.close).toHaveBeenCalledOnce();
useImageWorkspaceStore.getState().reset();
expect(second.close).toHaveBeenCalledOnce();
expect(useImageWorkspaceStore.getState().taskStreamState).toBe('idle');
});
it('does not switch back when an Agent turn finishes after selecting another Workspace', async () => {
const first = new MockEventSource();
const second = new MockEventSource();
const response = deferred<DesignConversation>();
openImageWorkspaceTaskEventsMock
.mockResolvedValueOnce(first as unknown as EventSource)
.mockResolvedValueOnce(second as unknown as EventSource);
fetchImageWorkspaceMock.mockResolvedValue(bootstrap(['workspace-one', 'workspace-two']));
fetchImageWorkspaceProjectMock.mockImplementation((workspaceId: string) => (
Promise.resolve(workspace(workspaceId))
));
sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
await useImageWorkspaceStore.getState().load();
const sending = useImageWorkspaceStore.getState().sendMessage('继续优化海报');
await useImageWorkspaceStore.getState().selectProject('workspace-two');
response.resolve({
...conversation('workspace-one', 2),
turnRevision: 2,
});
await sending;
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeWorkspaceId: 'workspace-two',
workspace: { workspaceId: 'workspace-two' },
pendingTurn: null,
});
});
it('does not let a slow previous Workspace selection overwrite the latest one', async () => {
const rootSource = new MockEventSource();
const latestSource = new MockEventSource();
const slowWorkspace = deferred<DesignWorkspace>();
openImageWorkspaceTaskEventsMock
.mockResolvedValueOnce(rootSource as unknown as EventSource)
.mockResolvedValueOnce(latestSource as unknown as EventSource);
fetchImageWorkspaceMock.mockResolvedValue(bootstrap([
'workspace-root',
'workspace-one',
'workspace-two',
]));
fetchImageWorkspaceProjectMock.mockImplementation((workspaceId: string) => (
workspaceId === 'workspace-one'
? slowWorkspace.promise
: Promise.resolve(workspace(workspaceId))
));
fetchImageWorkspaceTasksMock.mockResolvedValue([]);
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock)
.toHaveBeenCalledWith('workspace-root', 'conversation-root'));
const staleSelection = useImageWorkspaceStore.getState().selectProject('workspace-one');
await vi.waitFor(() => expect(fetchImageWorkspaceProjectMock)
.toHaveBeenCalledWith('workspace-one'));
await useImageWorkspaceStore.getState().selectProject('workspace-two');
slowWorkspace.resolve(workspace('workspace-one'));
await staleSelection;
expect(useImageWorkspaceStore.getState().activeWorkspaceId).toBe('workspace-two');
expect(useImageWorkspaceStore.getState().workspace?.workspaceId).toBe('workspace-two');
expect(openImageWorkspaceTaskEventsMock)
.not.toHaveBeenCalledWith('workspace-one', 'conversation-one');
});
it('uses updatedAt to keep a newer degraded REST task over an older replay', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());
fetchImageWorkspaceProjectMock.mockResolvedValue(workspace('workspace-one', 8));
fetchImageWorkspaceTasksMock.mockResolvedValue([{
...task,
status: 'succeeded',
updatedAt: '2026-08-02T11:00:00Z',
}]);
await useImageWorkspaceStore.getState().refreshWorkspace();
await useImageWorkspaceStore.getState().refreshTasks();
source.emit('design.generation_task.updated', taskEvent(7, 'failed'));
expect(useImageWorkspaceStore.getState().tasks[0].status).toBe('succeeded');
});
it('polls slowly only while the event stream is degraded', async () => {
vi.useFakeTimers();
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
await useImageWorkspaceStore.getState().load();
await vi.waitFor(() => expect(source.onerror).not.toBeNull());
source.onerror?.(new Event('error'));
await vi.advanceTimersByTimeAsync(14_999);
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2);
source.onopen?.(new Event('open'));
await vi.advanceTimersByTimeAsync(15_000);
expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2);
});
it('switches the conversation stream without clearing project generation tasks', async () => {
const first = new MockEventSource();
const second = new MockEventSource();
openImageWorkspaceTaskEventsMock
.mockResolvedValueOnce(first as unknown as EventSource)
.mockResolvedValueOnce(second as unknown as EventSource);
const project = workspace();
const { messages: _messages, ...secondSummary } = conversation(
'workspace-one',
0,
'conversation-two',
);
fetchImageWorkspaceProjectMock.mockResolvedValue({
...project,
conversationCount: 2,
conversations: [...project.conversations, secondSummary],
});
await useImageWorkspaceStore.getState().load();
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
expect(first.close).toHaveBeenCalledOnce();
expect(openImageWorkspaceTaskEventsMock)
.toHaveBeenLastCalledWith('workspace-one', 'conversation-two');
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeWorkspaceId: 'workspace-one',
activeConversationId: 'conversation-two',
conversation: { conversationId: 'conversation-two' },
tasks: [{ taskId: 'task-one' }],
});
});
it('accepts a lower-revision fresh task event after switching conversations', async () => {
const first = new MockEventSource();
const second = new MockEventSource();
openImageWorkspaceTaskEventsMock
.mockResolvedValueOnce(first as unknown as EventSource)
.mockResolvedValueOnce(second as unknown as EventSource);
const project = workspace('workspace-one', 50);
const { messages: _messages, ...secondSummary } = conversation(
'workspace-one',
0,
'conversation-two',
);
fetchImageWorkspaceProjectMock.mockResolvedValue({
...project,
conversationCount: 2,
conversations: [...project.conversations, secondSummary],
});
await useImageWorkspaceStore.getState().load();
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
second.emit('design.generation_task.updated', taskEvent(3, 'running'));
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeConversationId: 'conversation-two',
workspace: { viewRevision: 50 },
tasks: [{ taskId: 'task-one', status: 'running' }],
});
});
it('does not let an older ABA conversation response replace the latest turn', async () => {
const source = 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.mockResolvedValue(source as unknown as EventSource);
await useImageWorkspaceStore.getState().load();
const staleA = deferred<DesignConversation>();
const freshA = deferred<DesignConversation>();
fetchImageWorkspaceConversationMock
.mockReset()
.mockReturnValueOnce(staleA.promise)
.mockResolvedValueOnce(conversation('workspace-one', 2, 'conversation-two'))
.mockReturnValueOnce(freshA.promise);
const firstA = useImageWorkspaceStore.getState().selectConversation('conversation-one');
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
const secondA = useImageWorkspaceStore.getState().selectConversation('conversation-one');
freshA.resolve({
...conversation('workspace-one', 5, 'conversation-one'),
updatedAt: '2026-08-02T10:05:00Z',
});
await secondA;
staleA.resolve({
...conversation('workspace-one', 4, 'conversation-one'),
updatedAt: '2026-08-02T10:04:00Z',
});
await firstA;
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeConversationId: 'conversation-one',
conversation: { conversationId: 'conversation-one', turnRevision: 5 },
});
});
it('does not surface a late ABA conversation error on the latest selection', async () => {
const source = 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.mockResolvedValue(source as unknown as EventSource);
await useImageWorkspaceStore.getState().load();
let rejectStale!: (error: unknown) => void;
const staleA = new Promise<DesignConversation>((_resolve, reject) => {
rejectStale = reject;
});
fetchImageWorkspaceConversationMock
.mockReset()
.mockReturnValueOnce(staleA)
.mockResolvedValueOnce(conversation('workspace-one', 2, 'conversation-two'))
.mockResolvedValueOnce(conversation('workspace-one', 5, 'conversation-one'));
const firstA = useImageWorkspaceStore.getState().selectConversation('conversation-one');
await useImageWorkspaceStore.getState().selectConversation('conversation-two');
await useImageWorkspaceStore.getState().selectConversation('conversation-one');
rejectStale(new Error('stale request failed'));
await firstA;
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeConversationId: 'conversation-one',
conversation: { turnRevision: 5 },
error: null,
});
});
it('does not let stale ABA workspace tasks replace the latest project load', async () => {
const source = new MockEventSource();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
fetchImageWorkspaceMock.mockResolvedValue(bootstrap(['workspace-one', 'workspace-two']));
await useImageWorkspaceStore.getState().load();
const staleTasks = deferred<DesignGenerationTask[]>();
fetchImageWorkspaceProjectMock
.mockReset()
.mockResolvedValueOnce(workspace('workspace-one', 4))
.mockResolvedValueOnce(workspace('workspace-two', 1))
.mockResolvedValueOnce(workspace('workspace-one', 5));
fetchImageWorkspaceTasksMock
.mockReset()
.mockReturnValueOnce(staleTasks.promise)
.mockResolvedValueOnce([{ ...task, taskId: 'task-two', workspaceId: 'workspace-two' }])
.mockResolvedValueOnce([{
...task,
status: 'succeeded',
updatedAt: '2026-08-02T11:00:00Z',
}]);
const firstA = useImageWorkspaceStore.getState().selectProject('workspace-one');
await vi.waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledOnce());
await useImageWorkspaceStore.getState().selectProject('workspace-two');
await useImageWorkspaceStore.getState().selectProject('workspace-one');
staleTasks.resolve([{
...task,
status: 'failed',
updatedAt: '2026-08-02T10:04:00Z',
}]);
await firstA;
expect(useImageWorkspaceStore.getState()).toMatchObject({
activeWorkspaceId: 'workspace-one',
workspace: { workspaceId: 'workspace-one', viewRevision: 5 },
tasks: [{ taskId: 'task-one', status: 'succeeded' }],
});
});
it('prevents duplicate conversations while one create request is in flight', async () => {
const source = new MockEventSource();
const pending = deferred<DesignConversation>();
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
createImageWorkspaceConversationMock.mockReturnValueOnce(pending.promise);
await useImageWorkspaceStore.getState().load();
const firstCreate = useImageWorkspaceStore.getState().createConversation();
await expect(useImageWorkspaceStore.getState().createConversation())
.rejects.toThrow('正在创建新会话');
expect(createImageWorkspaceConversationMock).toHaveBeenCalledOnce();
pending.resolve(conversation('workspace-one', 0, 'conversation-new'));
await firstCreate;
expect(useImageWorkspaceStore.getState().creatingConversation).toBe(false);
});
});