merge: integrate remote and local main histories
# Conflicts: # .project-docs/20-architecture/module-map.md # .project-docs/30-worklog/current-state.md # .project-docs/50-evidence/evidence-index.md # src/pages/MyPlugins/index.tsx # tests/unit/pi-agent-server-process-real.test.ts # tests/unit/pi-managed-worker-opener.test.ts # tests/unit/plugin-marketplace-pages.test.tsx
This commit is contained in:
@@ -96,7 +96,7 @@ test.describe('AI Design V2 workspace', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps conversation and direct edits on one Living Form through immutable confirmation', async ({ launchElectronApp }) => {
|
||||
test('keeps conversation and youth-friendly direct edits on one canonical design through production confirmation', async ({ launchElectronApp }) => {
|
||||
test.setTimeout(120_000);
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
@@ -193,7 +193,7 @@ test.describe('AI Design V2 workspace', () => {
|
||||
turnId: 'turn-1',
|
||||
rawTurnSequence: 1,
|
||||
userMessage: '做一张便携咖啡机的小红书主视觉',
|
||||
assistantMessage: '我已经把用途和初步构想写入设计表单。',
|
||||
assistantMessage: '明白了:你想做一张适合小红书的便携咖啡机图片。',
|
||||
}],
|
||||
tasks: [] as Array<Record<string, unknown>>,
|
||||
assets: [],
|
||||
@@ -278,6 +278,9 @@ test.describe('AI Design V2 workspace', () => {
|
||||
if (operation.kind === 'set' && operation.path === 'output.aspect_ratio') {
|
||||
workspace.form.specification.values.output.aspect_ratio = String(operation.value ?? '');
|
||||
}
|
||||
if (operation.kind === 'set' && operation.path === 'output.orientation') {
|
||||
workspace.form.specification.values.output.orientation = String(operation.value ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (command.input?.kind === 'chat' && command.input.message) {
|
||||
@@ -286,7 +289,7 @@ test.describe('AI Design V2 workspace', () => {
|
||||
turnId: `turn-${workspace.form.rawTurnSequence}`,
|
||||
rawTurnSequence: workspace.form.rawTurnSequence,
|
||||
userMessage: command.input.message,
|
||||
assistantMessage: '已把这条补充写入同一份设计规格。',
|
||||
assistantMessage: '记住了:主体更简洁,也会为标题留出空间。',
|
||||
});
|
||||
}
|
||||
workspace.form.directionRevision += 1;
|
||||
@@ -403,50 +406,72 @@ test.describe('AI Design V2 workspace', () => {
|
||||
(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.getByTestId('image-workspace-creation-pane')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '我的创作' })).toBeVisible();
|
||||
await expect(page.getByText('把便携咖啡机呈现为城市通勤中的精密工具')).toBeVisible();
|
||||
await expect(page.getByText('Living Form')).toHaveCount(0);
|
||||
await expect(page.getByText('目标与用途')).toHaveCount(0);
|
||||
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"]')
|
||||
const cardPane = document.querySelector('[data-testid="image-workspace-creation-pane"]')
|
||||
?.getBoundingClientRect();
|
||||
return conversation && form
|
||||
? { conversationHeight: conversation.height, formHeight: form.height, topGap: Math.abs(conversation.top - form.top) }
|
||||
return conversation && cardPane
|
||||
? {
|
||||
conversationHeight: conversation.height,
|
||||
cardHeight: cardPane.height,
|
||||
topGap: Math.abs(conversation.top - cardPane.top),
|
||||
conversationWidth: conversation.width,
|
||||
cardWidth: cardPane.width,
|
||||
}
|
||||
: null;
|
||||
});
|
||||
expect(layout).not.toBeNull();
|
||||
expect(layout!.conversationHeight).toBeGreaterThan(300);
|
||||
expect(Math.abs(layout!.conversationHeight - layout!.formHeight)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(layout!.conversationHeight - layout!.cardHeight)).toBeLessThanOrEqual(1);
|
||||
expect(layout!.topGap).toBeLessThanOrEqual(1);
|
||||
expect(layout!.conversationWidth).toBeGreaterThan(layout!.cardWidth);
|
||||
|
||||
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 desktopViewport = page.viewportSize();
|
||||
await page.setViewportSize({ width: 375, height: 800 });
|
||||
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeHidden();
|
||||
await page.getByRole('button', { name: /^我的创作/ }).click();
|
||||
await expect(page.getByTestId('youth-creation-card')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.setViewportSize({ width: 768, height: 800 });
|
||||
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeHidden();
|
||||
|
||||
await page.setViewportSize(desktopViewport ?? { width: 1280, height: 800 });
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '精细调整' }).click();
|
||||
await page.getByRole('button', { name: '电脑横屏' }).click();
|
||||
await page.getByRole('button', { name: '完成调整' }).click();
|
||||
await expect(page.getByTestId('youth-creation-card').getByText('电脑横屏')).toBeVisible();
|
||||
|
||||
const followUp = '主体更克制,留出品牌标题空间';
|
||||
await page.getByLabel('给设计 Agent 留言').fill(followUp);
|
||||
await page.getByLabel('给设计 Agent 留言').press('Enter');
|
||||
await page.getByLabel('告诉 AI 你想创作什么').fill(followUp);
|
||||
await page.getByLabel('告诉 AI 你想创作什么').press('Enter');
|
||||
await expect(page.getByTestId('image-workspace-conversation')).toContainText(followUp);
|
||||
await expect(page.getByTestId('image-workspace-conversation'))
|
||||
.toContainText('已把这条补充写入同一份设计规格。');
|
||||
.toContainText('记住了:主体更简洁,也会为标题留出空间。');
|
||||
|
||||
await page.getByRole('button', { name: '编译生成方案' }).click();
|
||||
await expect(page.getByText('报价已冻结')).toBeVisible();
|
||||
await expect(page.getByText('对应规格版本 5,确认后不会再读取当前表单的新修改。')).toBeVisible();
|
||||
await expect(page.getByText('确认即按此不可变报价创建任务,不会在客户端拼接或改写提示词。')).toBeVisible();
|
||||
await page.getByRole('button', { name: '看看制作方案' }).click();
|
||||
await expect(page.getByRole('heading', { name: '制作方案准备好了' })).toBeVisible();
|
||||
await expect(page.getByText('开始后会按现在的想法制作;如果再改想法,需要重新准备方案。')).toBeVisible();
|
||||
await expect(page.getByLabel(/Prompt|提示词/i)).toHaveCount(0);
|
||||
await expect(page.getByText(/规格版本|编译器|生成方式|不可变/)).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 page.getByRole('button', { name: '花 12 设计点开始制作' }).click();
|
||||
await expect(page.getByText('作品进度')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '我的图片', exact: true })).toBeVisible();
|
||||
await expect(page.getByText('正在准备', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('正在准备画面').first()).toBeVisible();
|
||||
await expect(page.getByText('1200', { exact: true })).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
|
||||
@@ -63,6 +63,7 @@ async function installCodingFirstChatHost(
|
||||
captured: CapturedRequest[];
|
||||
conversationCreated: boolean;
|
||||
interactionAnswered: boolean;
|
||||
abortRequested: boolean;
|
||||
releaseSnapshot: (() => void) | null;
|
||||
snapshotPending: boolean;
|
||||
};
|
||||
@@ -73,6 +74,7 @@ async function installCodingFirstChatHost(
|
||||
captured: [],
|
||||
conversationCreated: false,
|
||||
interactionAnswered: false,
|
||||
abortRequested: false,
|
||||
releaseSnapshot: null,
|
||||
snapshotPending: false,
|
||||
};
|
||||
@@ -509,9 +511,44 @@ async function installCodingFirstChatHost(
|
||||
state.snapshotPending = false;
|
||||
}
|
||||
return respond({
|
||||
snapshot: state.interactionAnswered
|
||||
? { ...snapshot, pendingInteractions: [] }
|
||||
: snapshot,
|
||||
snapshot: state.abortRequested
|
||||
? {
|
||||
...snapshot,
|
||||
nodes: snapshot.nodes.map((node) => {
|
||||
if (node.kind === 'message' && node.role === 'assistant') {
|
||||
return {
|
||||
...node,
|
||||
status: 'aborted',
|
||||
blocks: node.blocks.map((block) => ({ ...block, status: 'complete' })),
|
||||
};
|
||||
}
|
||||
if (node.kind === 'subagent') {
|
||||
return {
|
||||
...node,
|
||||
details: {
|
||||
...node.details,
|
||||
tasks: node.details.tasks.map((task) => (
|
||||
task.status === 'running' ? { ...task, status: 'aborted' } : task
|
||||
)),
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
run: {
|
||||
status: 'idle',
|
||||
runId: 'run-e2e-feature',
|
||||
mode: 'prompt',
|
||||
settledAt: 12_000,
|
||||
terminalReason: 'aborted',
|
||||
},
|
||||
queue: { items: [] },
|
||||
pendingInteractions: [],
|
||||
cursor: { workerGeneration: 1, seq: 1 },
|
||||
}
|
||||
: state.interactionAnswered
|
||||
? { ...snapshot, pendingInteractions: [] }
|
||||
: snapshot,
|
||||
});
|
||||
}
|
||||
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
|
||||
@@ -531,7 +568,11 @@ async function installCodingFirstChatHost(
|
||||
},
|
||||
}, 202);
|
||||
}
|
||||
if (/^\/api\/coding\/conversations\/[^/]+\/(abort|compact|recover)$/.test(path) && method === 'POST') {
|
||||
if (/^\/api\/coding\/conversations\/[^/]+\/abort$/.test(path) && method === 'POST') {
|
||||
state.abortRequested = true;
|
||||
return respond({});
|
||||
}
|
||||
if (/^\/api\/coding\/conversations\/[^/]+\/(compact|recover)$/.test(path) && method === 'POST') {
|
||||
return respond({});
|
||||
}
|
||||
if (/^\/api\/coding\/conversations\/[^/]+\/model$/.test(path) && method === 'POST') {
|
||||
@@ -712,9 +753,15 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
]);
|
||||
expect(titlebarBounds.every((bounds) => bounds?.y === 0 && bounds.height === 40)).toBe(true);
|
||||
const logoBounds = await page.getByTestId('titlebar-logo').boundingBox();
|
||||
const windowWidth = await page.evaluate(() => window.innerWidth);
|
||||
expect(logoBounds).not.toBeNull();
|
||||
expect(Math.abs((logoBounds!.x + logoBounds!.width) - windowWidth)).toBeLessThanOrEqual(1);
|
||||
if (await page.evaluate(() => window.electron.platform === 'win32')) {
|
||||
const minimizeBounds = await page.getByTitle('Minimize').boundingBox();
|
||||
expect(minimizeBounds).not.toBeNull();
|
||||
expect(logoBounds!.x + logoBounds!.width).toBeLessThanOrEqual(minimizeBounds!.x);
|
||||
} else {
|
||||
const windowWidth = await page.evaluate(() => window.innerWidth);
|
||||
expect(Math.abs((logoBounds!.x + logoBounds!.width) - windowWidth)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
const conversationHeader = page.getByTestId('coding-conversation-header');
|
||||
await expect(conversationHeader).toContainText('新对话');
|
||||
await expect(conversationHeader).not.toContainText('Pi ·');
|
||||
@@ -837,6 +884,9 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(runtimeSettings).not.toHaveClass(/bg-surface-subtle\/75/);
|
||||
await expect(runtimeSettings).toBeDisabled();
|
||||
await page.getByRole('button', { name: '中止', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '中止', exact: true })).toHaveCount(0);
|
||||
await expect(composer.getByRole('button', { name: '中止生成' })).toHaveCount(0);
|
||||
await expect(runtimeSettings).toBeEnabled();
|
||||
|
||||
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
|
||||
await expect(builderConversations).toBeVisible();
|
||||
@@ -848,6 +898,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(page.getByText('本地编程运行时暂时不可用')).toHaveCount(0);
|
||||
await builderConversations.getByRole('button', { name: /^新对话/ }).click();
|
||||
await expect(page.getByText('Durable user fork source')).toBeVisible();
|
||||
await page.getByTestId('coding-process-group').locator('summary').first().click();
|
||||
await expect(page.getByText('Durable assistant response')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '从这里创建新对话分支' })).toHaveCount(1);
|
||||
await builderConversations.getByRole('button', { name: 'Second Conversation' }).click();
|
||||
|
||||
20
tests/e2e/windows-titlebar.spec.ts
Normal file
20
tests/e2e/windows-titlebar.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test('keeps the Canvas logo clear of Windows window controls', async ({ launchElectronApp }) => {
|
||||
test.skip(process.platform !== 'win32', 'Windows custom titlebar only');
|
||||
const app = await launchElectronApp({ 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 logoBounds = await page.getByTestId('titlebar-logo').boundingBox();
|
||||
const minimizeBounds = await page.getByTitle('Minimize').boundingBox();
|
||||
expect(logoBounds).not.toBeNull();
|
||||
expect(minimizeBounds).not.toBeNull();
|
||||
expect(logoBounds!.x + logoBounds!.width).toBeLessThanOrEqual(minimizeBounds!.x);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user