import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppError } from '@/lib/error-model'; import { createHostEventSource, ensureHostApiToken, getHostApiBase, hostApiFetch, } from '@/lib/host-api'; import { confirmImageWorkspaceGeneration, createImageWorkspaceProject, fetchImageWorkspace, ImageWorkspaceApiError, openImageWorkspaceTaskEvents, resolveImageWorkspaceAssetUrl, sendImageWorkspaceMessage, } from '@/lib/image-workspace'; vi.mock('@/lib/host-api', () => ({ hostApiFetch: vi.fn(), createHostEventSource: vi.fn(), ensureHostApiToken: vi.fn(), getHostApiBase: vi.fn(), })); const hostApiFetchMock = vi.mocked(hostApiFetch); const createHostEventSourceMock = vi.mocked(createHostEventSource); const ensureHostApiTokenMock = vi.mocked(ensureHostApiToken); const getHostApiBaseMock = vi.mocked(getHostApiBase); const bootstrap = { capabilities: { conversation: true, generation: true, image: true, video: false, }, workspaces: [], }; describe('AI design renderer API boundary', () => { beforeEach(() => { hostApiFetchMock.mockReset(); hostApiFetchMock.mockResolvedValue({ success: true, status: 200, data: bootstrap }); ensureHostApiTokenMock.mockReset(); ensureHostApiTokenMock.mockResolvedValue('host-token'); createHostEventSourceMock.mockReset(); createHostEventSourceMock.mockReturnValue({ close: vi.fn() } as unknown as EventSource); getHostApiBaseMock.mockReset(); getHostApiBaseMock.mockReturnValue('http://127.0.0.1:13210'); }); it('uses only the Main-owned route and never forwards a Works Square token', async () => { await fetchImageWorkspace(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-workspace', {}); }); it('opens task events only after the local Host API token is ready', async () => { const source = await openImageWorkspaceTaskEvents('workspace/one'); expect(source).toBe(createHostEventSourceMock.mock.results[0].value); expect(createHostEventSourceMock).toHaveBeenCalledWith( '/api/works/image-workspace/workspaces/workspace%2Fone/events', ); expect(ensureHostApiTokenMock.mock.invocationCallOrder[0]) .toBeLessThan(createHostEventSourceMock.mock.invocationCallOrder[0]); }); it('creates an idempotent Workspace with a trimmed user-visible title', async () => { await createImageWorkspaceProject(' 角色设计 ', 'workspace-client-1'); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-workspace/workspaces', { method: 'POST', body: JSON.stringify({ clientWorkspaceId: 'workspace-client-1', title: '角色设计', }), }); }); it('sends conversation turns with the current turn revision and no provider settings', async () => { await sendImageWorkspaceMessage('workspace/one', 4, ' 继续调整构图 '); const [path, init] = hostApiFetchMock.mock.calls[0]; expect(path).toBe('/api/works/image-workspace/workspaces/workspace%2Fone/messages'); expect(init).toMatchObject({ method: 'POST' }); expect(JSON.parse(String(init?.body))).toMatchObject({ expectedTurnRevision: 4, message: '继续调整构图', attachmentAssetIds: [], clientTurnId: expect.stringMatching(/^turn-/), }); expect(String(init?.body)).not.toMatch(/model|resolution|outputCount/); }); it('confirms a Quote through a separate structured action route', async () => { await confirmImageWorkspaceGeneration('workspace-one', 5, 'quote/one'); const [path, init] = hostApiFetchMock.mock.calls[0]; expect(path).toBe( '/api/works/image-workspace/workspaces/workspace-one/quotes/quote%2Fone/confirm', ); expect(JSON.parse(String(init?.body))).toMatchObject({ expectedTurnRevision: 5, clientTurnId: expect.stringMatching(/^turn-/), }); }); it('preserves structured Main error codes for revision recovery', async () => { hostApiFetchMock.mockRejectedValueOnce(new AppError( 'UNKNOWN', 'Design workspace revision has changed', undefined, { status: 409, backendCode: 'workspace_revision_conflict' }, )); await expect(fetchImageWorkspace()).rejects.toMatchObject({ status: 409, code: 'workspace_revision_conflict', }); }); it('builds a loopback media URL with only the Host API session token', async () => { await expect(resolveImageWorkspaceAssetUrl('/api/works/image-workspace/assets/one')) .resolves.toBe( 'http://127.0.0.1:13210/api/works/image-workspace/assets/one?token=host-token', ); }); });