feat(canvas): cut over design agent to living form v2
This commit is contained in:
1 parent
f245603923
commit
b0b5a602b5
35 files changed
+5729
-13986
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user