import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { getLocalImageWorkspaceDirectory, isLocalImageWorkspaceDevelopmentEnabled, LocalImageWorkspace, LocalImageWorkspaceError, } from '@electron/image-workspace/local-workspace'; const temporaryDirectories: string[] = []; function createTemporaryDirectory(): string { const directory = mkdtempSync(join(tmpdir(), 'niancode-image-workspace-')); temporaryDirectories.push(directory); return directory; } function createService(userDataDir: string) { let sequence = 0; let tick = 0; return new LocalImageWorkspace({ userDataDir, now: () => new Date(Date.parse('2026-07-31T10:00:00.000Z') + tick++ * 1_000), createId: () => String(++sequence), }); } afterEach(() => { for (const directory of temporaryDirectories.splice(0)) { rmSync(directory, { recursive: true, force: true }); } }); describe('local AI design workspace', () => { it('routes the default development command through the local adapter', () => { const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8')) as { scripts: Record; }; expect(packageJson.scripts.dev).toBe('node scripts/run-local-image-workspace-dev.mjs'); expect(packageJson.scripts['dev:image-workspace:local']) .toBe('node scripts/run-local-image-workspace-dev.mjs'); }); it('never enables the local adapter in packaged apps or explicit cloud mode', () => { expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: false, configuredMode: 'local', })).toBe(true); expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: false, isDevelopmentServer: true, })).toBe(true); expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: false, configuredMode: 'cloud', isDevelopmentServer: true, })).toBe(false); expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: true, configuredMode: 'local', })).toBe(false); }); it('persists titled Workspaces and keeps creation idempotent by client key', async () => { const userDataDir = createTemporaryDirectory(); const service = createService(userDataDir); await expect(service.bootstrap()).resolves.toMatchObject({ workspaces: [] }); const created = await service.createWorkspace({ clientWorkspaceId: 'client-one', title: ' 本地角色设计 ', }); const replay = await service.createWorkspace({ clientWorkspaceId: 'client-one', title: '不能覆盖标题', }); expect(replay.workspaceId).toBe(created.workspaceId); expect(replay.title).toBe('本地角色设计'); await service.renameWorkspace({ workspaceId: created.workspaceId, title: '角色设计第二版', }); const reloaded = new LocalImageWorkspace({ userDataDir }); await expect(reloaded.bootstrap()).resolves.toMatchObject({ workspaces: [{ title: '角色设计第二版', turnRevision: 0, viewRevision: 1 }], }); }); it('creates a Quote first and a visible generation task only after confirmation', async () => { const service = createService(createTemporaryDirectory()); const created = await service.createWorkspace({ clientWorkspaceId: 'client-flow', title: '海洋公益海报', }); const discussed = await service.submitMessage({ workspaceId: created.workspaceId, clientTurnId: 'turn-one', expectedTurnRevision: 0, message: '做一张保护海洋的竖版公益海报', }); const quote = discussed.messages.at(-1)?.generationQuote; expect(discussed.phase).toBe('awaiting_confirmation'); expect(quote).toMatchObject({ status: 'active', medium: 'image' }); await expect(service.listTasks(created.workspaceId)).resolves.toEqual([]); const confirmed = await service.confirmGeneration({ workspaceId: created.workspaceId, clientTurnId: 'turn-two', expectedTurnRevision: 1, quoteId: quote!.quoteId, }); expect(confirmed.turnRevision).toBe(2); const queued = await service.listTasks(created.workspaceId); const running = await service.listTasks(created.workspaceId); const succeeded = await service.listTasks(created.workspaceId); expect(queued[0].status).toBe('queued'); expect(running[0].status).toBe('running'); expect(succeeded[0]).toMatchObject({ status: 'succeeded', resultAssets: [{ mimeType: 'image/svg+xml' }], }); const asset = succeeded[0].resultAssets[0]; const response = await service.openAssetContent(created.workspaceId, asset.assetId); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('image/svg+xml'); expect(await response.text()).toContain('AI Design Workspace'); }); it('enforces turn CAS and removes only the isolated development data on reset', async () => { const userDataDir = createTemporaryDirectory(); const service = createService(userDataDir); const created = await service.createWorkspace({ clientWorkspaceId: 'client-reset', title: '可重置项目', }); await expect(service.submitMessage({ workspaceId: created.workspaceId, clientTurnId: 'stale-turn', expectedTurnRevision: 3, message: '测试', })).rejects.toMatchObject({ status: 409, code: 'workspace_revision_conflict', }); expect(existsSync(getLocalImageWorkspaceDirectory(userDataDir))).toBe(true); await expect(service.reset()).resolves.toMatchObject({ workspaces: [] }); expect(existsSync(getLocalImageWorkspaceDirectory(userDataDir))).toBe(false); }); });