feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。 实现: - 增加会话选择、新建入口及本地数据迁移 - Conversation 独立消息、Brief、Quote 与流式状态 - 保留项目任务并修复 Task revision 与 ABA 异步竞态 - 保持 Enter 发送、Shift+Enter 换行及 IME 保护
This commit is contained in:
@@ -2,8 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import type {
|
||||
DesignAssistantDeltaEvent,
|
||||
DesignConversation,
|
||||
DesignConversationSnapshotEvent,
|
||||
DesignGenerationTask,
|
||||
DesignGenerationTasksSnapshotEvent,
|
||||
DesignGenerationTaskUpdatedEvent,
|
||||
DesignWorkspace,
|
||||
DesignWorkspaceBootstrap,
|
||||
@@ -11,6 +12,8 @@ import type {
|
||||
|
||||
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());
|
||||
@@ -22,6 +25,12 @@ vi.mock('@/lib/image-workspace', async (importOriginal) => {
|
||||
...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),
|
||||
@@ -62,6 +71,7 @@ function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
|
||||
const task: DesignGenerationTask = {
|
||||
taskId: 'task-one',
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
medium: 'image',
|
||||
status: 'queued',
|
||||
briefVersion: 1,
|
||||
@@ -74,12 +84,16 @@ const task: DesignGenerationTask = {
|
||||
updatedAt: '2026-08-02T10:00:00Z',
|
||||
};
|
||||
|
||||
function workspace(workspaceId = 'workspace-one', viewRevision = 1): DesignWorkspace {
|
||||
function conversation(
|
||||
workspaceId = 'workspace-one',
|
||||
turnRevision = 1,
|
||||
conversationId = workspaceId.replace('workspace', 'conversation'),
|
||||
): DesignConversation {
|
||||
return {
|
||||
conversationId,
|
||||
workspaceId,
|
||||
title: workspaceId,
|
||||
turnRevision: 1,
|
||||
viewRevision,
|
||||
title: '主会话',
|
||||
turnRevision,
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 1,
|
||||
@@ -91,6 +105,20 @@ function workspace(workspaceId = 'workspace-one', viewRevision = 1): DesignWorks
|
||||
},
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,7 +126,7 @@ function bootstrap(workspaceIds = ['workspace-one']): DesignWorkspaceBootstrap {
|
||||
return {
|
||||
capabilities: { conversation: true, generation: true, image: true, video: true },
|
||||
workspaces: workspaceIds.map((workspaceId) => {
|
||||
const { messages: _messages, ...summary } = workspace(workspaceId);
|
||||
const { conversations: _conversations, ...summary } = workspace(workspaceId);
|
||||
return summary;
|
||||
}),
|
||||
};
|
||||
@@ -124,13 +152,14 @@ function taskEvent(
|
||||
function taskSnapshotEvent(
|
||||
workspaceViewRevision: number,
|
||||
status: DesignGenerationTask['status'],
|
||||
): DesignGenerationTasksSnapshotEvent {
|
||||
): DesignConversationSnapshotEvent {
|
||||
return {
|
||||
id: `session-one:${workspaceViewRevision}`,
|
||||
type: 'design.generation_tasks.snapshot',
|
||||
type: 'design.conversation.snapshot',
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
workspaceViewRevision,
|
||||
workspace: workspace('workspace-one', workspaceViewRevision),
|
||||
conversation: conversation('workspace-one', workspaceViewRevision),
|
||||
generationTasks: [{
|
||||
...task,
|
||||
status,
|
||||
@@ -146,10 +175,16 @@ describe('AI design task event store', () => {
|
||||
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(workspace());
|
||||
sendImageWorkspaceMessageMock.mockResolvedValue(conversation());
|
||||
confirmImageWorkspaceGenerationMock.mockResolvedValue({
|
||||
...workspace('workspace-one', 2),
|
||||
...conversation('workspace-one', 2),
|
||||
turnRevision: 2,
|
||||
});
|
||||
});
|
||||
@@ -159,30 +194,30 @@ describe('AI design task event store', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps a revision-5 snapshot over revision 3, then upserts a newer task by task id', async () => {
|
||||
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', 5));
|
||||
fetchImageWorkspaceProjectMock.mockResolvedValue(workspace('workspace-one', 50));
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock)
|
||||
.toHaveBeenCalledWith('workspace-one'));
|
||||
.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('queued');
|
||||
source.emit('design.generation_tasks.snapshot', taskSnapshotEvent(6, 'running'));
|
||||
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: 6 },
|
||||
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<DesignWorkspace>();
|
||||
const response = deferred<DesignConversation>();
|
||||
openImageWorkspaceTaskEventsMock.mockResolvedValue(source as unknown as EventSource);
|
||||
sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
|
||||
|
||||
@@ -200,6 +235,7 @@ describe('AI design task event store', () => {
|
||||
id: 'session-one:2',
|
||||
type: 'design.assistant.delta',
|
||||
workspaceId: 'workspace-one',
|
||||
conversationId: 'conversation-one',
|
||||
clientTurnId: pending!.clientTurnId,
|
||||
turnRevision: 2,
|
||||
chunkIndex: 0,
|
||||
@@ -218,7 +254,7 @@ describe('AI design task event store', () => {
|
||||
.toBe('可以,先强化鲸鱼的轮廓。');
|
||||
|
||||
const canonical = {
|
||||
...workspace('workspace-one', 2),
|
||||
...conversation('workspace-one', 2),
|
||||
turnRevision: 2,
|
||||
messages: [{
|
||||
id: 'workspace-one:2:assistant:0',
|
||||
@@ -231,14 +267,14 @@ describe('AI design task event store', () => {
|
||||
createdAt: '2026-08-02T10:01:00Z',
|
||||
}],
|
||||
};
|
||||
source.emit('design.generation_tasks.snapshot', {
|
||||
source.emit('design.conversation.snapshot', {
|
||||
...taskSnapshotEvent(2, 'queued'),
|
||||
id: 'session-one:4',
|
||||
workspace: canonical,
|
||||
} satisfies DesignGenerationTasksSnapshotEvent);
|
||||
conversation: canonical,
|
||||
} satisfies DesignConversationSnapshotEvent);
|
||||
|
||||
expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull();
|
||||
expect(useImageWorkspaceStore.getState().workspace?.messages[0].text)
|
||||
expect(useImageWorkspaceStore.getState().conversation?.messages[0].text)
|
||||
.toBe('可以,先强化鲸鱼的轮廓。');
|
||||
response.resolve(canonical);
|
||||
await sending;
|
||||
@@ -257,6 +293,7 @@ describe('AI design task event store', () => {
|
||||
|
||||
expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledWith(
|
||||
'workspace-one',
|
||||
'conversation-one',
|
||||
1,
|
||||
'quote-one',
|
||||
expect.stringMatching(/^turn-/),
|
||||
@@ -309,7 +346,7 @@ describe('AI design task event store', () => {
|
||||
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<DesignWorkspace>();
|
||||
const response = deferred<DesignConversation>();
|
||||
openImageWorkspaceTaskEventsMock
|
||||
.mockResolvedValueOnce(first as unknown as EventSource)
|
||||
.mockResolvedValueOnce(second as unknown as EventSource);
|
||||
@@ -323,7 +360,7 @@ describe('AI design task event store', () => {
|
||||
const sending = useImageWorkspaceStore.getState().sendMessage('继续优化海报');
|
||||
await useImageWorkspaceStore.getState().selectProject('workspace-two');
|
||||
response.resolve({
|
||||
...workspace('workspace-one', 2),
|
||||
...conversation('workspace-one', 2),
|
||||
turnRevision: 2,
|
||||
});
|
||||
await sending;
|
||||
@@ -356,7 +393,7 @@ describe('AI design task event store', () => {
|
||||
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
await vi.waitFor(() => expect(openImageWorkspaceTaskEventsMock)
|
||||
.toHaveBeenCalledWith('workspace-root'));
|
||||
.toHaveBeenCalledWith('workspace-root', 'conversation-root'));
|
||||
const staleSelection = useImageWorkspaceStore.getState().selectProject('workspace-one');
|
||||
await vi.waitFor(() => expect(fetchImageWorkspaceProjectMock)
|
||||
.toHaveBeenCalledWith('workspace-one'));
|
||||
@@ -366,10 +403,11 @@ describe('AI design task event store', () => {
|
||||
|
||||
expect(useImageWorkspaceStore.getState().activeWorkspaceId).toBe('workspace-two');
|
||||
expect(useImageWorkspaceStore.getState().workspace?.workspaceId).toBe('workspace-two');
|
||||
expect(openImageWorkspaceTaskEventsMock).not.toHaveBeenCalledWith('workspace-one');
|
||||
expect(openImageWorkspaceTaskEventsMock)
|
||||
.not.toHaveBeenCalledWith('workspace-one', 'conversation-one');
|
||||
});
|
||||
|
||||
it('advances each task watermark after a degraded REST snapshot', async () => {
|
||||
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);
|
||||
|
||||
@@ -379,6 +417,7 @@ describe('AI design task event store', () => {
|
||||
fetchImageWorkspaceTasksMock.mockResolvedValue([{
|
||||
...task,
|
||||
status: 'succeeded',
|
||||
updatedAt: '2026-08-02T11:00:00Z',
|
||||
}]);
|
||||
await useImageWorkspaceStore.getState().refreshWorkspace();
|
||||
await useImageWorkspaceStore.getState().refreshTasks();
|
||||
@@ -404,4 +443,203 @@ describe('AI design task event store', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user