merge: integrate upstream main with local Makelore changes

This commit is contained in:
inman
2026-08-31 10:55:48 +08:00
128 changed files with 8278 additions and 3030 deletions

View File

@@ -49,6 +49,19 @@ test.describe('Makelore Electron smoke flows', () => {
await expect(page.getByRole('button', { name: '在浏览器中继续', exact: true })).toHaveCount(0);
});
test('redirects a signed-out direct workspace route to login', async ({ page }) => {
await expect(page.getByTestId('setup-page')).toBeVisible();
await page.getByTestId('setup-skip-button').click();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.evaluate(() => {
window.location.hash = '#/chat';
});
await expect(page.getByRole('tab', { name: '密码登录', exact: true })).toBeVisible();
await expect(page.getByTestId('main-layout')).toHaveCount(0);
});
test('persists skipped setup across relaunch for the same isolated profile', async ({ electronApp, launchElectronApp }) => {
const firstWindow = await electronApp.firstWindow();
await firstWindow.waitForLoadState('domcontentloaded');

View File

@@ -6,7 +6,7 @@ import { closeElectronApp, expect, getStableWindow, test } from './fixtures/elec
const BOUND_PROJECT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
async function readProjectConfig(projectPath: string): Promise<Record<string, unknown>> {
return JSON.parse(await readFile(path.join(projectPath, '.niancode', 'project.json'), 'utf8')) as Record<string, unknown>;
return JSON.parse(await readFile(path.join(projectPath, '.makelore', 'project.json'), 'utf8')) as Record<string, unknown>;
}
async function selectProgrammingProjectFolder(
@@ -76,7 +76,7 @@ test.describe('Coding project identity UX', () => {
const legacyConfig = await readProjectConfig(projectPath);
delete legacyConfig.projectId;
await writeFile(path.join(projectPath, '.niancode', 'project.json'), JSON.stringify(legacyConfig, null, 2));
await writeFile(path.join(projectPath, '.makelore', 'project.json'), JSON.stringify(legacyConfig, null, 2));
await page.reload();
await expect(page.getByTestId('legacy-project-identity-card')).toBeVisible();

View File

@@ -148,6 +148,7 @@ test.describe('Makelore module navigation without setup flow', () => {
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
await expect(page.getByRole('img', { name: 'Makelore logo' })).toBeVisible();
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('编程 Code');
await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible();
await expect(page.getByTestId('sidebar-create-project')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-project-config')).toHaveCount(0);

View File

@@ -62,6 +62,7 @@ async function installCodingFirstChatHost(
type MainState = {
captured: CapturedRequest[];
conversationCreated: boolean;
interactionAnswered: boolean;
releaseSnapshot: (() => void) | null;
snapshotPending: boolean;
};
@@ -71,6 +72,7 @@ async function installCodingFirstChatHost(
const state: MainState = {
captured: [],
conversationCreated: false,
interactionAnswered: false,
releaseSnapshot: null,
snapshotPending: false,
};
@@ -117,7 +119,6 @@ async function installCodingFirstChatHost(
initialized: true,
agents: [agent],
knowledgeDirectory: 'knowledge',
legacyConversationNotice: featureComplete ? 'pending' : 'none',
createdAt: now,
updatedAt: now,
};
@@ -165,8 +166,20 @@ async function installCodingFirstChatHost(
id: 'message-assistant-e2e',
sourceEntryId: 'entry-assistant-e2e',
role: 'assistant',
status: 'complete',
blocks: [{ kind: 'text', id: 'message-assistant-e2e:content:0', text: 'Durable assistant response', status: 'complete' }],
status: 'streaming',
blocks: [
{
kind: 'thinking',
id: 'message-assistant-e2e:thinking:0',
text: [
'Inspecting the project before responding.',
'Checking the runtime boundary and current configuration.',
'Preparing the smallest safe next step.',
].join('\n\n'),
status: 'streaming',
},
{ kind: 'text', id: 'message-assistant-e2e:content:0', text: 'Durable assistant response', status: 'complete' },
],
},
{
kind: 'subagent',
@@ -199,9 +212,25 @@ async function installCodingFirstChatHost(
title: '浏览器状态',
inputText: 'status',
status: 'complete',
output: [{ kind: 'text', id: 'browser-output-e2e', text: 'Browser ready', status: 'complete' }],
output: [{
kind: 'text',
id: 'browser-output-e2e',
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(5)}Browser ready`,
status: 'complete',
}],
details: { schema: 'agent-browser.v1', action: 'status' },
},
{
kind: 'tool',
id: 'changes-tool-e2e',
toolCallId: 'changes-call-e2e',
toolName: 'changed_file',
title: '记录文件更改',
inputText: 'src/app.ts',
status: 'complete',
output: [],
details: { schema: 'changed-file.v1', paths: ['src/app.ts'] },
},
] : [],
run: featureComplete ? { status: 'running', runId: 'run-e2e-feature', mode: 'prompt' } : { status: 'idle' },
queue: { items: featureComplete ? [{ id: 'queue-e2e', clientRequestId: 'request-queued', mode: 'follow-up', text: 'Queued follow-up', attachmentIds: [] }] : [] },
@@ -212,9 +241,13 @@ async function installCodingFirstChatHost(
id: 'interaction-e2e',
conversationId: conversation.id,
runId: 'run-e2e-feature',
kind: 'confirm',
kind: 'select',
title: '允许继续?',
message: '确认当前实现方向。',
options: [
{ id: 'interaction-e2e:option:0', label: '按当前方向继续' },
{ id: 'interaction-e2e:option:1', label: '重新调整方向' },
],
status: 'pending',
}] : [],
worker: { status: 'ready', generation: 1 },
@@ -229,10 +262,89 @@ async function installCodingFirstChatHost(
model: {
model: secondConversation.model,
modelResolution: secondConversation.modelResolution,
...(featureComplete
? { availableThinkingLevels: ['off', 'low', 'medium', 'high'] }
: {}),
},
},
nodes: [],
run: { status: 'idle' },
nodes: featureComplete ? [
{
kind: 'message',
id: 'message-second-user-e2e',
role: 'user',
status: 'complete',
blocks: [{ kind: 'text', id: 'message-second-user-e2e:content:0', text: '检查第二个项目', status: 'complete' }],
},
{
kind: 'boundary',
id: 'boundary-second-start-e2e',
runId: 'run-second-e2e',
boundary: 'turn-start',
},
{
kind: 'message',
id: 'message-second-work-e2e',
role: 'assistant',
status: 'complete',
stopReason: 'tool-use',
blocks: [
{ kind: 'thinking', id: 'message-second-work-e2e:thinking:0', text: '读取配置并核对入口。', status: 'complete' },
{ kind: 'text', id: 'message-second-work-e2e:content:0', text: '我先读取配置。', status: 'complete' },
],
},
{
kind: 'tool',
id: 'tool-second-e2e',
toolCallId: 'tool-call-second-e2e',
toolName: 'agent_browser',
title: '检查网站',
inputText: 'status',
status: 'error',
output: [{ kind: 'text', id: 'tool-second-e2e:output:0', text: 'Bridge request failed', status: 'complete' }],
details: { schema: 'agent-browser.v1', action: 'status' },
},
{
kind: 'boundary',
id: 'boundary-second-end-e2e',
runId: 'run-second-e2e',
boundary: 'turn-end',
},
{
kind: 'message',
id: 'message-second-final-e2e',
role: 'assistant',
status: 'complete',
stopReason: 'stop',
blocks: [{
kind: 'text',
id: 'message-second-final-e2e:content:0',
text: [
'## 检查结论',
'',
'结论:第二个项目配置正常。',
'',
'文件位置:`~/Desktop/123/guiyang/index.html`',
'',
'查看方式:打开 `guiyang/index.html`,或访问 `http://localhost:8642/index.html`。',
'',
'| 检查项 | 状态 |',
'| --- | --- |',
'| 项目配置 | 正常 |',
'',
'[查看项目文档](https://example.com/docs)',
].join('\n'),
status: 'complete',
}],
},
] : [],
run: featureComplete ? {
status: 'idle',
runId: 'run-second-e2e',
mode: 'prompt',
startedAt: 1_000,
settledAt: 5_000,
terminalReason: 'completed',
} : { status: 'idle' },
queue: { items: [] },
pendingInteractions: [],
worker: {
@@ -338,16 +450,6 @@ async function installCodingFirstChatHost(
if (path === `/api/coding/projects/config?projectId=${project.id}`) {
return respond({ snapshot: { project, config, knowledgeFiles: [] } });
}
if (path === '/api/coding/projects/legacy-conversation-notice/acknowledge'
&& method === 'POST') {
return respond({
snapshot: {
project,
config: { ...config, legacyConversationNotice: 'acknowledged' },
knowledgeFiles: [],
},
});
}
if (path === `/api/coding/projects/conversations?projectId=${project.id}`) {
return respond({
conversations: featureComplete
@@ -367,7 +469,11 @@ async function installCodingFirstChatHost(
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
state.snapshotPending = false;
}
return respond({ snapshot });
return respond({
snapshot: state.interactionAnswered
? { ...snapshot, pendingInteractions: [] }
: snapshot,
});
}
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
return respond({ snapshot: secondSnapshot });
@@ -399,6 +505,7 @@ async function installCodingFirstChatHost(
return respond({ conversation: { ...conversation, ...(body?.title ? { title: body.title } : {}), unread: body?.unread === true, archivedAt: body?.archived === true ? now : null } });
}
if (/^\/api\/coding\/interactions\/[^/]+\/respond$/.test(path) && method === 'POST') {
state.interactionAnswered = true;
return respond({});
}
if (/^\/api\/coding\/conversations\/[^/]+\/changes$/.test(path)) {
@@ -536,7 +643,7 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
}
});
test('PI feature UI isolates Conversations and exposes queue, interaction, model, subagent, and project tools', async ({
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
@@ -555,31 +662,139 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByTestId('coding-conversation-header')).toContainText('生成中');
await expect(page.getByTestId('coding-conversation-header')).toContainText('队列 1');
await expect(page.getByText('并行子任务')).toBeVisible();
await expect(page.getByTestId('coding-conversation-sidebar-titlebar')).toBeVisible();
await expect(page.getByTestId('coding-conversation-header-titlebar')).toBeVisible();
const titlebarBounds = await Promise.all([
page.getByTestId('coding-conversation-sidebar-titlebar').boundingBox(),
page.getByTestId('coding-conversation-header-titlebar').boundingBox(),
]);
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);
const conversationHeader = page.getByTestId('coding-conversation-header');
await expect(conversationHeader).toContainText('新对话');
await expect(conversationHeader).not.toContainText('Pi ·');
await expect(conversationHeader).not.toContainText('队列 1');
await expect(page.getByTestId('coding-conversation-sidebar-titlebar')).not.toContainText('Pi 本地对话');
await expect(conversationHeader.getByRole('button', { name: '创建分支' })).toHaveCount(0);
await expect(conversationHeader.getByRole('button', { name: /标为(已读|未读)/ })).toHaveCount(0);
await expect(conversationHeader.getByRole('button', { name: '归档' })).toHaveCount(0);
await expect(conversationHeader.getByRole('button', { name: '智能体设置' })).toHaveCount(0);
await expect(conversationHeader.getByRole('button', { name: '打开编程工具' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '项目设置' })).toHaveCount(1);
await expect(page.getByTestId('coding-conversation-sidebar')).toBeVisible();
const activeProcess = page.getByTestId('coding-process-group');
await expect(activeProcess).toHaveAttribute('open', '');
await expect(activeProcess.locator('summary').first()).toContainText('处理中');
const activeThinking = page.getByLabel('思考过程');
await expect(activeThinking).toContainText('Preparing the smallest safe next step.');
await expect(activeThinking).not.toContainText('Inspecting the project before responding.');
await expect(activeThinking).toHaveAttribute('data-collapsed-lines', '1');
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
await expect(activeThinking).toHaveAttribute('data-streaming', 'true');
const activeThinkingPreview = activeThinking.getByTestId('process-progress-preview');
await expect(activeThinkingPreview).toHaveAttribute('data-progress-tail', 'true');
await expect(activeThinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
await expect(activeThinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
await expect(activeThinkingPreview).not.toHaveAttribute('data-roll-revision');
await expect(activeThinkingPreview.locator('.streaming-progress-roll')).toHaveCount(0);
await expect(activeThinkingPreview).toHaveAttribute('data-progress-shimmer', 'true');
await expect(activeThinkingPreview).toHaveCSS('text-align', 'left');
const activeShimmer = activeThinkingPreview.locator('.streaming-progress-shimmer');
await expect(activeShimmer).toBeVisible();
expect(await activeShimmer.evaluate((element) => (
getComputedStyle(element).animationName
))).toContain('makelore-progress-shimmer');
const collapsedThinkingBounds = await activeThinking.boundingBox();
expect(collapsedThinkingBounds).not.toBeNull();
expect(collapsedThinkingBounds!.height).toBeLessThanOrEqual(24);
const thinkingToggle = page.getByRole('button', { name: '展开思考详情' });
await expect(thinkingToggle).toHaveAttribute('aria-expanded', 'false');
await thinkingToggle.click();
await expect(activeThinking).toHaveAttribute('data-expanded', 'true');
await expect(activeThinking).toContainText('Inspecting the project before responding.');
await expect(activeThinking.getByTestId('assistant-markdown'))
.toHaveClass(/text-muted-foreground/);
await expect.poll(async () => (
(await activeThinking.boundingBox())?.height ?? 0
)).toBeGreaterThan(collapsedThinkingBounds!.height);
await page.getByRole('button', { name: '收起思考详情' }).click();
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
const activeCommentary = page.locator('[data-node-kind="assistant-commentary"]');
await expect(activeCommentary).toHaveCount(1);
await expect(activeCommentary).toHaveClass(/text-foreground/);
const activeCommentaryPreview = activeCommentary.getByTestId('process-progress-preview');
await expect(activeCommentaryPreview).toContainText('Durable assistant response');
await expect(activeCommentaryPreview).toHaveClass(/text-foreground/);
await expect(activeCommentaryPreview).not.toHaveClass(/text-muted-foreground/);
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-update-motion', 'none');
await expect(activeCommentaryPreview.locator('.streaming-progress-shimmer')).toHaveCount(0);
await expect(activeCommentaryPreview.locator('.streaming-progress-roll')).toHaveCount(0);
await page.getByRole('button', { name: '展开过程说明' }).click();
await expect(activeCommentary.getByTestId('assistant-markdown')).toHaveClass(/text-foreground/);
await expect(activeCommentary.getByTestId('assistant-markdown'))
.not.toHaveClass(/text-muted-foreground/);
await page.getByRole('button', { name: '收起过程说明' }).click();
await expect(page.locator('[data-node-id="subagent-e2e"] > summary')).toContainText('并行子任务');
await page.locator('[data-node-id="compaction-e2e"] > summary').click();
await expect(page.getByText('系统会自动重试。')).toBeVisible();
await page.locator('[data-node-id="browser-tool-e2e"] > summary').click();
const compactTool = page.locator('[data-node-id="browser-tool-e2e"]');
const compactToolSummary = compactTool.locator('> summary');
await expect(compactTool).not.toHaveAttribute('open', '');
await expect(compactToolSummary).toHaveAttribute('data-collapsed-lines', '1');
await expect(compactTool.getByTestId('tool-progress-preview')).toContainText('Browser ready');
await expect(compactTool.getByTestId('tool-progress-preview'))
.toHaveAttribute('data-progress-tail', 'true');
const toolProgressViewport = compactTool.getByTestId('tool-progress-preview');
await expect(toolProgressViewport).toHaveAttribute('data-progress-alignment', 'left');
await expect(toolProgressViewport).toHaveAttribute('data-progress-shimmer', 'false');
await expect(toolProgressViewport.locator('.streaming-progress-shimmer')).toHaveCount(0);
await expect(toolProgressViewport).toHaveCSS('text-align', 'left');
const toolProgressScroll = await toolProgressViewport.evaluate((element) => ({
clientWidth: element.clientWidth,
scrollLeft: element.scrollLeft,
scrollWidth: element.scrollWidth,
}));
expect(toolProgressScroll.scrollWidth).toBeGreaterThan(toolProgressScroll.clientWidth);
expect(Math.abs(
toolProgressScroll.scrollLeft
- (toolProgressScroll.scrollWidth - toolProgressScroll.clientWidth),
)).toBeLessThanOrEqual(2);
const compactToolBounds = await compactToolSummary.boundingBox();
expect(compactToolBounds).not.toBeNull();
expect(compactToolBounds!.height).toBeLessThanOrEqual(33);
await compactToolSummary.click();
await expect(page.getByText('浏览器操作 · status')).toBeVisible();
await expect(page.getByText('允许继续?')).toBeVisible();
await expect(page.getByTestId('legacy-conversation-notice')).toBeVisible();
await page.getByTestId('legacy-conversation-notice').getByRole('button', { name: '知道了' }).click();
const changesSummary = page.getByRole('button', { name: '查看 1 个文件的更改' });
await expect(changesSummary).toContainText('1 个文件已更改');
await expect(changesSummary).toContainText('+1');
await changesSummary.click();
await expect(page.getByText('src/app.ts')).toBeVisible();
await expect(page.getByTestId('legacy-conversation-notice')).toHaveCount(0);
await page.getByRole('button', { name: '确认', exact: true }).click();
const interactionPanel = page.getByTestId('coding-interactions');
await interactionPanel.getByRole('textbox', { name: '允许继续?的其他回答' })
.fill('先补充错误处理再继续');
await interactionPanel.getByRole('button', { name: '提交回答' }).click();
await expect(interactionPanel).toHaveCount(0);
await page.getByTestId('coding-message-composer').getByRole('textbox').fill('排到下一轮验证');
const mode = page.getByRole('combobox', { name: '消息发送方式' });
await mode.selectOption('follow-up');
await expect(mode).toHaveValue('follow-up');
const model = page.getByRole('combobox', { name: '当前对话模型' });
await model.selectOption(JSON.stringify(['account-e2e', 'model-b']));
await page.getByRole('button', { name: '中止' }).click();
await page.getByRole('button', { name: '打开编程工具' }).click();
await expect(page.getByRole('dialog')).toContainText('编程工具');
await page.getByRole('tab', { name: '命令' }).click();
await page.getByRole('button', { name: /\/review/ }).click();
await expect(page.getByRole('textbox')).toHaveValue('/review ');
const composer = page.getByTestId('coding-message-composer');
await expect(composer).not.toContainText('项目内可写');
await expect(composer.getByRole('button', { name: '整理上下文' })).toHaveCount(0);
await expect(composer.getByRole('button', { name: '语音输入' })).toBeVisible();
const runtimeSettings = composer.getByRole('button', { name: /模型与思考设置/ });
await expect(runtimeSettings).toHaveClass(/bg-transparent/);
await expect(runtimeSettings).not.toHaveClass(/bg-surface-subtle\/75/);
await expect(runtimeSettings).toBeDisabled();
await page.getByRole('button', { name: '中止', exact: true }).click();
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
await expect(builderConversations).toBeVisible();
@@ -595,32 +810,105 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
await expect(page.getByRole('button', { name: '从这里创建新对话分支' })).toHaveCount(1);
await builderConversations.getByRole('button', { name: 'Second Conversation' }).click();
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
await expect(page.getByRole('combobox', { name: '当前对话模型' })).toHaveValue(
JSON.stringify(['account-e2e', 'model-b']),
const completedProcess = page.getByTestId('coding-process-group');
await expect(completedProcess).not.toHaveAttribute('open', '');
await expect(completedProcess.locator('summary').first()).toContainText('已处理 4 秒');
await expect(completedProcess.locator('summary').first()).not.toContainText('处理失败');
await expect(page.getByRole('heading', { level: 2, name: '检查结论' })).toBeVisible();
await expect(page.getByText('结论:第二个项目配置正常。')).toBeVisible();
await expect(page.getByRole('table')).toContainText('项目配置');
await expect(page.getByRole('link', { name: '查看项目文档' }))
.toHaveAttribute('href', 'https://example.com/docs');
const localReplyLink = page.getByRole('button', {
name: '用默认浏览器打开 guiyang/index.html',
});
await expect(localReplyLink).toBeVisible();
await expect(localReplyLink).toHaveClass(/text-brand/);
await expect(localReplyLink).not.toHaveClass(/bg-/);
const localhostReplyLink = page.getByRole('link', {
name: 'http://localhost:8642/index.html',
});
await expect(localhostReplyLink)
.toHaveAttribute('href', 'http://localhost:8642/index.html');
await expect(localhostReplyLink).toHaveClass(/text-brand/);
await expect(localhostReplyLink).not.toHaveClass(/bg-/);
await expect(page.getByText('开始新一轮')).toHaveCount(0);
await expect(page.getByText('本轮已结算')).toHaveCount(0);
await completedProcess.locator('summary').first().click();
await expect(page.getByLabel('思考过程')).toContainText('读取配置并核对入口。');
const completedCommentary = page.locator(
'[data-node-id="message-second-work-e2e"][data-node-kind="assistant-commentary"]',
);
await expect(completedCommentary.getByTestId('process-progress-preview'))
.toContainText('我先读取配置。');
await expect(completedCommentary.getByTestId('process-progress-preview'))
.toHaveClass(/text-foreground/);
await expect(completedCommentary.getByTestId('process-progress-preview'))
.not.toHaveClass(/text-muted-foreground/);
await expect(page.locator('[data-node-id="tool-second-e2e"] > summary')).toContainText('失败');
const secondRuntimeSettings = composer.getByRole('button', { name: /模型与思考设置/ });
await expect(secondRuntimeSettings).toBeEnabled();
await expect(secondRuntimeSettings).toContainText('model-b');
await secondRuntimeSettings.click();
await page.getByRole('menuitem', { name: '推理强度 低' }).click();
await expect(page.getByRole('menuitemradio', { name: '关闭' })).toBeVisible();
await expect(page.getByRole('menuitemradio', { name: '低' })).toBeVisible();
await expect(page.getByRole('menuitemradio', { name: '中等' })).toBeVisible();
await expect(page.getByRole('menuitemradio', { name: '高' })).toBeVisible();
await expect(page.getByRole('menuitemradio', { name: '极简' })).toHaveCount(0);
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
await page.evaluate(() => {
const trackedWindow = window as typeof window & {
__makeloreReconnectObserver?: MutationObserver;
__makeloreSawReconnectBanner?: boolean;
};
trackedWindow.__makeloreSawReconnectBanner = false;
trackedWindow.__makeloreReconnectObserver = new MutationObserver(() => {
if (document.body.textContent?.includes('正在重新连接本地 Agent')) {
trackedWindow.__makeloreSawReconnectBanner = true;
}
});
trackedWindow.__makeloreReconnectObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
});
});
await secondRuntimeSettings.click();
await page.getByRole('menuitem', { name: '模型 model-b' }).click();
await page.getByRole('menuitemradio', { name: 'model-a' }).click();
await expect(secondRuntimeSettings).toContainText('model-b');
expect(await page.evaluate(() => {
const trackedWindow = window as typeof window & {
__makeloreReconnectObserver?: MutationObserver;
__makeloreSawReconnectBanner?: boolean;
};
trackedWindow.__makeloreReconnectObserver?.disconnect();
return trackedWindow.__makeloreSawReconnectBanner ?? false;
})).toBe(false);
await expect(page.getByRole('textbox')).toHaveValue('');
await page.getByRole('button', { name: '恢复' }).click();
await page.getByRole('button', { name: '整理上下文' }).click();
await page.getByRole('button', { name: '打开编程工具' }).click();
await page.getByRole('tab', { name: '技能' }).click();
await expect(page.getByRole('dialog')).toContainText('Research');
await expect(page.getByText(/分享|取消分享|回滚|恢复回滚|待办|全局运行时/)).toHaveCount(0);
await expect(page.getByText(/编程工具|分享|取消分享|回滚|恢复回滚|待办|全局运行时/)).toHaveCount(0);
const state = await readState(electronApp);
expect(state.captured.some((request) => request.path.endsWith('/model') && request.method === 'POST')).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/abort') && request.method === 'POST')).toBe(true);
expect(state.captured.some((request) => request.path.includes('/interactions/') && request.path.endsWith('/respond'))).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/commands'))).toBe(true);
expect(state.captured.some((request) => (
request.path.includes('/interactions/')
&& request.path.endsWith('/respond')
&& request.body?.value === '先补充错误处理再继续'
))).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/commands'))).toBe(false);
expect(state.captured.some((request) => request.path.endsWith('/changes'))).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/recover') && request.method === 'POST')).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/compact') && request.method === 'POST')).toBe(true);
expect(state.captured.some((request) => request.path.endsWith('/compact') && request.method === 'POST')).toBe(false);
expect(state.captured.some((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/fork'
&& request.method === 'POST'
&& request.body?.sourceEntryId === 'entry-user-e2e'
))).toBe(true);
expect(state.captured.some((request) => request.path.includes('legacy-conversation-notice'))).toBe(true);
expect(state.captured.some((request) => request.path.includes('legacy-conversation-notice'))).toBe(false);
expect(state.captured.every((request) => !request.path.includes('/api/opencode/share'))).toBe(true);
} finally {
await releaseSnapshot(electronApp);

View File

@@ -6,7 +6,7 @@ import { createCodingProjectConfigV2 } from '../../electron/coding-projects/proj
test.describe('Project configuration skills', () => {
test('does not expose the removed Superpowers feature and supports Skill details', async ({ launchElectronApp }) => {
const parentPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-configuration-e2e-'));
const parentPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-configuration-e2e-'));
const app = await launchElectronApp({ skipSetup: true });
try {
@@ -33,16 +33,16 @@ test.describe('Project configuration skills', () => {
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await expect(page.getByText(/新项目还没有伙伴/)).toHaveCount(0);
await expect(page.getByText(/新项目还没有智能体/)).toHaveCount(0);
await expect(page.getByTestId('project-configuration-actions')).toBeVisible();
const actionBarBottomGap = await page.getByTestId('project-configuration-actions').evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom);
expect(actionBarBottomGap).toBeLessThan(40);
await expect(page.getByRole('button', { name: '创建伙伴' })).toBeVisible();
await expect(page.getByRole('button', { name: '创建智能体' })).toBeVisible();
for (const [resourceId, title] of [
['resource-card-models', '大脑(大语言模型)'],
['resource-card-knowledge', '笔记本(知识库)'],
['resource-card-skills', '工具箱(技能)'],
['resource-card-models', '可用模型'],
['resource-card-knowledge', '知识文件'],
['resource-card-skills', '可用技能'],
] as const) {
await page.getByTestId(resourceId).click();
const drawer = page.getByRole('dialog', { name: title });
@@ -74,21 +74,21 @@ test.describe('Project configuration skills', () => {
}
const now = new Date().toISOString();
await mkdir(path.join(parentPath, '.niancode'), { recursive: true });
await writeFile(path.join(parentPath, '.niancode', 'project.json'), JSON.stringify({
await mkdir(path.join(parentPath, '.makelore'), { recursive: true });
await writeFile(path.join(parentPath, '.makelore', 'project.json'), JSON.stringify({
...createCodingProjectConfigV2(now, 'mini_game'),
initialized: true,
agents: [{
id: 'e2e-partner',
id: 'e2e-agent',
avatarId: 'avatar-01',
roleName: '项目伙伴',
name: 'E2E伙伴',
roleName: '项目智能体',
name: 'E2E智能体',
builtIn: false,
enabled: true,
model: { accountId: 'moonshot', modelId: 'kimi-k2.6', thinkingLevel: 'off' },
modelResolution: 'resolved',
skillIds: [],
responsibility: { mission: '验证伙伴维护弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] },
responsibility: { mission: '验证智能体编辑弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] },
prompt: '',
archivedAt: null,
pinned: false,
@@ -152,16 +152,16 @@ test.describe('Project configuration skills', () => {
await expect(publishDialog.getByText('重新选择')).toBeVisible();
await publishDialog.getByRole('button', { name: '取消' }).click();
const agentCard = page.getByTestId('project-agent-e2e-partner');
await expect(agentCard).toContainText('E2E伙伴');
const agentCard = page.getByTestId('project-agent-e2e-agent');
await expect(agentCard).toContainText('E2E智能体');
await agentCard.click();
const maintenanceDialog = page.getByRole('dialog', { name: 'E2E伙伴 · 伙伴维护' });
const maintenanceDialog = page.getByRole('dialog', { name: 'E2E智能体 · 编辑智能体' });
await expect(maintenanceDialog).toBeVisible();
await expect(maintenanceDialog.getByLabel(/伙伴名称/)).toHaveValue('E2E伙伴');
await expect(maintenanceDialog.getByLabel(/智能体名称/)).toHaveValue('E2E智能体');
await maintenanceDialog.getByRole('button', { name: '更换头像' }).click();
const avatarDialog = page.getByRole('dialog', { name: '选择头像' });
await expect(avatarDialog).toBeVisible();
await avatarDialog.getByLabel('上传伙伴头像').setInputFiles(path.join(process.cwd(), 'src/assets/agent-avatars/avatar-01.png'));
await avatarDialog.getByLabel('上传智能体头像').setInputFiles(path.join(process.cwd(), 'src/assets/agent-avatars/avatar-01.png'));
await expect(avatarDialog).toHaveCount(0);
await expect(maintenanceDialog.getByAltText('当前上传头像')).toBeVisible();
await expect(page.getByText('完成配置')).toHaveCount(0);