155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
import { existsSync, mkdtempSync, 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;
|
|
return new LocalImageWorkspace({
|
|
userDataDir,
|
|
now: () => new Date('2026-07-29T10:00:00.000Z'),
|
|
createId: () => String(++sequence),
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const directory of temporaryDirectories.splice(0)) {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('local image workspace', () => {
|
|
it('requires both an unpackaged app and the explicit local mode', () => {
|
|
expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: false, configuredMode: 'local' })).toBe(true);
|
|
expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: false, configuredMode: undefined })).toBe(false);
|
|
expect(isLocalImageWorkspaceDevelopmentEnabled({ isPackaged: true, configuredMode: 'local' })).toBe(false);
|
|
});
|
|
|
|
it('starts empty, persists projects and Agents, and survives a new service instance', async () => {
|
|
const userDataDir = createTemporaryDirectory();
|
|
const service = createService(userDataDir);
|
|
|
|
const emptyWorkspace = await service.getSnapshot();
|
|
expect(emptyWorkspace).toMatchObject({
|
|
projects: [],
|
|
activeProjectId: null,
|
|
});
|
|
expect(emptyWorkspace).not.toHaveProperty('runtimeMode');
|
|
|
|
const created = await service.createProject(' 本地角色设计 ');
|
|
expect(created.projects).toHaveLength(1);
|
|
expect(created.projects[0]).toMatchObject({
|
|
name: '本地角色设计',
|
|
agents: [{ name: '视觉创作 Agent' }],
|
|
messages: [],
|
|
});
|
|
|
|
const withAgent = await service.addAgent(created.projects[0].id);
|
|
expect(withAgent.projects[0].agents.map((agent) => agent.name)).toEqual([
|
|
'视觉创作 Agent',
|
|
'创作 Agent 2',
|
|
]);
|
|
|
|
const reloaded = new LocalImageWorkspace({ userDataDir });
|
|
await expect(reloaded.getSnapshot()).resolves.toMatchObject({
|
|
activeProjectId: created.projects[0].id,
|
|
projects: [{ name: '本地角色设计', agents: [{}, {}] }],
|
|
});
|
|
});
|
|
|
|
it('persists reference uploads and creates deterministic multi-image placeholder turns', async () => {
|
|
const userDataDir = createTemporaryDirectory();
|
|
const service = createService(userDataDir);
|
|
const created = await service.createProject('连续创作');
|
|
const project = created.projects[0];
|
|
const agent = project.agents[0];
|
|
const upload = await service.uploadReference({
|
|
projectId: project.id,
|
|
fileName: '参考图.png',
|
|
mimeType: 'image/png',
|
|
contentBase64: Buffer.from('fake-png-bytes').toString('base64'),
|
|
});
|
|
|
|
expect(upload.reference).toMatchObject({
|
|
alt: '参考图.png',
|
|
url: expect.stringMatching(/^data:image\/png;base64,/),
|
|
});
|
|
|
|
const input = {
|
|
projectId: project.id,
|
|
agentId: agent.id,
|
|
prompt: '夜色中的机械城堡',
|
|
referenceImageIds: [upload.reference.id],
|
|
settings: {
|
|
modeId: 'edit',
|
|
modelId: 'local-placeholder-v1',
|
|
aspectRatioId: '16:9',
|
|
resolutionId: '1024',
|
|
outputCountId: '2',
|
|
},
|
|
};
|
|
const firstTurn = await service.sendMessage(input);
|
|
const firstOutput = firstTurn.projects[0].messages.at(-1)!;
|
|
|
|
expect(firstTurn.projects[0].messages).toHaveLength(2);
|
|
expect(firstTurn.projects[0].messages[0].images).toEqual([upload.reference]);
|
|
expect(firstOutput).toMatchObject({
|
|
role: 'assistant',
|
|
status: 'succeeded',
|
|
text: '已生成 2 张图片。',
|
|
});
|
|
expect(firstOutput.images).toHaveLength(2);
|
|
expect(firstOutput.images[0].url).toMatch(/^data:image\/svg\+xml;base64,/);
|
|
const placeholderSvg = Buffer.from(firstOutput.images[0].url.split(',')[1], 'base64').toString('utf8');
|
|
expect(placeholderSvg).toContain('CREATIVE · 1');
|
|
expect(placeholderSvg).toContain('标准创作模型');
|
|
expect(placeholderSvg).not.toMatch(/LOCAL DEV|local-placeholder-v1|本地开发|本地占位/);
|
|
|
|
const secondTurn = await service.sendMessage(input);
|
|
const secondOutput = secondTurn.projects[0].messages.at(-1)!;
|
|
expect(secondOutput.images.map((image) => image.url)).toEqual(
|
|
firstOutput.images.map((image) => image.url),
|
|
);
|
|
|
|
const reloaded = new LocalImageWorkspace({ userDataDir });
|
|
expect((await reloaded.getSnapshot()).projects[0].messages).toHaveLength(4);
|
|
});
|
|
|
|
it('rejects unknown option ids and removes all persisted data on reset', async () => {
|
|
const userDataDir = createTemporaryDirectory();
|
|
const service = createService(userDataDir);
|
|
const created = await service.createProject('可重置项目');
|
|
const project = created.projects[0];
|
|
|
|
await expect(service.sendMessage({
|
|
projectId: project.id,
|
|
agentId: project.agents[0].id,
|
|
prompt: '测试',
|
|
referenceImageIds: [],
|
|
settings: { modelId: 'unknown-model' },
|
|
})).rejects.toMatchObject<LocalImageWorkspaceError>({
|
|
status: 400,
|
|
code: 'IMAGE_WORKSPACE_INVALID_SETTING',
|
|
});
|
|
|
|
expect(existsSync(getLocalImageWorkspaceDirectory(userDataDir))).toBe(true);
|
|
await expect(service.reset()).resolves.toMatchObject({ projects: [], activeProjectId: null });
|
|
expect(existsSync(getLocalImageWorkspaceDirectory(userDataDir))).toBe(false);
|
|
await expect(new LocalImageWorkspace({ userDataDir }).getSnapshot()).resolves.toMatchObject({ projects: [] });
|
|
});
|
|
});
|