import type { IncomingMessage, ServerResponse } from 'node:http'; import { Readable, Writable } from 'node:stream'; import { describe, expect, it, vi } from 'vitest'; import { handleImageWorkspaceRoutes } from '@electron/api/routes/image-workspace'; import type { HostApiContext } from '@electron/api/context'; function createResponse() { const chunks: string[] = []; const res = { statusCode: 0, headersSent: false, setHeader: vi.fn(), end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }), destroy: vi.fn(), on: vi.fn(), once: vi.fn(), emit: vi.fn(), write: vi.fn(), } as unknown as ServerResponse; return { res, get statusCode() { return res.statusCode; }, json: () => JSON.parse(chunks.join('')) as Record, }; } function createRequest(method: string, body?: unknown): IncomingMessage { const request = Readable.from( body === undefined ? [] : [JSON.stringify(body)], ) as unknown as IncomingMessage; request.method = method; request.headers = body === undefined ? {} : { 'content-type': 'application/json' }; return request; } class MediaResponse extends Writable { statusCode = 0; readonly chunks: Buffer[] = []; readonly headers = new Map(); setHeader(name: string, value: string | number | readonly string[]): this { this.headers.set(name.toLowerCase(), String(value)); return this; } _write( chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void, ): void { this.chunks.push(Buffer.from(chunk)); callback(); } } const bootstrap = { capabilities: { conversation: true, generation: true, image: true, video: true, }, workspaces: [], }; describe('AI design Main route boundary', () => { it('does not claim unrelated routes', async () => { const response = createResponse(); const handled = await handleImageWorkspaceRoutes( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/projects'), {} as never, ); expect(handled).toBe(false); expect(response.res.end).not.toHaveBeenCalled(); }); it('returns a stable unavailable response only when Main has no module', async () => { const response = createResponse(); await handleImageWorkspaceRoutes( { method: 'GET' } as IncomingMessage, response.res, new URL('http://127.0.0.1/api/works/image-workspace'), {} as never, ); expect(response.statusCode).toBe(501); expect(response.json()).toMatchObject({ success: false, code: 'IMAGE_WORKSPACE_UNAVAILABLE', error: 'AI 设计暂不可用', }); }); it('maps Host routes to the deep Workspace module instead of provider-shaped calls', async () => { const workspace = { bootstrap: vi.fn().mockResolvedValue(bootstrap), createWorkspace: vi.fn().mockResolvedValue({ workspaceId: 'workspace-one' }), renameWorkspace: vi.fn(), getWorkspace: vi.fn(), submitMessage: vi.fn().mockResolvedValue({ workspaceId: 'workspace/one' }), confirmGeneration: vi.fn().mockResolvedValue({ workspaceId: 'workspace/one' }), listTasks: vi.fn().mockResolvedValue([]), openAssetContent: vi.fn(), getCapabilities: vi.fn(), reset: vi.fn().mockResolvedValue(bootstrap), }; const ctx = { imageWorkspace: workspace } as unknown as HostApiContext; const getResponse = createResponse(); await handleImageWorkspaceRoutes( createRequest('GET'), getResponse.res, new URL('http://127.0.0.1/api/works/image-workspace'), ctx, ); expect(getResponse.json()).toMatchObject({ success: true, data: bootstrap }); const createResponseBody = createResponse(); await handleImageWorkspaceRoutes( createRequest('POST', { clientWorkspaceId: 'client-one', title: '角色设计', }), createResponseBody.res, new URL('http://127.0.0.1/api/works/image-workspace/workspaces'), ctx, ); expect(workspace.createWorkspace).toHaveBeenCalledWith({ clientWorkspaceId: 'client-one', title: '角色设计', }); const messageResponse = createResponse(); await handleImageWorkspaceRoutes( createRequest('POST', { clientTurnId: 'turn-one', expectedTurnRevision: 3, message: '继续编辑', attachmentAssetIds: [], modelId: 'must-be-ignored', }), messageResponse.res, new URL( 'http://127.0.0.1/api/works/image-workspace/workspaces/workspace%2Fone/messages', ), ctx, ); expect(workspace.submitMessage).toHaveBeenCalledWith({ workspaceId: 'workspace/one', clientTurnId: 'turn-one', expectedTurnRevision: 3, message: '继续编辑', attachmentAssetIds: [], }); const confirmResponse = createResponse(); await handleImageWorkspaceRoutes( createRequest('POST', { clientTurnId: 'turn-two', expectedTurnRevision: 4, }), confirmResponse.res, new URL( 'http://127.0.0.1/api/works/image-workspace/workspaces/workspace%2Fone/quotes/quote%2Fone/confirm', ), ctx, ); expect(workspace.confirmGeneration).toHaveBeenCalledWith({ workspaceId: 'workspace/one', quoteId: 'quote/one', clientTurnId: 'turn-two', expectedTurnRevision: 4, }); }); it('streams private asset bytes and preserves Range response headers', async () => { const openAssetContent = vi.fn().mockResolvedValue(new Response('partial', { status: 206, headers: { 'Accept-Ranges': 'bytes', 'Content-Range': 'bytes 0-6/100', 'Content-Type': 'video/mp4', }, })); const ctx = { imageWorkspace: { openAssetContent }, } as unknown as HostApiContext; const request = createRequest('GET'); request.headers = { range: 'bytes=0-6' }; const response = new MediaResponse(); const handled = await handleImageWorkspaceRoutes( request, response as unknown as ServerResponse, new URL( 'http://127.0.0.1/api/works/image-workspace/workspaces/workspace-one/assets/asset-one/content', ), ctx, ); expect(handled).toBe(true); expect(openAssetContent).toHaveBeenCalledWith( 'workspace-one', 'asset-one', 'bytes=0-6', ); expect(response.statusCode).toBe(206); expect(response.headers.get('content-range')).toBe('bytes 0-6/100'); expect(Buffer.concat(response.chunks).toString()).toBe('partial'); }); });