Files
makelore/tests/e2e/pi-coding-first-chat.spec.ts

1589 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ElectronApplication, Page } from 'playwright-core';
import { appendFile } from 'node:fs/promises';
import { expect, getStableWindow, test } from './fixtures/electron';
type CapturedRequest = {
path: string;
method: string;
body?: Record<string, unknown>;
byteLength?: number;
contentType?: string;
at: number;
};
type HostConnection = {
baseUrl: string;
token: string;
};
async function disableCodingEventSource(page: Page): Promise<void> {
await page.addInitScript(() => {
const sources = new Set<LocalEventSource>();
type TrackedWindow = Window & {
__makeloreCodingEventSources?: LocalEventSource[];
};
const trackedWindow = window as TrackedWindow;
trackedWindow.__makeloreCodingEventSources = [];
class LocalEventSource extends EventTarget {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
readonly CONNECTING = LocalEventSource.CONNECTING;
readonly OPEN = LocalEventSource.OPEN;
readonly CLOSED = LocalEventSource.CLOSED;
readonly url: string;
readonly withCredentials = false;
readyState = LocalEventSource.OPEN;
onopen: ((event: Event) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
constructor(url: string) {
super();
this.url = url;
sources.add(this);
trackedWindow.__makeloreCodingEventSources?.push(this);
queueMicrotask(() => this.onopen?.(new Event('open')));
}
close(): void {
this.readyState = LocalEventSource.CLOSED;
sources.delete(this);
}
}
Object.defineProperty(window, 'EventSource', {
configurable: true,
writable: true,
value: LocalEventSource,
});
Object.defineProperty(window, '__makeloreEmitCodingEvent', {
configurable: true,
value(type: string, payload: unknown) {
for (const source of sources) {
source.dispatchEvent(new MessageEvent(type, { data: JSON.stringify(payload) }));
}
},
});
});
}
async function emitCodingEvent(page: Page, type: string, payload: unknown): Promise<void> {
await page.evaluate(({ eventType, eventPayload }) => {
const testWindow = window as typeof window & {
__makeloreEmitCodingEvent?: (type: string, payload: unknown) => void;
};
testWindow.__makeloreEmitCodingEvent?.(eventType, eventPayload);
}, { eventType: type, eventPayload: payload });
}
async function installCodingFirstChatHost(
electronApp: ElectronApplication,
hostConnection: HostConnection,
featureComplete = false,
managedCapabilities = false,
): Promise<void> {
await electronApp.evaluate(async (_, payload) => {
const { connection, featureComplete, managedCapabilities } = payload;
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
type MainState = {
captured: CapturedRequest[];
conversationCreated: boolean;
interactionAnswered: boolean;
abortRequested: boolean;
releaseSnapshot: (() => void) | null;
snapshotPending: boolean;
snapshotSettled: boolean;
};
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: MainState;
};
const state: MainState = {
captured: [],
conversationCreated: false,
interactionAnswered: false,
abortRequested: false,
releaseSnapshot: null,
snapshotPending: false,
snapshotSettled: false,
};
mainGlobal.__makelorePiFirstChatE2E = state;
const now = '2026-08-24T00:00:00.000Z';
const metadata = new Map<string, Record<string, unknown>>();
const project = {
id: 'project-pi-first-chat',
name: 'PI first chat',
createdAt: now,
updatedAt: now,
lastOpenedAt: now,
};
const configuredModel = {
accountId: managedCapabilities ? 'niancode-user-models' : 'account-e2e',
modelId: 'model-a',
thinkingLevel: 'off',
};
const agent = {
id: 'builder',
avatarId: 'avatar-01',
roleName: '实现者',
name: 'Builder',
builtIn: false,
enabled: true,
model: configuredModel,
modelResolution: 'resolved',
skillIds: [],
responsibility: {
mission: 'Implement',
owns: [],
boundaries: [],
collaborators: [],
principles: [],
},
prompt: '',
archivedAt: null,
pinned: true,
createdAt: now,
updatedAt: now,
};
const config = {
schemaVersion: 2,
projectType: 'custom',
initialized: true,
agents: [agent],
knowledgeDirectory: 'knowledge',
createdAt: now,
updatedAt: now,
};
const conversation = {
id: 'conversation-pi-first-chat',
agentId: agent.id,
title: '新对话',
model: configuredModel,
modelResolution: 'resolved',
archivedAt: null,
unread: false,
createdAt: now,
updatedAt: now,
};
const secondConversation = {
...conversation,
id: 'conversation-pi-second',
title: 'Second Conversation',
updatedAt: '2026-08-23T23:59:00.000Z',
model: featureComplete
? { accountId: 'account-e2e', modelId: 'model-b', thinkingLevel: 'low' }
: null,
modelResolution: featureComplete ? 'resolved' : 'required',
};
const historyConversation = {
...conversation,
id: 'conversation-pi-history',
title: 'History and quota',
updatedAt: '2026-08-23T23:58:00.000Z',
};
const snapshot = {
schemaVersion: 1,
conversation: {
id: conversation.id,
projectId: project.id,
agentId: agent.id,
title: conversation.title,
model: { model: configuredModel, modelResolution: 'resolved' },
},
nodes: featureComplete ? [
{
kind: 'message',
id: 'message-user-e2e',
sourceEntryId: 'entry-user-e2e',
role: 'user',
status: 'complete',
blocks: [{ kind: 'text', id: 'message-user-e2e:content:0', text: 'Durable user fork source', status: 'complete' }],
},
{
kind: 'message',
id: 'message-assistant-e2e',
sourceEntryId: 'entry-assistant-e2e',
role: 'assistant',
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',
id: 'subagent-e2e',
runId: 'run-e2e-feature',
details: {
schema: 'subagent.v1',
dispatchId: 'dispatch-e2e',
mode: 'parallel',
tasks: [
{ taskId: 'task-reader', agentId: 'reader', toolProfile: 'read-only', status: 'complete', summary: 'Read complete' },
{ taskId: 'task-builder', agentId: 'builder', toolProfile: 'coding', status: 'running' },
],
},
},
{
kind: 'compaction',
id: 'compaction-e2e',
runId: 'run-e2e-feature',
source: 'automatic',
status: 'error',
willRetry: true,
summary: '保留了本轮关键上下文。',
},
{
kind: 'tool',
id: 'browser-tool-e2e',
toolCallId: 'browser-call-e2e',
toolName: 'agent_browser',
title: '浏览器状态',
inputText: 'status',
status: 'complete',
output: [{
kind: 'text',
id: 'browser-output-e2e',
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(8)}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: [] }] : [] },
context: featureComplete
? { usedTokens: 256, contextWindow: 4096, compaction: 'idle' }
: { usedTokens: 0, contextWindow: 0, compaction: 'idle' },
pendingInteractions: featureComplete ? [{
id: 'interaction-e2e',
conversationId: conversation.id,
runId: 'run-e2e-feature',
kind: 'select',
title: '允许继续?',
message: '确认当前实现方向。',
options: [
{ id: 'interaction-e2e:option:0', label: '按当前方向继续' },
{ id: 'interaction-e2e:option:1', label: '重新调整方向' },
],
status: 'pending',
}] : [],
worker: { status: 'ready', generation: 1 },
cursor: { workerGeneration: 1, seq: 0 },
};
const secondSnapshot = {
...snapshot,
conversation: {
...snapshot.conversation,
id: secondConversation.id,
title: secondConversation.title,
model: {
model: secondConversation.model,
modelResolution: secondConversation.modelResolution,
...(featureComplete
? { availableThinkingLevels: ['off', 'low', 'medium', 'high'] }
: {}),
},
},
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: {
status: 'error',
generation: 1,
error: { code: 'CODING_RUNTIME_START_FAILED', message: 'Worker stopped', recoverable: true },
},
};
const historySnapshot = {
...snapshot,
conversation: {
...snapshot.conversation,
id: historyConversation.id,
title: historyConversation.title,
},
nodes: [
...Array.from({ length: 150 }, (_, index) => ({
kind: 'message',
id: `history-message-${index}`,
role: 'user',
status: 'complete',
blocks: [{
kind: 'text',
id: `history-message-${index}:content:0`,
text: `History message ${index}`,
status: 'complete',
}],
})),
{
kind: 'notice',
id: 'history-quota-notice',
code: 'CODING_PROVIDER_QUOTA_EXHAUSTED',
level: 'error',
message: '词元点数余额不足,请充值后重试。',
},
],
run: { status: 'idle' },
queue: { items: [] },
pendingInteractions: [],
worker: { status: 'ready', generation: 1 },
};
const respond = (json: unknown, status = 200) => ({
ok: true,
data: { status, ok: status >= 200 && status < 300, json },
});
const browserSnapshot = (overrides: Record<string, unknown> = {}) => ({
browserId: null,
projectId: project.id,
projectPath: null,
state: 'closed',
generation: 0,
url: '',
title: '',
visible: false,
bounds: null,
canGoBack: false,
canGoForward: false,
eventCursor: 0,
...overrides,
});
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (
_event,
request: {
path?: string;
method?: string;
headers?: Record<string, string>;
body?: unknown;
},
) => {
const path = request.path ?? '';
const method = request.method ?? 'GET';
const body = typeof request.body === 'string' && request.body
? JSON.parse(request.body) as Record<string, unknown>
: undefined;
const binaryBody = request.body instanceof ArrayBuffer
? new Uint8Array(request.body)
: ArrayBuffer.isView(request.body)
? new Uint8Array(
request.body.buffer,
request.body.byteOffset,
request.body.byteLength,
)
: undefined;
state.captured.push({
path,
method,
...(body ? { body } : {}),
...(binaryBody ? { byteLength: binaryBody.byteLength } : {}),
...(request.headers?.['Content-Type']
? { contentType: request.headers['Content-Type'] }
: {}),
at: Date.now(),
});
if (path === '/api/coding/attachments'
|| /^\/api\/coding\/attachments\/[^/]+\/content$/.test(path)) {
const response = await fetch(`${connection.baseUrl}${path}`, {
method,
headers: {
Authorization: `Bearer ${connection.token}`,
...(request.headers ?? {}),
},
...(binaryBody ? { body: binaryBody } : {}),
});
const responseContentType = response.headers.get('content-type') ?? '';
if (responseContentType.includes('application/json')) {
return {
ok: true,
data: {
status: response.status,
ok: response.ok,
json: await response.json(),
transport: 'loopback',
},
};
}
return {
ok: true,
data: {
status: response.status,
ok: response.ok,
bytes: new Uint8Array(await response.arrayBuffer()),
contentType: responseContentType.split(';', 1)[0]?.trim(),
transport: 'loopback',
},
};
}
if (path === '/api/coding/projects') {
return respond({ projects: [project], activeProjectId: project.id });
}
if (path === '/api/provider-accounts') {
return respond([{
id: managedCapabilities ? 'niancode-user-models' : 'account-e2e',
vendorId: 'custom',
label: 'E2E account',
authMode: 'api_key',
model: 'model-a',
fallbackModels: ['model-b'],
...(managedCapabilities ? { metadata: { worksSquareModelCapabilitiesV2: {
schemaVersion: 2, fetchedAt: now, refreshStatus: 'fresh', models: {
'model-a': { inputModalities: ['text'], outputModalities: ['text'],
reasoning: { supported: true, canDisable: true, defaultEnabled: true,
effortValues: ['xhigh', 'medium', 'low'], defaultEffort: 'xhigh', controlFormat: 'qwen', budget: null },
limits: null, resolutionStatus: 'ready', issues: [], updatedAt: now },
},
} } } : {}),
enabled: true,
isDefault: true,
createdAt: now,
updatedAt: now,
}]);
}
if (path === '/api/provider-accounts/key-info') return respond([]);
if (path === '/api/provider-vendors') return respond([{ id: 'custom', name: 'Custom' }]);
if (path === '/api/provider-accounts/default') return respond({ accountId: 'account-e2e' });
if (path === `/api/coding/projects/config?projectId=${project.id}`) {
return respond({ snapshot: { project, config, knowledgeFiles: [] } });
}
if (path === `/api/coding/projects/conversations?projectId=${project.id}`) {
return respond({
conversations: (featureComplete
? [conversation, secondConversation, historyConversation]
: state.conversationCreated
? [conversation]
: []).map((item) => ({ ...item, ...metadata.get(item.id) })),
});
}
if (path.startsWith('/api/agent-browser/state?')) {
return respond({ success: true, browser: browserSnapshot() });
}
if (path === '/api/agent-browser/open' && method === 'POST') {
return respond({
success: true,
browser: browserSnapshot({
browserId: 'browser-e2e',
state: 'attached',
generation: 1,
url: body?.url,
visible: true,
bounds: body?.bounds,
}),
});
}
if (path === '/api/agent-browser/present' && method === 'POST') {
return respond({
success: true,
browser: browserSnapshot({
browserId: 'browser-e2e',
state: 'attached',
generation: 1,
url: 'http://127.0.0.1:4173/',
visible: body?.visible === true,
bounds: body?.bounds ?? null,
}),
});
}
if (path === '/api/agent-browser/diagnostics' && method === 'POST') {
return respond({ success: true, browser: browserSnapshot() });
}
if (path === '/api/agent-browser/cdp/events' && method === 'POST') {
return respond({
success: true,
page: { events: [], nextCursor: 0, hasMore: false },
});
}
if (path === '/api/agent-browser/close' && method === 'POST') {
return respond({ success: true, browser: browserSnapshot() });
}
if (path === '/api/coding/projects/conversations' && method === 'POST') {
state.conversationCreated = true;
return respond({ conversation }, 201);
}
if (path === `/api/coding/conversations/${conversation.id}/snapshot`) {
if (!featureComplete) {
state.snapshotPending = true;
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
state.snapshotPending = false;
}
const posted = state.captured.find((request) => (
request.path === `/api/coding/conversations/${conversation.id}/prompt` && request.method === 'POST'
));
if (!featureComplete && posted?.body) {
const refs = posted.body.attachments as Array<{ attachmentId: string }>;
return respond({ snapshot: {
...snapshot,
nodes: [{ kind: 'message', id: 'entry:sent-image', sourceEntryId: 'sent-image',
role: 'user', status: 'complete', blocks: [
{ kind: 'text', id: 'entry:sent-image:content:0', text: posted.body.text, status: 'complete' },
...refs.map(({ attachmentId }, index) => ({
kind: 'image', id: `entry:sent-image:image:${index}`, attachmentId, mime: 'image/png',
})),
] }],
cursor: { workerGeneration: 1, seq: 1 },
} });
}
const currentSnapshot = state.snapshotSettled
? {
...snapshot,
run: {
status: 'idle',
runId: 'run-e2e-feature',
mode: 'prompt',
startedAt: 1_000,
settledAt: 2_000,
terminalReason: 'aborted',
},
queue: { items: [] },
pendingInteractions: [],
worker: { status: 'stopped', generation: 1 },
cursor: { workerGeneration: 1, seq: 1 },
}
: snapshot;
return respond({
snapshot: state.abortRequested
? {
...currentSnapshot,
nodes: currentSnapshot.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
? { ...currentSnapshot, pendingInteractions: [] }
: currentSnapshot,
});
}
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
return respond({ snapshot: secondSnapshot });
}
if (path === `/api/coding/conversations/${historyConversation.id}/snapshot`) {
return respond({ snapshot: historySnapshot });
}
if (path === `/api/coding/conversations/${conversation.id}/prompt` && method === 'POST') {
return respond({
acceptance: {
accepted: true,
conversationId: conversation.id,
clientRequestId: body?.clientRequestId,
runId: 'run-e2e-1',
mode: body?.mode ?? 'prompt',
},
}, 202);
}
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') {
return respond({ model: { model: body?.model, modelResolution: 'resolved' } });
}
if (/^\/api\/coding\/conversations\/[^/]+\/thinking$/.test(path) && method === 'POST') {
return respond({ model: snapshot.conversation.model });
}
if (/^\/api\/coding\/conversations\/[^/]+\/fork$/.test(path) && method === 'POST') {
return respond({ conversation: { ...conversation, id: 'conversation-forked', title: 'Feature UI branch' } }, 201);
}
if (/^\/api\/coding\/conversations\/[^/]+$/.test(path) && method === 'PATCH') {
const target = [conversation, secondConversation, historyConversation].find((item) => path.endsWith('/' + item.id)) ?? conversation;
const updated = { ...target, ...metadata.get(target.id),
...(body?.title ? { title: body.title } : {}),
...(typeof body?.unread === 'boolean' ? { unread: body.unread } : {}),
...(typeof body?.archived === 'boolean' ? { archivedAt: body.archived ? now : null } : {}),
};
metadata.set(target.id, updated);
return respond({ conversation: updated });
}
if (/^\/api\/coding\/interactions\/[^/]+\/respond$/.test(path) && method === 'POST') {
state.interactionAnswered = true;
return respond({});
}
if (/^\/api\/coding\/conversations\/[^/]+\/changes$/.test(path)) {
return respond({ changes: { conversationId: conversation.id, runId: 'run-e2e-feature', git: true, baselineHead: 'head-e2e', files: [{ path: 'src/app.ts', status: 'modified', diff: '+feature UI' }] } });
}
if (path === '/api/coding/files/status') return respond({ files: [{ path: 'src/app.ts', name: 'app.ts', type: 'file', status: 'modified' }] });
if (path.startsWith('/api/coding/files/content?')) return respond({ file: { path: 'src/app.ts', content: 'export const app = true;', truncated: false } });
if (path.startsWith('/api/coding/files/find?')) return respond({ files: [{ path: 'src/app.ts', name: 'app.ts', type: 'file' }] });
if (path.startsWith('/api/coding/skills')) return respond({ skills: [{ id: 'research', name: 'Research', description: 'Inspect sources', selected: true }] });
if (/^\/api\/coding\/conversations\/[^/]+\/commands$/.test(path)) return respond({ commands: [{ name: 'review', title: 'Review', description: 'Review changes', source: 'makelore' }] });
if (path === '/api/coding/runtime/diagnostics') return respond({ runtime: { revision: { provider: 1, resources: 1 }, workers: [{ conversationId: conversation.id, generation: 1, state: 'running', stage: 'running' }] } });
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
});
}, { connection: hostConnection, featureComplete, managedCapabilities });
}
async function readState(electronApp: ElectronApplication): Promise<{
captured: CapturedRequest[];
snapshotPending: boolean;
}> {
return await electronApp.evaluate(() => {
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: {
captured: CapturedRequest[];
snapshotPending: boolean;
};
};
return structuredClone({
captured: mainGlobal.__makelorePiFirstChatE2E?.captured ?? [],
snapshotPending: mainGlobal.__makelorePiFirstChatE2E?.snapshotPending ?? false,
});
});
}
async function releaseSnapshot(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(() => {
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: { releaseSnapshot: (() => void) | null };
};
mainGlobal.__makelorePiFirstChatE2E?.releaseSnapshot?.();
});
}
async function settleSnapshot(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(() => {
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: { snapshotSettled: boolean };
};
if (mainGlobal.__makelorePiFirstChatE2E) {
mainGlobal.__makelorePiFirstChatE2E.snapshotSettled = true;
}
});
}
test('conversation menus rename, archive and restore without selecting or stopping the target', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const connection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, connection, true);
await settleSnapshot(electronApp);
await disableCodingEventSource(page);
try {
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
const sidebar = page.getByTestId('coding-conversation-sidebar');
const header = page.getByTestId('coding-conversation-header');
await expect(header).toContainText('新对话');
await sidebar.getByRole('button', { name: '对话操作Second Conversation' }).focus();
await page.keyboard.press('Enter');
await page.getByRole('menuitem', { name: '重命名', exact: true }).click();
const title = page.getByRole('textbox', { name: '对话标题' });
await expect(title).toHaveValue('Second Conversation');
await title.fill('重命名后的会话');
await title.press('Enter');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(header).toContainText('新对话');
await sidebar.getByRole('button', { name: '对话操作:重命名后的会话' }).focus();
await page.keyboard.press('Enter');
await page.getByRole('menuitem', { name: '归档', exact: true }).click();
await expect(sidebar.getByRole('button', { name: '重命名后的会话', exact: true })).toHaveCount(0);
await sidebar.getByRole('button', { name: '已归档1' }).click();
await sidebar.getByRole('button', { name: '重命名后的会话', exact: true }).click();
await expect(page.getByRole('button', { name: '恢复对话', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '发送', exact: true })).toHaveCount(0);
await page.getByRole('button', { name: '恢复对话', exact: true }).click();
await expect(header).toContainText('重命名后的会话');
await expect(sidebar.getByText('最近对话', { exact: true })).toBeVisible();
const requests = (await readState(electronApp)).captured;
expect(requests.some((request) => request.path.endsWith('/abort'))).toBe(false);
expect(requests.filter((request) => request.method === 'PATCH' && request.path.endsWith('/conversation-pi-second'))
.map((request) => request.body)).toEqual([
{ title: '重命名后的会话' }, { archived: true }, { archived: false },
]);
await page.screenshot({ path: 'test-results/coding-session-controls.png' });
} finally { await releaseSnapshot(electronApp); }
});
test('managed capabilities expose native xhigh and block unsupported image input', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const connection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, connection, true, true);
await settleSnapshot(electronApp);
await disableCodingEventSource(page);
try {
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByRole('button', { name: '添加图片', exact: true })).toBeDisabled();
const controls = page.getByTestId('coding-composer-runtime-controls');
await expect(controls).toContainText('模型默认');
await controls.getByRole('button').click();
await page.getByRole('menuitem', { name: /推理强度/ }).click();
await expect(page.getByRole('menuitemradio', { name: '模型默认', exact: true })).toBeVisible();
await expect(page.getByRole('menuitemradio', { name: '关闭思考', exact: true })).toBeVisible();
await page.getByRole('menuitemradio', { name: 'xhigh', exact: true }).click();
await expect.poll(async () => (await readState(electronApp)).captured.find(
request => request.path.endsWith('/thinking') && request.method === 'POST')?.body?.reasoningChoice,
).toEqual({ mode: 'enabled', effort: 'xhigh' });
} finally { await releaseSnapshot(electronApp); }
});
test('first PI Conversation is editable under 500 ms and submits before runtime Snapshot', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection);
await disableCodingEventSource(page);
try {
await page.reload();
page = await getStableWindow(electronApp);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => {
performance.mark('pi-first-chat-start');
window.location.hash = '/chat';
});
const builderButton = page.getByRole('button', { name: 'Builder', exact: true });
await expect(builderButton).toHaveAttribute('aria-expanded', 'true');
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
await expect(builderConversations).toBeVisible();
await expect(builderConversations.getByRole('button', { name: '新对话', exact: true })).toBeVisible();
const composer = page.getByRole('textbox');
await expect(composer).toBeEnabled();
const editableMs = await page.evaluate(() => (
performance.now() - performance.getEntriesByName('pi-first-chat-start').at(-1)!.startTime
));
expect(editableMs).toBeLessThan(500);
const performanceFragment = process.env.MAKELORE_PI_PERF_COMPOSER_FRAGMENT;
if (performanceFragment) {
await appendFile(performanceFragment, `${JSON.stringify({ editableMs })}\n`);
}
const pixelPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64',
);
const largePng = Buffer.concat([pixelPng, Buffer.alloc(1024 * 1024)]);
await page.getByTestId('coding-file-attachment-input').setInputFiles({
name: 'large.png',
mimeType: 'image/png',
buffer: largePng,
});
await composer.fill('Build the first PI scene');
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
await page.getByTestId('coding-message-composer').evaluate(
(form: HTMLFormElement) => form.requestSubmit(),
);
await expect.poll(async () => {
const state = await readState(electronApp);
return {
snapshotPending: state.snapshotPending,
promptPosted: state.captured.some((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
&& request.method === 'POST'
)),
};
}).toEqual({ snapshotPending: true, promptPosted: true });
await expect(
page.getByTestId('coding-conversation-timeline').getByText('Build the first PI scene'),
).toBeVisible();
await expect(page.getByRole('img', { name: '对话图片附件' })).toBeVisible();
await expect(page.getByText('1 条消息已被本地 Agent 接收。')).toBeVisible();
const state = await readState(electronApp);
const uploads = state.captured.filter((request) => (
request.path === '/api/coding/attachments' && request.method === 'POST'
));
expect(uploads).toHaveLength(1);
expect(uploads[0]).toMatchObject({
byteLength: largePng.byteLength,
contentType: 'image/png',
});
expect(state.captured.some((request) => (
/^\/api\/coding\/attachments\/[^/]+\/content$/.test(request.path)
&& request.method === 'GET'
))).toBe(true);
const prompt = state.captured.find((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
));
expect(prompt?.body?.attachments).toEqual([
{ attachmentId: expect.any(String) },
]);
expect(JSON.stringify(prompt?.body)).not.toContain('data:image');
await releaseSnapshot(electronApp);
const persistedMessage = page.locator('[data-node-id="entry:sent-image"]');
await expect(persistedMessage).toBeVisible();
await expect(persistedMessage.getByRole('img', { name: '对话图片附件' })).toBeVisible();
await expect(persistedMessage.getByRole('button', { name: '从这里创建新对话分支' })).toBeVisible();
// A full renderer reload discards its optimistic state and blob URLs.
await page.reload();
page = await getStableWindow(electronApp);
await expect.poll(async () => (await readState(electronApp)).snapshotPending).toBe(true);
await releaseSnapshot(electronApp);
await expect(page.locator('[data-node-id="entry:sent-image"]').getByRole('img', {
name: '对话图片附件',
})).toBeVisible();
} finally {
await releaseSnapshot(electronApp);
}
});
test('hidden Conversation badge ignores process failures until interaction or task settlement', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection, true);
await disableCodingEventSource(page);
try {
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
const conversations = page.getByRole('group', { name: 'Builder 的对话' });
const firstConversation = conversations.getByRole('button', { name: '新对话', exact: true });
const secondConversation = conversations.getByRole('button', { name: 'Second Conversation' });
const secondUnreadBadge = secondConversation.locator('[aria-label="未读"]');
await expect(page.getByTestId('coding-conversation-header')).toContainText('新对话');
await expect(secondUnreadBadge).toHaveCount(0);
await emitCodingEvent(page, 'patch-batch', {
type: 'patch-batch',
conversationId: 'conversation-pi-second',
workerGeneration: 1,
fromSeq: 1,
toSeq: 3,
items: [
{
seq: 1,
at: 1_001,
runId: 'run-process-e2e',
patch: {
op: 'run.state',
run: { status: 'running', runId: 'run-process-e2e', mode: 'prompt', startedAt: 1_001 },
},
},
{
seq: 2,
at: 1_002,
runId: 'run-process-e2e',
patch: {
op: 'message.upsert',
node: {
kind: 'message',
id: 'message-process-e2e',
role: 'assistant',
status: 'streaming',
blocks: [{ kind: 'thinking', id: 'thinking-process-e2e', text: 'Checking', status: 'streaming' }],
},
},
},
{
seq: 3,
at: 1_003,
runId: 'run-process-e2e',
patch: {
op: 'tool.upsert',
node: {
kind: 'tool',
id: 'tool-process-e2e',
toolCallId: 'tool-call-process-e2e',
toolName: 'bash',
title: 'Run command',
inputText: 'false',
status: 'error',
output: [{ kind: 'text', id: 'tool-output-process-e2e', text: 'Command failed', status: 'complete' }],
},
},
},
],
});
await expect(secondUnreadBadge).toHaveCount(0);
await emitCodingEvent(page, 'patch-batch', {
type: 'patch-batch',
conversationId: 'conversation-pi-second',
workerGeneration: 1,
fromSeq: 4,
toSeq: 4,
items: [{
seq: 4,
at: 1_004,
runId: 'run-process-e2e',
patch: {
op: 'interaction.upsert',
interaction: {
id: 'interaction-process-e2e',
conversationId: 'conversation-pi-second',
runId: 'run-process-e2e',
kind: 'confirm',
title: 'Allow this action?',
status: 'pending',
},
},
}],
});
await expect(secondUnreadBadge).toHaveCount(1);
await secondConversation.click();
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
await expect(secondUnreadBadge).toHaveCount(0);
await firstConversation.click();
await expect(page.getByTestId('coding-conversation-header')).toContainText('新对话');
await emitCodingEvent(page, 'patch-batch', {
type: 'patch-batch',
conversationId: 'conversation-pi-second',
workerGeneration: 1,
fromSeq: 5,
toSeq: 5,
items: [{
seq: 5,
at: 1_005,
runId: 'run-process-e2e',
patch: {
op: 'interaction.remove',
interactionId: 'interaction-process-e2e',
},
}],
});
await expect(secondUnreadBadge).toHaveCount(0);
await emitCodingEvent(page, 'patch-batch', {
type: 'patch-batch',
conversationId: 'conversation-pi-second',
workerGeneration: 1,
fromSeq: 6,
toSeq: 6,
items: [{
seq: 6,
at: 1_006,
runId: 'run-process-e2e',
patch: {
op: 'run.state',
run: {
status: 'idle',
runId: 'run-process-e2e',
mode: 'prompt',
startedAt: 1_001,
settledAt: 1_006,
terminalReason: 'completed',
},
},
}],
});
await expect(secondUnreadBadge).toHaveCount(1);
} finally {
await releaseSnapshot(electronApp);
}
});
test('foreground focus rehydrates a terminal Snapshot after lifecycle sleep', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection, true);
await disableCodingEventSource(page);
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
const processGroup = page.getByTestId('coding-process-group');
await expect(processGroup).toHaveAttribute('data-process-state', 'active');
await expect(processGroup.locator('summary').first()).toContainText('处理中');
const snapshotPath = '/api/coding/conversations/conversation-pi-first-chat/snapshot';
const stateBeforeSleep = await readState(electronApp);
const snapshotCallsBeforeSleep = stateBeforeSleep.captured.filter((request) => (
request.path === snapshotPath
)).length;
await settleSnapshot(electronApp);
await electronApp.evaluate(({ BrowserWindow }) => {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send('lifecycle:sleep');
}
});
await expect.poll(async () => page.evaluate(() => {
const trackedWindow = window as typeof window & {
__makeloreCodingEventSources?: Array<{ readyState: number }>;
};
return trackedWindow.__makeloreCodingEventSources?.at(-1)?.readyState ?? -1;
})).toBe(2);
await page.evaluate(() => window.dispatchEvent(new FocusEvent('focus')));
await expect(processGroup).toHaveAttribute('data-process-state', 'settled');
await expect(processGroup.locator('summary').first()).not.toContainText('处理中');
await expect.poll(async () => {
const state = await readState(electronApp);
return state.captured.filter((request) => request.path === snapshotPath).length;
}).toBeGreaterThan(snapshotCallsBeforeSleep);
const stateAfterFocus = await readState(electronApp);
expect(stateAfterFocus.captured.some((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
))).toBe(false);
});
test('Windows Code titlebar keeps browser actions clear of the logo and window controls', async ({
launchElectronApp,
}) => {
test.skip(process.platform !== 'win32', 'Windows custom titlebar only');
const electronApp = await launchElectronApp({ skipSetup: true });
const page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection, true);
await disableCodingEventSource(page);
try {
await page.reload();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
const header = page.getByTestId('coding-conversation-header');
const assertChromeClear = async () => {
const logo = await page.getByTestId('titlebar-logo').boundingBox();
expect(logo).not.toBeNull();
for (const button of await header.getByRole('button').all()) {
const bounds = await button.boundingBox();
expect(bounds).not.toBeNull();
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(logo!.x);
}
const minimize = page.getByTitle('Minimize');
const minimizeBounds = await minimize.boundingBox();
expect(minimizeBounds).not.toBeNull();
expect(logo!.x + logo!.width).toBeLessThanOrEqual(minimizeBounds!.x);
// The conversation header must not intercept clicks in the window controls.
for (const title of ['Minimize', 'Maximize', 'Close']) {
await page.getByTitle(title, { exact: true }).click({ trial: true });
}
};
for (const { width, zoom } of [
{ width: 1280, zoom: 1 },
{ width: 1024, zoom: 1 },
{ width: 1280, zoom: 1.25 },
]) {
await electronApp.evaluate(({ BrowserWindow }, dimensions) => {
const window = BrowserWindow.getAllWindows()[0];
window.setSize(dimensions.width, 800);
window.webContents.setZoomFactor(dimensions.zoom);
}, { width, zoom });
await expect.poll(async () => page.evaluate(() => window.innerWidth)).toBe(Math.round(width / zoom));
await assertChromeClear();
if (width === 1280 && zoom === 1) {
await page.screenshot({ path: test.info().outputPath('windows-code-titlebar.png') });
}
await header.getByRole('button', { name: '打开开发浏览器' }).click();
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
await assertChromeClear();
await header.getByRole('button', { name: '关闭开发浏览器' }).click();
await expect(page.getByTestId('agent-browser-panel')).toHaveCount(0);
}
await page.getByTestId('titlebar-sidebar-toggle').click();
await assertChromeClear();
await settleSnapshot(electronApp);
await page.evaluate(() => window.dispatchEvent(new FocusEvent('focus')));
await expect(header.getByRole('button', { name: '中止', exact: true })).toHaveCount(0);
await assertChromeClear();
await page.getByTitle('Maximize', { exact: true }).click();
await expect(page.getByTitle('Restore', { exact: true })).toBeVisible();
await page.getByTitle('Restore', { exact: true }).click();
await expect(page.getByTitle('Maximize', { exact: true })).toBeVisible();
} finally {
await releaseSnapshot(electronApp);
}
});
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection, true);
await disableCodingEventSource(page);
try {
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
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();
expect(logoBounds).not.toBeNull();
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 ·');
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);
const browserToggle = conversationHeader.getByRole('button', { name: '打开开发浏览器' });
await expect(browserToggle).toBeVisible();
if (process.platform === 'win32') {
const browserBounds = await browserToggle.boundingBox();
expect(browserBounds).not.toBeNull();
expect(browserBounds!.x + browserBounds!.width).toBeLessThanOrEqual(logoBounds!.x);
}
await browserToggle.click();
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveAttribute('data-state', 'closed');
await expect.poll(async () => {
const state = await readState(electronApp);
return state.captured.some((request) => request.path.startsWith('/api/agent-browser/state?'));
}).toBe(true);
expect((await readState(electronApp)).captured.some(
(request) => request.path === '/api/agent-browser/diagnostics',
)).toBe(false);
await page.getByRole('button', { name: '关闭开发浏览器' }).last().click();
await expect(page.getByTestId('agent-browser-panel')).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('Inspecting the project before responding.');
await expect(activeThinking).not.toContainText('Preparing the smallest safe next step.');
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-origin', 'head');
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).toHaveAttribute('data-progress-origin', 'head');
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();
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('Checking browser runtime state');
await expect(compactTool.getByTestId('tool-progress-preview')).not.toContainText('Browser ready');
await expect(compactTool.getByTestId('tool-progress-preview'))
.toHaveAttribute('data-progress-origin', 'head');
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)).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();
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);
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');
await page.getByRole('button', { name: '发送', exact: true }).click();
await expect.poll(async () => (await readState(electronApp)).captured.some((request) => (
request.path.endsWith('/prompt') && request.body?.mode === 'follow-up'
))).toBe(true);
await expect(page.getByTestId('coding-file-attachment-input')).toBeEnabled();
await expect(page.getByText(/条消息已被本地 Agent 接收/)).toHaveCount(0);
await expect(page.getByTestId('coding-message-queue')).toBeVisible();
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();
await expect(page.getByRole('button', { name: '中止', exact: true })).toHaveCount(0);
await expect(composer.getByRole('button', { name: '中止生成' })).toHaveCount(0);
await expect(runtimeSettings).toBeEnabled();
await expect(page.getByTestId('coding-message-queue')).toHaveCount(0);
await composer.getByRole('textbox').fill('继续');
await expect(composer.getByRole('button', { name: '发送', exact: true })).toBeEnabled();
await composer.getByRole('textbox').press('Enter');
await expect.poll(async () => (await readState(electronApp)).captured.some((request) => (
request.path.endsWith('/prompt')
&& request.body?.mode === 'prompt'
&& request.body?.text === '继续'
))).toBe(true);
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
await expect(builderConversations).toBeVisible();
const forkActions = page.getByRole('button', { name: '从这里创建新对话分支' });
await expect(forkActions).toHaveCount(1);
await forkActions.click();
await expect(page.getByTestId('coding-conversation-header')).toContainText('Feature UI branch');
await expect(builderConversations.getByRole('button', { name: 'Feature UI branch' })).toBeVisible();
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();
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
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 builderConversations.getByRole('button', { name: 'History and quota' }).click();
const historyProcess = page.getByTestId('coding-process-group');
await expect(historyProcess.locator('summary').first())
.toContainText('词元点数余额不足,请充值后重试。');
await expect(page.getByText('History message 0')).toHaveCount(0);
await expect(page.getByText('History message 31')).toHaveCount(1);
const historyTimeline = page.getByTestId('coding-conversation-timeline');
const beforeHeight = await historyTimeline.evaluate((element) => {
element.scrollTop = 0;
const height = element.scrollHeight;
element.dispatchEvent(new Event('scroll', { bubbles: true }));
return height;
});
await expect(page.getByText('History message 0')).toHaveCount(1);
const anchoredScroll = await historyTimeline.evaluate((element) => ({
scrollHeight: element.scrollHeight,
scrollTop: element.scrollTop,
}));
expect(anchoredScroll.scrollTop).toBeGreaterThan(0);
expect(Math.abs(
anchoredScroll.scrollTop - (anchoredScroll.scrollHeight - beforeHeight),
)).toBeLessThanOrEqual(2);
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')
&& 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(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(false);
expect(state.captured.every((request) => !request.path.includes('/api/opencode/share'))).toBe(true);
} finally {
await releaseSnapshot(electronApp);
}
});