import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createImageWorkspaceOperationId, createImageWorkspaceProject, deleteImageWorkspaceProject, fetchImageWorkspace, fetchImageWorkspaceProject, ImageWorkspaceApiError, openImageWorkspaceEvents, renameImageWorkspaceProject, submitImageWorkspaceCommand, } from '@/lib/image-workspace'; import { useImageWorkspaceStore } from '@/stores/image-workspace'; import { designBootstrapFixture, designFormFixture, designQuoteFixture, designSummaryFixture, designWorkspaceFixture, } from '../fixtures/design-workspace-v2'; vi.mock('@/lib/image-workspace', async (importOriginal) => { const original = await importOriginal(); return { ...original, createImageWorkspaceOperationId: vi.fn(), createImageWorkspaceProject: vi.fn(), deleteImageWorkspaceProject: vi.fn(), fetchImageWorkspace: vi.fn(), fetchImageWorkspaceProject: vi.fn(), openImageWorkspaceEvents: vi.fn(), renameImageWorkspaceProject: vi.fn(), submitImageWorkspaceCommand: vi.fn(), }; }); vi.mock('@/lib/host-api', async (importOriginal) => { const original = await importOriginal(); return { ...original, setDesktopBackgroundLease: vi.fn() }; }); vi.mock('@/stores/auth', () => ({ useAuthStore: { getState: () => ({ isAuthenticated: () => true }), }, })); const operationIdMock = vi.mocked(createImageWorkspaceOperationId); const createProjectMock = vi.mocked(createImageWorkspaceProject); const deleteProjectMock = vi.mocked(deleteImageWorkspaceProject); const fetchBootstrapMock = vi.mocked(fetchImageWorkspace); const fetchProjectMock = vi.mocked(fetchImageWorkspaceProject); const openEventsMock = vi.mocked(openImageWorkspaceEvents); const renameProjectMock = vi.mocked(renameImageWorkspaceProject); const submitCommandMock = vi.mocked(submitImageWorkspaceCommand); class FakeEventSource { onopen: ((event: Event) => void) | null = null; onerror: ((event: Event) => void) | null = null; readonly listeners = new Map void>>(); close = vi.fn(); addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { const callback = typeof listener === 'function' ? listener : (event: Event) => listener.handleEvent(event); const listeners = this.listeners.get(type) ?? new Set<(event: Event) => void>(); listeners.add(callback); this.listeners.set(type, listeners); } emit(type: string, payload: unknown): void { const event = new MessageEvent(type, { data: JSON.stringify(payload) }); this.listeners.get(type)?.forEach((listener) => listener(event)); } } function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, reject, resolve }; } async function loadedStore(eventSource = new FakeEventSource()) { openEventsMock.mockResolvedValue(eventSource as unknown as EventSource); await useImageWorkspaceStore.getState().load(); await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalled()); return eventSource; } describe('V2 Living Form store', () => { beforeEach(() => { vi.clearAllMocks(); useImageWorkspaceStore.getState().reset(); const workspace = designWorkspaceFixture(); fetchBootstrapMock.mockResolvedValue(designBootstrapFixture()); fetchProjectMock.mockResolvedValue(workspace); createProjectMock.mockResolvedValue(workspace); renameProjectMock.mockResolvedValue(workspace); deleteProjectMock.mockResolvedValue({ workspaceId: 'workspace-1', deleted: true }); operationIdMock.mockReturnValue('operation-1'); submitCommandMock.mockResolvedValue({ clientOperationId: 'operation-1', runId: 'run-1', workspace, }); }); it('loads one canonical Workspace and opens its resumable event stream', async () => { await loadedStore(); expect(useImageWorkspaceStore.getState()).toMatchObject({ status: 'ready', activeWorkspaceId: 'workspace-1', workspace: { form: { specificationRevision: 3 } }, }); expect(openEventsMock).toHaveBeenCalledWith('workspace-1', 'session-1', undefined); }); it('closes an obsolete event source when a replacement connection wins the race', async () => { const workspace = designWorkspaceFixture(); useImageWorkspaceStore.setState({ status: 'ready', activeWorkspaceId: workspace.workspace.workspaceId, workspace, }); const firstOpen = deferred(); const secondOpen = deferred(); const firstSource = new FakeEventSource(); const secondSource = new FakeEventSource(); openEventsMock .mockReturnValueOnce(firstOpen.promise) .mockReturnValueOnce(secondOpen.promise); useImageWorkspaceStore.getState().connectEvents(); useImageWorkspaceStore.getState().disconnectEvents(); useImageWorkspaceStore.getState().connectEvents(); firstOpen.resolve(firstSource as unknown as EventSource); await Promise.resolve(); secondOpen.resolve(secondSource as unknown as EventSource); await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalledTimes(2)); expect(firstSource.close).toHaveBeenCalledOnce(); expect(secondSource.close).not.toHaveBeenCalled(); }); it('ignores late activity from a closed connection after reconnecting to the same Workspace', async () => { const firstSource = await loadedStore(); const oldOnOpen = firstSource.onopen; const oldOnError = firstSource.onerror; const secondSource = new FakeEventSource(); useImageWorkspaceStore.setState({ pendingOperations: { 'operation-chat-1': { id: 'operation-chat-1', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-1', input: { kind: 'chat', message: '继续完善画面' }, }, status: 'submitting', error: null, clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, }); openEventsMock.mockResolvedValueOnce(secondSource as unknown as EventSource); useImageWorkspaceStore.getState().disconnectEvents(); useImageWorkspaceStore.getState().connectEvents(); await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalledTimes(2)); secondSource.onopen?.(new Event('open')); const fetchCount = fetchProjectMock.mock.calls.length; const event = { type: 'design.assistant.progress', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-chat-1', stage: 'understanding', message: '正在听懂你刚补充的内容…', } as const; firstSource.emit('design.assistant.progress', { ...event, id: 'session-1:7-old' }); oldOnOpen?.(new Event('open')); oldOnError?.(new Event('error')); expect(useImageWorkspaceStore.getState().assistantActivities).toEqual({}); expect(useImageWorkspaceStore.getState().eventState).toBe('connected'); expect(fetchProjectMock).toHaveBeenCalledTimes(fetchCount); secondSource.emit('design.assistant.progress', { ...event, id: 'session-1:7' }); expect(useImageWorkspaceStore.getState().assistantActivities['operation-chat-1']) .toMatchObject({ status: 'active', steps: [{ stage: 'understanding' }] }); }); it('allows a new event connection after opening the previous one failed', async () => { const workspace = designWorkspaceFixture(); const recoveredSource = new FakeEventSource(); useImageWorkspaceStore.setState({ status: 'ready', activeWorkspaceId: workspace.workspace.workspaceId, workspace, }); openEventsMock .mockRejectedValueOnce(new Error('temporary event connection failure')) .mockResolvedValueOnce(recoveredSource as unknown as EventSource); useImageWorkspaceStore.getState().connectEvents(); await vi.waitFor(() => expect(useImageWorkspaceStore.getState().eventState).toBe('degraded')); useImageWorkspaceStore.getState().connectEvents(); await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalledTimes(2)); expect(recoveredSource.close).not.toHaveBeenCalled(); }); it('uses assistant chunk indexes to ignore replayed deltas', async () => { const source = await loadedStore(); useImageWorkspaceStore.setState({ pendingOperations: { 'operation-chat-1': { id: 'operation-chat-1', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-1', input: { kind: 'chat', message: '继续完善画面' }, }, status: 'submitting', error: null, clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, }); const baseEvent = { type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-chat-1', directionRevision: 4, } as const; source.emit('design.assistant.delta', { ...baseEvent, id: 'session-1:7', chunkIndex: 0, delta: '收到', }); source.emit('design.assistant.delta', { ...baseEvent, id: 'session-1:7-replayed', chunkIndex: 0, delta: '收到', }); source.emit('design.assistant.delta', { ...baseEvent, id: 'session-1:8', chunkIndex: 1, delta: ',正在整理', }); source.emit('design.assistant.delta', { ...baseEvent, id: 'session-1:9', chunkIndex: 1, delta: ',正在整理', }); expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({ 'operation-chat-1': '收到,正在整理', }); expect(useImageWorkspaceStore.getState().lastEventId).toBe('session-1:9'); }); it('keeps a field draft separate until the direct edit succeeds', async () => { await loadedStore(); useImageWorkspaceStore.getState().setFieldDraft('output.aspect_ratio', '16:9'); await useImageWorkspaceStore.getState().applyFieldOperations( [{ kind: 'set', path: 'output.aspect_ratio', value: '16:9' }], ['output.aspect_ratio'], ); expect(submitCommandMock).toHaveBeenCalledWith({ kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-1', input: { kind: 'direct_edit', operations: [{ kind: 'set', path: 'output.aspect_ratio', value: '16:9' }], }, }); expect(useImageWorkspaceStore.getState().fieldDrafts).not.toHaveProperty('output.aspect_ratio'); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); it('publishes a pending chat operation before the server result settles', async () => { await loadedStore(); const message = '画一只在月球踢球的熊猫'; const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft(message); const sendPromise = useImageWorkspaceStore.getState().sendChat(); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: message, pendingOperations: { 'operation-1': { id: 'operation-1', status: 'submitting', clearChatDraft: true, command: { kind: 'apply_input', workspaceId: 'workspace-1', clientOperationId: 'operation-1', input: { kind: 'chat', message }, }, }, }, }); const workspace = designWorkspaceFixture({ turns: [{ turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '你希望画面更像漫画,还是更像电影?', }], }); submission.resolve({ clientOperationId: 'operation-1', runId: 'run-2', workspace }); await sendPromise; expect(useImageWorkspaceStore.getState()).toMatchObject({ workspace, chatDraft: '', pendingOperations: {}, }); }); it('keeps public AI activity with its chat operation and settles it into the canonical turn', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫'; const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft(message); const sendPromise = useImageWorkspaceStore.getState().sendChat(); const progress = { type: 'design.assistant.progress', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', } as const; source.emit('design.assistant.progress', { ...progress, id: 'session-1:7', stage: 'understanding', message: '正在听懂你刚补充的内容…', }); source.emit('design.assistant.progress', { ...progress, id: 'session-1:8', stage: 'reviewing_context', message: '正在把它和前面的设计想法放在一起看…', }); expect(useImageWorkspaceStore.getState().assistantActivities['operation-1']) .toMatchObject({ workspaceId: 'workspace-1', directionId: 'direction-1', status: 'active', turnId: null, steps: [ { stage: 'understanding', message: '正在听懂你刚补充的内容…' }, { stage: 'reviewing_context', message: '正在把它和前面的设计想法放在一起看…' }, ], }); source.emit('design.assistant.progress', { ...progress, id: 'session-1:9', stage: 'understanding', message: '迟到的旧阶段', }); expect(useImageWorkspaceStore.getState().assistantActivities['operation-1']?.steps) .toHaveLength(2); source.emit('design.assistant.delta', { id: 'session-1:10', type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', directionRevision: 5, chunkIndex: 0, delta: '这个画面很有趣。', }); expect(useImageWorkspaceStore.getState().assistantActivities['operation-1']?.status) .toBe('completed'); const workspace = designWorkspaceFixture({ turns: [ { turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '这个画面很有趣。', }, { turnId: 'turn-3', rawTurnSequence: 3, userMessage: '后来补充的另一条消息', assistantMessage: '这是后来的回复。', }, ], }); submission.resolve({ clientOperationId: 'operation-1', runId: 'run-2', workspace }); await sendPromise; expect(useImageWorkspaceStore.getState().assistantActivities['operation-1']) .toMatchObject({ status: 'completed', turnId: 'turn-2' }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); it('settles an unknown chat from a replayed Gateway terminal event', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫'; const canonical = designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), turns: [{ turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '你希望它更像漫画还是电影?', }], }); fetchProjectMock.mockResolvedValueOnce(canonical); useImageWorkspaceStore.setState({ chatDraft: message, pendingOperations: { 'operation-chat-unknown': { id: 'operation-chat-unknown', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-unknown', input: { kind: 'chat', message }, }, status: 'unknown', error: '连接暂时中断', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-chat-unknown': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'reviewing_context', message: '正在整理上下文' }], }, }, }); source.emit('command.completed', { id: 'session-1:20', type: 'command.completed', workspaceId: 'workspace-1', clientOperationId: 'operation-chat-unknown', outcome: 'succeeded', errorCode: null, }); await vi.waitFor(() => { expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '', workspace: { form: { rawTurnSequence: 2 } }, assistantActivities: { 'operation-chat-unknown': { status: 'completed', turnId: 'turn-2' }, }, }); }); it('keeps a succeeded terminal event authoritative when command polling then disconnects', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫'; const canonical = designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), turns: [{ turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '你希望它更像漫画还是电影?', }], }); fetchProjectMock.mockResolvedValue(canonical); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft(message); const sendPromise = useImageWorkspaceStore.getState().sendChat(); source.emit('command.completed', { id: 'session-1:20', type: 'command.completed', workspaceId: 'workspace-1', clientOperationId: 'operation-1', outcome: 'succeeded', errorCode: null, }); submission.reject(new ImageWorkspaceApiError( 502, 'IMAGE_WORKSPACE_REQUEST_FAILED', '轮询连接中断', 'unknown', )); await expect(sendPromise).resolves.toMatchObject({ form: { rawTurnSequence: 2 }, }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '', error: null, pendingOperations: {}, workspace: { form: { rawTurnSequence: 2 } }, }); }); it('keeps a failed terminal event authoritative when command polling then disconnects', async () => { const source = await loadedStore(); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft('画一只机器人'); const sendPromise = useImageWorkspaceStore.getState().sendChat(); source.emit('command.failed', { id: 'session-1:20', type: 'command.failed', workspaceId: 'workspace-1', clientOperationId: 'operation-1', outcome: 'failed', errorCode: 'design_reasoner_invalid', }); submission.reject(new ImageWorkspaceApiError( 502, 'IMAGE_WORKSPACE_REQUEST_FAILED', '轮询连接中断', 'unknown', )); await expect(sendPromise).rejects.toMatchObject({ code: 'design_reasoner_invalid', commandOutcome: 'definitive_failure', }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '画一只机器人', error: 'AI 这次没有完成设计整理,内容还在输入框里,请再试一次', pendingOperations: {}, assistantActivities: {}, assistantStreams: {}, }); }); it('does not let a late polling success overwrite a Gateway terminal failure', async () => { const source = await loadedStore(); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft('画一只机器人'); const sendPromise = useImageWorkspaceStore.getState().sendChat(); source.emit('command.failed', { id: 'session-1:20', type: 'command.failed', workspaceId: 'workspace-1', clientOperationId: 'operation-1', outcome: 'failed', errorCode: 'design_reasoner_invalid', }); submission.resolve({ clientOperationId: 'operation-1', runId: 'run-1', workspace: designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), }), }); await expect(sendPromise).rejects.toMatchObject({ code: 'design_reasoner_invalid', commandOutcome: 'definitive_failure', }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '画一只机器人', error: 'AI 这次没有完成设计整理,内容还在输入框里,请再试一次', pendingOperations: {}, }); }); it('does not resurrect pending after direction success settles before polling disconnects', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫'; const canonical = designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), turns: [{ turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '你希望它更像漫画还是电影?', }], }); fetchProjectMock.mockResolvedValue(canonical); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft(message); const sendPromise = useImageWorkspaceStore.getState().sendChat(); source.emit('design.direction.updated', { id: 'session-1:19', type: 'design.direction.updated', clientOperationId: 'operation-1', replayed: false, operation: { operationKind: 'chat', interactionId: 'interaction-2', baseDirectionRevision: 4, newDirectionRevision: 5, rawTurnSequence: 2, specificationRevision: 3, specificationRevisionId: 'specification-revision-3', workspaceViewRevision: 7, specificationRevisionCreated: false, meaningChanged: false, changeSet: { interaction_id: 'interaction-2', changes: [] }, turnId: 'turn-2', assistantMessage: '你希望它更像漫画还是电影?', createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: null, generationTaskId: null, }, form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), }); await vi.waitFor(() => { expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); source.emit('command.completed', { id: 'session-1:20', type: 'command.completed', workspaceId: 'workspace-1', clientOperationId: 'operation-1', outcome: 'succeeded', errorCode: null, }); submission.reject(new ImageWorkspaceApiError( 502, 'IMAGE_WORKSPACE_REQUEST_FAILED', '轮询连接中断', 'unknown', )); await expect(sendPromise).resolves.toMatchObject({ form: { rawTurnSequence: 2 } }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '', error: null, pendingOperations: {}, workspace: { form: { rawTurnSequence: 2 } }, }); }); it('keeps the Quote created immediately after a committed generation chat', async () => { const source = await loadedStore(); const message = '开始生成'; const quoteForm = designFormFixture({ directionRevision: 6, rawTurnSequence: 2, workspaceViewRevision: 8, activeQuotes: [designQuoteFixture()], }); const canonical = designWorkspaceFixture({ form: quoteForm, turns: [ ...designWorkspaceFixture().turns, { turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。', }, ], }); fetchProjectMock.mockResolvedValue(canonical); useImageWorkspaceStore.setState({ chatDraft: message, pendingOperations: { 'operation-1': { id: 'operation-1', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-1', input: { kind: 'chat', message }, }, status: 'submitting', error: null, clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-1': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'submitting', turnId: null, steps: [], }, }, }); source.emit('design.direction.updated', { id: 'session-1:19', type: 'design.direction.updated', clientOperationId: 'operation-1', replayed: false, operation: { operationKind: 'chat', interactionId: 'interaction-2', baseDirectionRevision: 4, newDirectionRevision: 5, rawTurnSequence: 2, specificationRevision: 3, specificationRevisionId: 'specification-revision-3', workspaceViewRevision: 7, specificationRevisionCreated: false, meaningChanged: false, changeSet: { interaction_id: 'interaction-2', changes: [] }, turnId: 'turn-2', assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。', createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: null, generationTaskId: null, }, form: designFormFixture({ directionRevision: 5, rawTurnSequence: 2, workspaceViewRevision: 7, }), }); source.emit('design.direction.updated', { id: 'session-1:20', type: 'design.direction.updated', clientOperationId: 'operation-1', replayed: false, operation: { operationKind: 'quote_request', interactionId: 'interaction-quote-1', baseDirectionRevision: 5, newDirectionRevision: 6, rawTurnSequence: 2, specificationRevision: 3, specificationRevisionId: 'specification-revision-3', workspaceViewRevision: 8, specificationRevisionCreated: false, meaningChanged: false, changeSet: { interaction_id: 'interaction-quote-1', changes: [] }, turnId: null, assistantMessage: null, createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: 'quote-1', generationTaskId: null, }, form: quoteForm, }); await vi.waitFor(() => { expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '', pendingOperations: {}, workspace: { form: { directionRevision: 6 } }, }); }); expect(useImageWorkspaceStore.getState().workspace?.form.activeQuotes) .toEqual([designQuoteFixture()]); expect(useImageWorkspaceStore.getState().assistantActivities).toMatchObject({ 'operation-1': { status: 'completed', turnId: 'turn-2' }, }); }); it('keeps the optimistic chat until a terminal success is visible in the canonical Workspace', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫'; useImageWorkspaceStore.setState({ chatDraft: message, pendingOperations: { 'operation-chat-unknown': { id: 'operation-chat-unknown', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-unknown', input: { kind: 'chat', message }, }, status: 'unknown', error: '连接暂时中断', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-chat-unknown': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'composing', message: '已经整理好,正在准备回复…' }], }, }, }); fetchProjectMock.mockResolvedValueOnce(designWorkspaceFixture()); source.emit('command.completed', { id: 'session-1:20', type: 'command.completed', workspaceId: 'workspace-1', clientOperationId: 'operation-chat-unknown', outcome: 'succeeded', errorCode: null, }); await vi.waitFor(() => { expect(fetchProjectMock).toHaveBeenCalledTimes(2); }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: message, pendingOperations: { 'operation-chat-unknown': { status: 'unknown' }, }, }); const canonical = designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), turns: [{ turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '你希望它更像漫画还是电影?', }], }); fetchProjectMock.mockResolvedValueOnce(canonical); source.emit('design.direction.updated', { id: 'session-1:21', type: 'design.direction.updated', clientOperationId: 'operation-chat-unknown', replayed: false, operation: { operationKind: 'chat', interactionId: 'interaction-2', baseDirectionRevision: 4, newDirectionRevision: 5, rawTurnSequence: 2, specificationRevision: 3, specificationRevisionId: 'specification-revision-3', workspaceViewRevision: 7, specificationRevisionCreated: false, meaningChanged: false, changeSet: { interaction_id: 'interaction-2', changes: [] }, turnId: 'turn-2', assistantMessage: '你希望它更像漫画还是电影?', createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: null, generationTaskId: null, }, form: designFormFixture({ rawTurnSequence: 2, directionRevision: 5 }), }); await vi.waitFor(() => { expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '', assistantActivities: { 'operation-chat-unknown': { status: 'completed', turnId: 'turn-2' }, }, }); }); it('uses the Gateway operation turn ID when identical chats are both pending', async () => { const source = await loadedStore(); const message = '再画一只机器人'; const command = (operationId: string) => ({ kind: 'apply_input' as const, workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: operationId, input: { kind: 'chat' as const, message }, }); useImageWorkspaceStore.setState({ pendingOperations: { 'operation-chat-1': { id: 'operation-chat-1', label: '发送创作想法', command: command('operation-chat-1'), status: 'unknown', error: '结果尚未确认', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, 'operation-chat-2': { id: 'operation-chat-2', label: '发送创作想法', command: command('operation-chat-2'), status: 'unknown', error: '结果尚未确认', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-chat-1': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'composing', message: '已经整理好,正在准备回复…' }], }, 'operation-chat-2': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'composing', message: '已经整理好,正在准备回复…' }], }, }, }); const canonical = designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 3, directionRevision: 6 }), turns: [ { turnId: 'turn-2', rawTurnSequence: 2, userMessage: message, assistantMessage: '第一条相同消息的回复。', }, { turnId: 'turn-3', rawTurnSequence: 3, userMessage: message, assistantMessage: '第二条相同消息的回复。', }, ], }); fetchProjectMock.mockResolvedValueOnce(canonical); source.emit('design.direction.updated', { id: 'session-1:22', type: 'design.direction.updated', clientOperationId: 'operation-chat-2', replayed: false, operation: { operationKind: 'chat', interactionId: 'interaction-3', baseDirectionRevision: 5, newDirectionRevision: 6, rawTurnSequence: 3, specificationRevision: 3, specificationRevisionId: 'specification-revision-3', workspaceViewRevision: 8, specificationRevisionCreated: false, meaningChanged: false, changeSet: { interaction_id: 'interaction-3', changes: [] }, turnId: 'turn-3', assistantMessage: '第二条相同消息的回复。', createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: null, generationTaskId: null, }, form: designFormFixture({ rawTurnSequence: 3, directionRevision: 6 }), }); await vi.waitFor(() => { expect(useImageWorkspaceStore.getState().pendingOperations['operation-chat-2']) .toBeUndefined(); }); expect(useImageWorkspaceStore.getState().pendingOperations['operation-chat-1']) .toBeDefined(); expect(useImageWorkspaceStore.getState().assistantActivities).toMatchObject({ 'operation-chat-1': { turnId: null }, 'operation-chat-2': { status: 'completed', turnId: 'turn-3' }, }); }); it('settles an unknown chat when Gateway later reports terminal failure', async () => { const source = await loadedStore(); useImageWorkspaceStore.setState({ pendingOperations: { 'operation-chat-unknown': { id: 'operation-chat-unknown', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-unknown', input: { kind: 'chat', message: '画一只机器人' }, }, status: 'unknown', error: '连接暂时中断', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-chat-unknown': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'reviewing_context', message: '正在整理上下文' }], }, }, }); source.emit('command.failed', { id: 'session-1:20', type: 'command.failed', workspaceId: 'workspace-1', clientOperationId: 'operation-chat-unknown', outcome: 'failed', errorCode: 'design_reasoner_invalid', }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); expect(useImageWorkspaceStore.getState().assistantActivities).toEqual({}); expect(useImageWorkspaceStore.getState().error) .toBe('AI 这次没有完成设计整理,内容还在输入框里,请再试一次'); }); it('does not replace the active Workspace when an earlier Workspace command completes late', async () => { await loadedStore(); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft('旧项目里的消息'); const sendPromise = useImageWorkspaceStore.getState().sendChat(); const workspaceB = designWorkspaceFixture({ workspace: designSummaryFixture({ workspaceId: 'workspace-2', clientWorkspaceId: 'client-workspace-2', directionId: 'direction-2', sessionId: 'session-2', title: '当前项目', }), form: designFormFixture({ workspaceId: 'workspace-2', directionId: 'direction-2' }), }); useImageWorkspaceStore.setState({ activeWorkspaceId: 'workspace-2', workspace: workspaceB, assistantActivities: {}, assistantStreams: {}, }); submission.resolve({ clientOperationId: 'operation-1', runId: 'run-old', workspace: designWorkspaceFixture(), }); await sendPromise; expect(useImageWorkspaceStore.getState()).toMatchObject({ activeWorkspaceId: 'workspace-2', workspace: { workspace: { workspaceId: 'workspace-2', title: '当前项目' } }, pendingOperations: {}, }); }); it('does not apply an old A result after the user switches A to B and back to A', async () => { await loadedStore(); const submission = deferred>>(); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.getState().setChatDraft('旧的 A 项目消息'); const sendPromise = useImageWorkspaceStore.getState().sendChat(); const workspaceB = designWorkspaceFixture({ workspace: designSummaryFixture({ workspaceId: 'workspace-2', clientWorkspaceId: 'client-workspace-2', directionId: 'direction-2', sessionId: 'session-2', title: '项目 B', }), form: designFormFixture({ workspaceId: 'workspace-2', directionId: 'direction-2' }), }); const currentWorkspaceA = designWorkspaceFixture({ workspace: designSummaryFixture({ workspaceViewRevision: 12, title: '重新打开的项目 A' }), form: designFormFixture({ rawTurnSequence: 4, workspaceViewRevision: 12 }), turns: [{ turnId: 'turn-4', rawTurnSequence: 4, userMessage: 'A 项目中较新的消息', assistantMessage: '这是较新的回复。', }], }); fetchProjectMock .mockResolvedValueOnce(workspaceB) .mockResolvedValueOnce(currentWorkspaceA); await useImageWorkspaceStore.getState().selectProject('workspace-2'); await useImageWorkspaceStore.getState().selectProject('workspace-1'); submission.resolve({ clientOperationId: 'operation-1', runId: 'run-old-a', workspace: designWorkspaceFixture({ form: designFormFixture({ rawTurnSequence: 2, workspaceViewRevision: 7 }), }), }); await sendPromise; expect(useImageWorkspaceStore.getState()).toMatchObject({ activeWorkspaceId: 'workspace-1', pendingOperations: {}, workspace: { workspace: { title: '重新打开的项目 A', workspaceViewRevision: 12 }, form: { rawTurnSequence: 4, workspaceViewRevision: 12 }, }, }); }); it('does not show a late B loading error after the user returns to A', async () => { await loadedStore(); const workspaceB = deferred>>(); const currentWorkspaceA = designWorkspaceFixture({ workspace: designSummaryFixture({ workspaceViewRevision: 12, title: '重新打开的项目 A' }), form: designFormFixture({ rawTurnSequence: 4, workspaceViewRevision: 12 }), }); fetchProjectMock .mockReturnValueOnce(workspaceB.promise) .mockResolvedValueOnce(currentWorkspaceA); const selectBPromise = useImageWorkspaceStore.getState().selectProject('workspace-2'); await useImageWorkspaceStore.getState().selectProject('workspace-1'); workspaceB.reject(new Error('项目 B 加载失败')); await selectBPromise; expect(useImageWorkspaceStore.getState()).toMatchObject({ activeWorkspaceId: 'workspace-1', error: null, workspace: { workspace: { title: '重新打开的项目 A', workspaceViewRevision: 12 }, form: { rawTurnSequence: 4, workspaceViewRevision: 12 }, }, }); }); it('ignores progress and reply deltas that do not belong to the active pending chat', async () => { const source = await loadedStore(); source.emit('design.assistant.progress', { id: 'session-1:7', type: 'design.assistant.progress', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'unrelated-operation', stage: 'understanding', message: '不应显示', }); source.emit('design.assistant.delta', { id: 'session-1:8', type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'unrelated-operation', directionRevision: 4, chunkIndex: 0, delta: '不应显示', }); expect(useImageWorkspaceStore.getState().assistantActivities).toEqual({}); expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({}); expect(useImageWorkspaceStore.getState().lastEventId).toBe('session-1:8'); }); it('settles a definitive reasoner failure without losing the chat draft', async () => { const source = await loadedStore(); useImageWorkspaceStore.getState().setChatDraft('画一只会做饭的机器人'); submitCommandMock.mockImplementationOnce(async () => { source.emit('design.assistant.progress', { id: 'session-1:6', type: 'design.assistant.progress', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', stage: 'reviewing_context', message: '正在把它和前面的设计想法放在一起看…', }); source.emit('design.assistant.delta', { id: 'session-1:7', type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', directionRevision: 4, chunkIndex: 0, delta: '正在整理', }); throw new ImageWorkspaceApiError( 502, 'design_reasoner_invalid', 'AI 没有整理好这次想法,请再试一次', 'definitive_failure', ); }); await expect(useImageWorkspaceStore.getState().sendChat()).rejects.toMatchObject({ status: 502, code: 'design_reasoner_invalid', }); expect(useImageWorkspaceStore.getState()).toMatchObject({ chatDraft: '画一只会做饭的机器人', error: 'AI 没有整理好这次想法,请再试一次', }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); expect(useImageWorkspaceStore.getState().assistantActivities).toEqual({}); expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({}); }); it('settles any Main-confirmed terminal run without an error-code allowlist', async () => { await loadedStore(); submitCommandMock.mockRejectedValueOnce(new ImageWorkspaceApiError( 422, 'policy_blocked', '当前内容不符合创作安全规则', 'definitive_failure', )); await expect(useImageWorkspaceStore.getState().requestQuote()).rejects.toMatchObject({ code: 'policy_blocked', commandOutcome: 'definitive_failure', }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); expect(useImageWorkspaceStore.getState().error).toBe('当前内容不符合创作安全规则'); }); it('keeps the accepted command identity when polling auth expires', async () => { const source = await loadedStore(); useImageWorkspaceStore.getState().setChatDraft('画一只会做饭的机器人'); submitCommandMock.mockImplementationOnce(async () => { source.emit('design.assistant.progress', { id: 'session-1:6', type: 'design.assistant.progress', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', stage: 'reviewing_context', message: '正在把它和前面的设计想法放在一起看…', }); source.emit('design.assistant.delta', { id: 'session-1:7', type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-1', directionRevision: 4, chunkIndex: 0, delta: '正在整理', }); throw new ImageWorkspaceApiError( 401, 'AUTH_EXPIRED', '登录状态已失效,请重新登录', 'unknown', ); }); await expect(useImageWorkspaceStore.getState().sendChat()).rejects.toMatchObject({ status: 401, code: 'AUTH_EXPIRED', commandOutcome: 'unknown', }); expect(useImageWorkspaceStore.getState().chatDraft).toBe('画一只会做饭的机器人'); expect(useImageWorkspaceStore.getState().pendingOperations['operation-1']).toMatchObject({ id: 'operation-1', status: 'unknown', }); expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({ 'operation-1': '正在整理', }); expect(useImageWorkspaceStore.getState().assistantActivities['operation-1']) .toMatchObject({ status: 'unknown', steps: [{ stage: 'reviewing_context' }] }); }); it('preserves an earlier unknown activity when another chat starts', async () => { await loadedStore(); const submission = deferred>>(); operationIdMock.mockReturnValueOnce('operation-chat-2'); submitCommandMock.mockReturnValueOnce(submission.promise); useImageWorkspaceStore.setState({ chatDraft: '再补充一只小兔子', pendingOperations: { 'operation-chat-1': { id: 'operation-chat-1', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-1', input: { kind: 'chat', message: '先画一只小熊' }, }, status: 'unknown', error: '结果尚未确认', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, assistantActivities: { 'operation-chat-1': { workspaceId: 'workspace-1', directionId: 'direction-1', status: 'unknown', turnId: null, steps: [{ stage: 'reviewing_context', message: '正在整理上下文' }], }, }, }); const sendPromise = useImageWorkspaceStore.getState().sendChat(); expect(useImageWorkspaceStore.getState().assistantActivities).toMatchObject({ 'operation-chat-1': { status: 'unknown', steps: [{ stage: 'reviewing_context' }], }, }); expect(useImageWorkspaceStore.getState().pendingOperations).toMatchObject({ 'operation-chat-1': { status: 'unknown' }, 'operation-chat-2': { status: 'submitting' }, }); submission.resolve({ clientOperationId: 'operation-chat-2', runId: 'run-2', workspace: designWorkspaceFixture(), }); await sendPromise; }); it('expires quote blockers only after the canonical Specification changes', async () => { await loadedStore(); const blocker = { code: 'aspect_ratio_required', message: 'Choose an aspect ratio supported by the frozen route.', severity: 'blocker' as const, path: 'output.aspect_ratio', }; useImageWorkspaceStore.setState({ quoteBlockers: [blocker] }); await useImageWorkspaceStore.getState().refreshWorkspace(); expect(useImageWorkspaceStore.getState().quoteBlockers).toEqual([blocker]); const revisedWorkspace = designWorkspaceFixture({ form: designFormFixture({ directionRevision: 5, specificationRevision: 4, specificationRevisionId: 'specification-revision-4', }), }); submitCommandMock.mockResolvedValueOnce({ clientOperationId: 'operation-1', runId: 'run-2', workspace: revisedWorkspace, }); await useImageWorkspaceStore.getState().applyFieldOperations([ { kind: 'set', path: 'output.aspect_ratio', value: '16:9' }, ]); expect(useImageWorkspaceStore.getState().quoteBlockers).toEqual([]); expect(useImageWorkspaceStore.getState().workspace?.form.specificationRevision).toBe(4); }); it('treats a terminal quote-blocked response as definitive instead of unknown', async () => { await loadedStore(); submitCommandMock.mockRejectedValueOnce(new ImageWorkspaceApiError( 422, 'design_quote_blocked', 'The frozen compiler needs more information.', )); await expect(useImageWorkspaceStore.getState().requestQuote()).rejects.toMatchObject({ status: 422, code: 'design_quote_blocked', }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); expect(useImageWorkspaceStore.getState().error).toBeNull(); }); it('settles an unknown quote from its blocked event, then allows a revised Quote', async () => { const source = await loadedStore(); submitCommandMock.mockRejectedValueOnce(new Error('network disconnected')); await expect(useImageWorkspaceStore.getState().requestQuote()).rejects.toThrow( 'network disconnected', ); expect(useImageWorkspaceStore.getState().pendingOperations['operation-1']?.status).toBe('unknown'); const blocker = { code: 'aspect_ratio_required', message: 'Choose an aspect ratio supported by the frozen route.', severity: 'blocker' as const, path: 'output.aspect_ratio', }; source.emit('design.quote.blocked', { id: 'session-1:quote-blocked', type: 'design.quote.blocked', clientOperationId: 'operation-1', blockers: [blocker], warnings: [], form: designFormFixture(), }); expect(useImageWorkspaceStore.getState()).toMatchObject({ pendingOperations: {}, quoteBlockers: [blocker], error: null, }); const revisedWorkspace = designWorkspaceFixture({ form: designFormFixture({ directionRevision: 5, specificationRevision: 4, specificationRevisionId: 'specification-revision-4', }), }); submitCommandMock.mockResolvedValueOnce({ clientOperationId: 'operation-1', runId: 'run-edit', workspace: revisedWorkspace, }); await useImageWorkspaceStore.getState().applyFieldOperations([ { kind: 'set', path: 'output.aspect_ratio', value: '16:9' }, ]); expect(useImageWorkspaceStore.getState().quoteBlockers).toEqual([]); submitCommandMock.mockResolvedValueOnce({ clientOperationId: 'operation-1', runId: 'run-quote-revised', workspace: revisedWorkspace, }); await useImageWorkspaceStore.getState().requestQuote(); expect(submitCommandMock).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'request_quote', expectedDirectionRevision: 5, specificationRevision: 4, })); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); it('refreshes on a definitive revision conflict and preserves the unsent draft', async () => { await loadedStore(); useImageWorkspaceStore.getState().setFieldDraft('intent.purpose', '新的用途'); submitCommandMock.mockRejectedValueOnce(new ImageWorkspaceApiError( 409, 'design_direction_revision_conflict', 'conflict', )); await expect(useImageWorkspaceStore.getState().applyFieldOperations( [{ kind: 'set', path: 'intent.purpose', value: '新的用途' }], ['intent.purpose'], )).rejects.toMatchObject({ status: 409 }); expect(fetchProjectMock).toHaveBeenCalledWith('workspace-1'); expect(useImageWorkspaceStore.getState().fieldDrafts).toEqual({ 'intent.purpose': '新的用途', }); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); it('retains an uncertain accepted write and retries the exact same command identity', async () => { await loadedStore(); submitCommandMock.mockRejectedValueOnce(new Error('network disconnected')); await expect(useImageWorkspaceStore.getState().requestQuote()).rejects.toThrow( 'network disconnected', ); const pending = useImageWorkspaceStore.getState().pendingOperations['operation-1']; expect(pending).toMatchObject({ id: 'operation-1', status: 'unknown', command: { kind: 'request_quote', expectedDirectionRevision: 4, specificationRevision: 3, }, }); submitCommandMock.mockResolvedValueOnce({ clientOperationId: 'operation-1', runId: 'run-original', workspace: designWorkspaceFixture(), }); await useImageWorkspaceStore.getState().retryOperation('operation-1'); expect(submitCommandMock).toHaveBeenNthCalledWith(2, pending.command); expect(useImageWorkspaceStore.getState().pendingOperations).toEqual({}); }); it('reconciles assistant deltas and canonical Form events without mutating drafts', async () => { const source = await loadedStore(); useImageWorkspaceStore.getState().setFieldDraft('intent.purpose', '本地草稿'); useImageWorkspaceStore.setState({ assistantStreams: { 'operation-chat-unknown': '这段回复还没确认' }, pendingOperations: { 'operation-chat-unknown': { id: 'operation-chat-unknown', label: '发送创作想法', command: { kind: 'apply_input', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, clientOperationId: 'operation-chat-unknown', input: { kind: 'chat', message: '保留这条未知结果' }, }, status: 'unknown', error: '结果尚未确认', clearDraftPaths: [], clearChatDraft: true, baseRawTurnSequence: 1, }, }, quoteBlockers: [{ code: 'missing_choice', message: '还需要选择作品形状', severity: 'blocker', path: 'output.aspect_ratio', }], }); source.emit('design.assistant.delta', { id: 'session-1:7', type: 'design.assistant.delta', workspaceId: 'workspace-1', directionId: 'direction-1', clientOperationId: 'operation-chat-1', directionRevision: 4, chunkIndex: 0, delta: '正在整理', }); source.emit('design.direction.updated', { id: 'session-1:8', type: 'design.direction.updated', clientOperationId: 'operation-chat-1', replayed: false, operation: { operationKind: 'chat', interactionId: 'interaction-2', baseDirectionRevision: 4, newDirectionRevision: 5, rawTurnSequence: 2, specificationRevision: 4, specificationRevisionId: 'specification-revision-4', workspaceViewRevision: 7, specificationRevisionCreated: true, meaningChanged: true, changeSet: { interaction_id: 'interaction-2', changes: [] }, turnId: 'turn-2', assistantMessage: '已更新', createdDecisionPromptIds: [], resolvedDecisionPromptId: null, supersededDecisionPromptIds: [], supersededQuoteCount: 0, quoteId: null, generationTaskId: null, }, form: designFormFixture({ directionRevision: 5, specificationRevision: 4, specificationRevisionId: 'specification-revision-4', workspaceViewRevision: 7, }), }); expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({ 'operation-chat-unknown': '这段回复还没确认', }); expect(useImageWorkspaceStore.getState()).toMatchObject({ lastEventId: 'session-1:8', fieldDrafts: { 'intent.purpose': '本地草稿' }, quoteBlockers: [], workspace: { form: { directionRevision: 5, specificationRevision: 4 } }, }); }); it('creates, renames and deletes only Workspace identities', async () => { await useImageWorkspaceStore.getState().createProject('新项目'); await useImageWorkspaceStore.getState().renameProject('workspace-1', '新名称'); await useImageWorkspaceStore.getState().deleteProject('workspace-1'); expect(createProjectMock).toHaveBeenCalledWith('新项目'); expect(renameProjectMock).toHaveBeenCalledWith('workspace-1', '新名称'); expect(deleteProjectMock).toHaveBeenCalledWith('workspace-1'); expect(useImageWorkspaceStore.getState().activeWorkspaceId).toBeNull(); }); it('renames a background Workspace without changing the active Workspace', async () => { await loadedStore(); const backgroundSummary = designSummaryFixture({ workspaceId: 'workspace-2', clientWorkspaceId: 'client-workspace-2', title: '背景项目', directionId: 'direction-2', sessionId: 'session-2', }); useImageWorkspaceStore.setState((state) => ({ bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: [...state.bootstrap.workspaces, backgroundSummary] } : null, })); renameProjectMock.mockResolvedValueOnce(designWorkspaceFixture({ workspace: { ...backgroundSummary, title: '背景项目新名称' }, })); await useImageWorkspaceStore.getState().renameProject('workspace-2', '背景项目新名称'); expect(useImageWorkspaceStore.getState()).toMatchObject({ activeWorkspaceId: 'workspace-1', workspace: { workspace: { workspaceId: 'workspace-1' } }, }); expect(useImageWorkspaceStore.getState().bootstrap?.workspaces).toEqual( expect.arrayContaining([ expect.objectContaining({ workspaceId: 'workspace-2', title: '背景项目新名称' }), ]), ); }); });