merge: integrate marketplace and design v2 client

This commit is contained in:
2026-08-30 22:45:44 +08:00
43 changed files with 5662 additions and 14134 deletions

View File

@@ -12,7 +12,6 @@ import { tmpdir } from 'node:os';
import { basename, join, resolve } from 'node:path';
export type LaunchElectronOptions = {
imageWorkspaceMode?: 'local';
skipSetup?: boolean;
};
@@ -80,7 +79,6 @@ function buildElectronEnvironment(
resources: ElectronTestResources,
skipSetup: boolean,
hostApiPort: number,
imageWorkspaceMode?: 'local',
): Record<string, string> {
const env: Record<string, string> = {};
for (const key of PASSTHROUGH_ENVIRONMENT_KEYS) {
@@ -105,9 +103,6 @@ function buildElectronEnvironment(
if (skipSetup) {
env.NIANCODE_E2E_SKIP_SETUP = '1';
}
if (imageWorkspaceMode) {
env.NIANCODE_IMAGE_WORKSPACE_MODE = imageWorkspaceMode;
}
if (process.platform === 'linux') {
env.ELECTRON_DISABLE_SANDBOX = '1';
}
@@ -128,7 +123,6 @@ async function launchMakeloreElectron(
resources,
skipSetup,
hostApiPort,
options.imageWorkspaceMode,
),
timeout: 90_000,
});

View File

@@ -1,264 +0,0 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('AI Design workspace', () => {
test('keeps Prompt Museum cards usable when relative media fails', async ({ launchElectronApp }) => {
test.setTimeout(90_000);
const app = await launchElectronApp({
imageWorkspaceMode: 'local',
skipSetup: true,
});
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-painting').click();
const imageSidebar = page.getByTestId('sidebar-image-workspace');
await expect(imageSidebar.getByTestId('sidebar-open-image-prompt-museum')).toBeVisible();
await app.evaluate(({ ipcMain }) => {
const image = {
url: '/api/image-prompt-museum/e2e-entry/media/thumbnail',
width: 1200,
height: 900,
alt: 'E2E museum thumbnail',
};
const card = {
id: 'e2e-entry',
slug: 'e2e-entry',
title: '相对媒体 E2E',
summary: '验证图片失败不会阻断卡片。',
thumbnail: image,
categories: [],
model: { id: 'e2e-model', name: 'E2E Model' },
language: 'zh-CN',
attribution: {
author: { name: 'E2E 作者' },
source: { name: 'E2E 来源' },
license: { name: '测试许可', attributionText: 'E2E attribution' },
},
publishedAt: '2026-08-18T00:00:00.000Z',
updatedAt: '2026-08-18T00:00:00.000Z',
};
const respond = (json: unknown) => ({
ok: true,
data: { status: 200, ok: true, json },
});
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => {
const path = request.path ?? '';
if (path === '/api/works/image-prompt-museum') {
return respond({
success: true,
data: {
items: [card],
facets: { useCases: [], styles: [], subjects: [] },
nextCursor: null,
total: 1,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry') {
return respond({
success: true,
data: {
...card,
prompt: 'Create a resilient image card.',
variables: [],
images: [image],
requiresReferenceImages: false,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry/media/thumbnail') {
return respond({ mimeType: 'text/html', dataBase64: 'PGgxPnVuc2FmZTwvaDE+' });
}
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
});
});
await imageSidebar.getByTestId('sidebar-open-image-prompt-museum').click();
const cardButton = page.getByRole('button', { name: '查看 相对媒体 E2E' });
await expect(cardButton).toBeVisible();
await expect(page.getByLabel('E2E museum thumbnail加载失败')).toBeVisible();
await expect(cardButton).toBeEnabled();
await cardButton.click();
await expect(page.getByRole('button', { name: '关闭详情' })).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('keeps the conversation and generation task panes full height', async ({ launchElectronApp }) => {
test.setTimeout(150_000);
const app = await launchElectronApp({
imageWorkspaceMode: 'local',
skipSetup: true,
});
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-painting').click();
const imageSidebar = page.getByTestId('sidebar-image-workspace');
await expect(imageSidebar.getByTestId('sidebar-open-image-prompt-museum')).toBeVisible();
await expect(imageSidebar.getByTestId('sidebar-create-image-project')).toBeVisible();
await page.getByTestId('sidebar-create-image-project').click();
await page.getByLabel('项目名称').fill('多会话验收项目');
await page.getByRole('button', { name: '创建项目' }).click();
const projectCard = page.locator('[data-testid^="sidebar-image-project-"]').first();
await expect(projectCard).not.toContainText('1 个会话');
await expect(projectCard).not.toContainText('设计会话');
await expect(page.getByRole('button', { name: '收起 多会话验收项目 的会话' })).toBeVisible();
await expect(page.getByRole('button', { name: '重命名 多会话验收项目' })).toBeVisible();
await expect(page.locator('[data-testid^="sidebar-image-conversations-"]').first())
.not.toHaveClass(/border-t/);
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
await expect(page.getByTestId('image-workspace-conversation-fade')).toBeVisible();
await expect(page.getByTestId('design-task-list')).toBeVisible();
await expect(page.getByTestId('design-task-fade')).toBeVisible();
await expect(page.getByTestId('image-canvas-conversation-select')).toHaveCount(0);
const emptyState = page.getByTestId('image-workspace-empty-state');
await expect(emptyState).toContainText('今天一起描绘什么?');
await expect(emptyState.getByRole('img', { name: 'Makelore' })).toHaveCount(1);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: '上传参考图' }).click();
await fileChooserPromise;
await expect(page.getByRole('dialog', { name: '选择视频首帧' })).toHaveCount(0);
const layoutBounds = await page.evaluate(() => {
const readBounds = (testId: string) => {
const element = document.querySelector(`[data-testid="${testId}"]`);
if (!element) return null;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
borderLeftWidth: style.borderLeftWidth,
overflowY: style.overflowY,
};
};
return {
shell: readBounds('main-layout'),
canvas: readBounds('image-canvas-page'),
conversation: readBounds('image-workspace-conversation-pane'),
tasks: readBounds('design-task-list'),
taskScroll: readBounds('design-task-scroll'),
};
});
expect(layoutBounds.shell).not.toBeNull();
expect(layoutBounds.canvas).not.toBeNull();
expect(layoutBounds.conversation).not.toBeNull();
expect(layoutBounds.tasks).not.toBeNull();
expect(layoutBounds.taskScroll).not.toBeNull();
expect(Math.abs(layoutBounds.canvas!.top - layoutBounds.shell!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.canvas!.bottom - layoutBounds.shell!.bottom)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.conversation!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.conversation!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.tasks!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.tasks!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1);
expect(layoutBounds.tasks!.borderLeftWidth).toBe('1px');
expect(layoutBounds.taskScroll!.overflowY).toBe('auto');
const firstPrompt = '为项目设计一张蓝色星空海报';
await page.getByLabel('设计需求').fill(firstPrompt);
await page.getByLabel('设计需求').press('Enter');
await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt);
await expect(page.getByText('设计 Agent', { exact: true })).toHaveCount(0);
await expect(page.getByText('你', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('image-workspace-message').first()).toHaveClass(/w-full/);
const firstQuote = page.locator('[data-testid^="design-quote-"]').first();
await expect(firstQuote.getByRole('button', { name: '继续调整' })).toBeVisible();
await expect(firstQuote.getByTestId('design-quote-model')).toHaveCount(0);
await expect(firstQuote).not.toContainText('提示词模式');
await page.getByRole('button', { name: '确认并开始生成' }).click();
await expect(page.getByRole('button', { name: '确认生成' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '继续调整' })).toHaveCount(0);
const task = page
.locator('[data-testid^="design-task-local-task-"]')
.filter({ hasText: firstPrompt })
.first();
await expect(task).toContainText(firstPrompt);
const firstConversation = page.locator('[data-testid^="sidebar-image-conversation-"]').first();
await expect(firstConversation).toContainText('图片任务已创建,可在右侧任务列表查看进度。');
await expect(firstConversation).not.toContainText('新会话');
const firstConversationTestId = await page
.locator('[data-testid^="sidebar-image-conversation-"]')
.first()
.getAttribute('data-testid');
expect(firstConversationTestId).toBeTruthy();
await projectCard.hover();
await page.getByRole('button', { name: '新建设计会话' }).click();
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(2);
const secondPrompt = '这一条会话只讨论红色角色动态视频';
await page.getByLabel('设计需求').fill(secondPrompt);
await page.getByRole('button', { name: '发送给设计 Agent' }).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(secondPrompt);
await expect(page.getByTestId('design-quote-final-prompt')).toHaveValue(secondPrompt);
await page.getByTestId('design-quote-final-prompt').fill('用户编辑后的动态视频完整提示词');
await expect(page.getByTestId('design-quote-model')).toHaveCount(0);
await page.getByTestId('design-quote-resolution').selectOption('720P');
await page.getByTestId('design-quote-aspect-ratio').selectOption('9:16');
await page.getByTestId('design-quote-duration').selectOption('4');
await expect(page.getByRole('button', { name: '重新报价中' })).toBeDisabled();
await expect(page.getByRole('button', { name: '确认并开始生成' })).toBeEnabled({ timeout: 10_000 });
await page.getByRole('button', { name: '确认并开始生成' }).click();
await expect(page.getByRole('button', { name: '确认并开始生成' })).toHaveCount(0);
await page.getByTestId(firstConversationTestId!).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt);
await expect(page.getByTestId('image-workspace-conversation')).not.toContainText(secondPrompt);
// The local adapter deterministically advances queued -> running -> succeeded as the
// task fallback reconciles. The delete confirmation also explains what happens to
// queued and running tasks on the server.
await task.getByRole('button', { name: '查看生图任务详情' }).click();
const taskDetails = page.getByRole('dialog', { name: '生成内容详情' });
await expect(taskDetails).toContainText('已完成', { timeout: 40_000 });
await taskDetails.getByRole('button', { name: '关闭生成内容详情' }).click();
await projectCard.hover();
await page.getByRole('button', { name: '删除 多会话验收项目' }).click();
const deleteDialog = page.getByRole('dialog', { name: '删除项目' });
await expect(deleteDialog).toContainText('未提交的任务会被取消并释放预留积分');
await expect(deleteDialog).toContainText('已提交或运行中的任务会继续后台结算');
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled();
await expect(projectCard).toBeVisible();
await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收');
await expect(deleteDialog.getByText('项目名称必须完全匹配。')).toBeVisible();
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled();
await expect(projectCard).toBeVisible();
await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收项目');
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeEnabled();
await deleteDialog.getByTestId('confirm-delete-image-project').click();
await expect(deleteDialog).toHaveCount(0);
await expect(page.locator('[data-testid^="sidebar-image-project-"]')).toHaveCount(0);
await expect(imageSidebar).toContainText('还没有设计项目');
await expect(page.getByRole('heading', { name: '创建第一个设计项目' })).toBeVisible();
await imageSidebar.getByTestId('sidebar-open-image-prompt-museum').click();
await expect(page.getByTestId('image-prompt-museum-page')).toBeVisible();
await expect(page.getByRole('heading', { name: '获取灵感' })).toBeVisible();
await expect(page.getByText('提示词博物馆', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('titlebar-page-title')).toHaveText('获取灵感');
await expect(page.getByTestId('titlebar-logo')).toHaveCount(1);
await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/sticky/);
await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/top-0/);
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -0,0 +1,455 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('AI Design V2 workspace', () => {
test('keeps Prompt Museum cards usable when relative media fails', async ({ launchElectronApp }) => {
test.setTimeout(90_000);
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await app.evaluate(({ ipcMain }) => {
const image = {
url: '/api/image-prompt-museum/e2e-entry/media/thumbnail',
width: 1200,
height: 900,
alt: 'E2E museum thumbnail',
};
const card = {
id: 'e2e-entry',
slug: 'e2e-entry',
title: '相对媒体 E2E',
summary: '验证图片失败不会阻断卡片。',
thumbnail: image,
categories: [],
model: { id: 'e2e-model', name: 'E2E Model' },
language: 'zh-CN',
attribution: {
author: { name: 'E2E 作者' },
source: { name: 'E2E 来源' },
license: { name: '测试许可', attributionText: 'E2E attribution' },
},
publishedAt: '2026-08-18T00:00:00.000Z',
updatedAt: '2026-08-18T00:00:00.000Z',
};
const respond = (json: unknown) => ({
ok: true,
data: { status: 200, ok: true, json },
});
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => {
const path = request.path ?? '';
if (path === '/api/works/image-workspace') {
return respond({
success: true,
data: {
capabilities: { conversation: true, generation: true, image: true, video: true },
workspaces: [],
},
});
}
if (path === '/api/works/image-prompt-museum') {
return respond({
success: true,
data: {
items: [card],
facets: { useCases: [], styles: [], subjects: [] },
nextCursor: null,
total: 1,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry') {
return respond({
success: true,
data: {
...card,
prompt: 'Create a resilient image card.',
variables: [],
images: [image],
requiresReferenceImages: false,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry/media/thumbnail') {
return respond({ mimeType: 'text/html', dataBase64: 'PGgxPnVuc2FmZTwvaDE+' });
}
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
});
});
await page.getByTestId('ai-module-option-painting').click();
const imageSidebar = page.getByTestId('sidebar-image-workspace');
await expect(imageSidebar.getByRole('button', { name: '获取灵感' })).toBeVisible();
await imageSidebar.getByRole('button', { name: '获取灵感' }).click();
const cardButton = page.getByRole('button', { name: '查看 相对媒体 E2E' });
await expect(cardButton).toBeVisible();
await expect(page.getByLabel('E2E museum thumbnail加载失败')).toBeVisible();
await expect(cardButton).toBeEnabled();
await cardButton.click();
await expect(page.getByRole('button', { name: '关闭详情' })).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('keeps conversation and direct edits on one Living Form through immutable confirmation', async ({ launchElectronApp }) => {
test.setTimeout(120_000);
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await app.evaluate(({ ipcMain }) => {
const summary = {
workspaceId: 'workspace-1',
clientWorkspaceId: 'client-workspace-1',
title: '咖啡机新品主视觉',
directionId: 'direction-1',
sessionId: 'session-1',
directionRevision: 4,
specificationRevision: 3,
workspaceViewRevision: 6,
updatedAt: '2026-08-30T10:00:00.000Z',
};
const workspace = {
workspace: summary,
form: {
schemaVersion: 1,
workspaceId: 'workspace-1',
directionId: 'direction-1',
directionRevision: 4,
rawTurnSequence: 1,
specificationRevision: 3,
workspaceViewRevision: 6,
specificationRevisionId: 'specification-revision-3',
specificationDigest: 'a'.repeat(64),
specification: {
schema_version: 1,
values: {
intent: {
media: 'image',
creation_intent: 'create',
purpose: '新品发布主视觉',
channel: '小红书首图',
audience: '年轻消费者',
},
content: {
concept: '把便携咖啡机呈现为城市通勤中的精密工具',
subjects: [],
exact_content: [],
},
visual: {},
layout: { regions: [] },
references: [],
image: {
operation_intent: 'new_generation',
preserve_instructions: [],
change_instructions: [],
},
video: { shots: [] },
output: {
aspect_ratio: '3:4',
orientation: 'portrait',
delivery_format: 'PNG',
variant_count: 1,
variant_purposes: [],
},
constraints: {
must_include: [],
must_preserve: [],
must_avoid: [],
legal_notes: [],
safety_notes: [],
acceptance_criteria: [],
},
},
field_decisions: {
'intent.media': {
resolution: 'resolved',
provenance: 'user',
locked: true,
changed_by: 'interaction-1',
origin_turn_id: 'turn-1',
},
'output.aspect_ratio': {
resolution: 'resolved',
provenance: 'user',
locked: false,
changed_by: 'interaction-1',
origin_turn_id: 'turn-1',
},
},
},
decisionPrompts: [],
activeQuotes: [] as Array<Record<string, unknown>>,
recentChangeSet: { interaction_id: 'interaction-1', changes: [] },
},
turns: [{
turnId: 'turn-1',
rawTurnSequence: 1,
userMessage: '做一张便携咖啡机的小红书主视觉',
assistantMessage: '我已经把用途和初步构想写入设计表单。',
}],
tasks: [] as Array<Record<string, unknown>>,
assets: [],
};
const respond = (json: unknown) => ({
ok: true,
data: { status: 200, ok: true, json },
});
const envelope = (data: unknown) => respond({ success: true, data });
const e2eSession = {
accessToken: 'e2e-design-access-token',
tokenType: 'Bearer',
expiresAt: Date.now() + 60 * 60 * 1000,
lastActiveAt: Date.now(),
canRefresh: false,
};
const e2eGlobal = globalThis as typeof globalThis & {
__designE2eAuthReady?: boolean;
__designE2eProfileReady?: boolean;
};
e2eGlobal.__designE2eAuthReady = false;
e2eGlobal.__designE2eProfileReady = false;
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: {
path?: string;
method?: string;
body?: string | null;
}) => {
const path = request.path ?? '';
if (path === '/api/auth/session/sync') {
return respond({ success: true, session: e2eSession });
}
if (path === '/api/auth/me') {
e2eGlobal.__designE2eAuthReady = true;
return respond({
success: true,
moduleAccess: { programming: true, design: true, learning: true, robot: true },
});
}
if (path === '/api/works/user/agent-profile') {
e2eGlobal.__designE2eProfileReady = true;
return respond({
success: true,
profile: {
display_name: 'E2E 设计用户',
age: null,
gender: null,
avatar_url: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 1,
updated_at: '2026-08-30T10:00:00.000Z',
},
});
}
if (path === '/api/works/image-workspace') {
return envelope({
capabilities: { conversation: true, generation: true, image: true, video: true },
workspaces: [workspace.workspace],
});
}
if (path === '/api/works/image-workspace/workspaces/workspace-1' && request.method === 'GET') {
return envelope(workspace);
}
if (path === '/api/works/image-workspace/workspaces/workspace-1/commands') {
const command = JSON.parse(request.body ?? '{}') as {
kind?: string;
clientOperationId?: string;
input?: {
kind?: string;
message?: string;
operations?: Array<{ kind?: string; path?: string; value?: unknown }>;
};
};
if (command.kind === 'apply_input') {
if (command.input?.kind === 'direct_edit') {
for (const operation of command.input.operations ?? []) {
if (operation.kind === 'set' && operation.path === 'output.aspect_ratio') {
workspace.form.specification.values.output.aspect_ratio = String(operation.value ?? '');
}
}
}
if (command.input?.kind === 'chat' && command.input.message) {
workspace.form.rawTurnSequence += 1;
workspace.turns.push({
turnId: `turn-${workspace.form.rawTurnSequence}`,
rawTurnSequence: workspace.form.rawTurnSequence,
userMessage: command.input.message,
assistantMessage: '已把这条补充写入同一份设计规格。',
});
}
workspace.form.directionRevision += 1;
workspace.form.specificationRevision += 1;
workspace.form.workspaceViewRevision += 1;
workspace.form.specificationRevisionId = `specification-revision-${workspace.form.specificationRevision}`;
workspace.workspace.directionRevision = workspace.form.directionRevision;
workspace.workspace.specificationRevision = workspace.form.specificationRevision;
workspace.workspace.workspaceViewRevision = workspace.form.workspaceViewRevision;
}
if (command.kind === 'request_quote') {
workspace.form.activeQuotes = [{
quoteId: 'quote-1',
status: 'offered',
specificationRevision: workspace.form.specificationRevision,
specificationRevisionId: workspace.form.specificationRevisionId,
specificationDigest: workspace.form.specificationDigest,
medium: 'image',
compilerVersion: 'design-compiler-v2',
outputSummary: {
medium: 'image',
strategy: 'direct',
aspectRatio: workspace.form.specification.values.output.aspect_ratio,
outputCount: 1,
deliveryFormat: 'PNG',
durationSeconds: null,
requiredOutputRoles: ['primary'],
},
warnings: [],
quotedDesignPoints: 12,
maximumCustomerChargeAtoms: 1200,
expiresAt: '2030-01-02T03:04:05.000Z',
}];
}
if (command.kind === 'confirm_generation') {
const offeredQuote = workspace.form.activeQuotes[0] ?? {};
workspace.form.activeQuotes = [{ ...offeredQuote, status: 'consumed' }];
workspace.tasks = [{
taskId: 'task-1',
quoteId: 'quote-1',
status: 'queued',
taskRevision: 1,
specificationRevision: workspace.form.specificationRevision,
specificationRevisionId: workspace.form.specificationRevisionId,
specificationDigest: workspace.form.specificationDigest,
medium: 'image',
compilerVersion: 'design-compiler-v2',
outputSummary: offeredQuote.outputSummary,
maximumCustomerChargeAtoms: 1200,
customerBilling: {
quotedAtoms: 1200,
heldAtoms: 1200,
chargedAtoms: 0,
refundedAtoms: 0,
pendingResolutionAtoms: 0,
},
customerHoldExpiresAt: '2030-01-02T03:04:05.000Z',
resolutionDeadlineAt: '2030-01-02T03:09:05.000Z',
progress: { stage: 'queued', completedRequiredSteps: 0, totalRequiredSteps: 1 },
executionHealth: 'normal',
cancellation: { state: 'available', outcome: null },
issues: [],
resultAssetIds: [],
nextActions: ['cancel'],
createdAt: '2026-08-30T10:01:00.000Z',
}];
}
return envelope({
clientOperationId: command.clientOperationId ?? 'operation-e2e',
runId: `run-${command.clientOperationId ?? 'e2e'}`,
workspace,
});
}
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
});
});
await page.addInitScript(() => {
const now = Date.now();
localStorage.setItem('niancode-auth', JSON.stringify({
state: {
authBase: 'https://biz.nianxx.cn/auth/',
clientId: 'app',
accessToken: 'e2e-design-access-token',
tokenType: 'Bearer',
expiresAt: now + 60 * 60 * 1000,
lastActiveAt: now,
canRefresh: false,
legacyRefreshToken: null,
user: {
username: 'e2e-design-user',
userId: 'e2e-user-1',
tenantId: null,
deptId: null,
authorities: [],
},
moduleAccess: { programming: true, design: true, learning: true, robot: true },
},
version: 2,
}));
});
await page.reload();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await expect.poll(async () => await app.evaluate(() => Boolean(
(globalThis as typeof globalThis & { __designE2eAuthReady?: boolean }).__designE2eAuthReady,
))).toBe(true);
await page.getByTestId('ai-module-option-painting').click();
await expect(page.getByTestId('image-workspace')).toBeVisible();
await expect.poll(async () => await app.evaluate(() => Boolean(
(globalThis as typeof globalThis & { __designE2eProfileReady?: boolean }).__designE2eProfileReady,
))).toBe(true);
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
await expect(page.getByTestId('image-workspace-living-form')).toBeVisible();
await expect(page.getByText('Living Form')).toBeVisible();
await expect(page.getByText('目标与用途')).toBeVisible();
await expect(page.getByText('内容与叙事')).toBeVisible();
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(0);
const layout = await page.evaluate(() => {
const conversation = document.querySelector('[data-testid="image-workspace-conversation"]')
?.getBoundingClientRect();
const form = document.querySelector('[data-testid="image-workspace-living-form"]')
?.getBoundingClientRect();
return conversation && form
? { conversationHeight: conversation.height, formHeight: form.height, topGap: Math.abs(conversation.top - form.top) }
: null;
});
expect(layout).not.toBeNull();
expect(layout!.conversationHeight).toBeGreaterThan(300);
expect(Math.abs(layout!.conversationHeight - layout!.formHeight)).toBeLessThanOrEqual(1);
expect(layout!.topGap).toBeLessThanOrEqual(1);
const ratioInput = page.getByLabel('画幅比例');
await ratioInput.fill('16:9');
await expect(ratioInput).toHaveValue('16:9');
await page.getByRole('button', { name: '保存', exact: true }).click();
await expect(page.getByText('方向 5')).toBeVisible();
const followUp = '主体更克制,留出品牌标题空间';
await page.getByLabel('给设计 Agent 留言').fill(followUp);
await page.getByLabel('给设计 Agent 留言').press('Enter');
await expect(page.getByTestId('image-workspace-conversation')).toContainText(followUp);
await expect(page.getByTestId('image-workspace-conversation'))
.toContainText('已把这条补充写入同一份设计规格。');
await page.getByRole('button', { name: '编译生成方案' }).click();
await expect(page.getByText('报价已冻结')).toBeVisible();
await expect(page.getByText('对应规格版本 5确认后不会再读取当前表单的新修改。')).toBeVisible();
await expect(page.getByText('确认即按此不可变报价创建任务,不会在客户端拼接或改写提示词。')).toBeVisible();
await expect(page.getByLabel(/Prompt|提示词/i)).toHaveCount(0);
await page.getByRole('button', { name: '确认生成' }).click();
await expect(page.getByText('生成任务')).toBeVisible();
await expect(page.getByText('图片任务')).toBeVisible();
await expect(page.getByText('排队中')).toBeVisible();
await expect(page.getByText('正常')).toBeVisible();
await expect(page.getByText('1200', { exact: true })).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
});