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

2078 lines
108 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,
removedModel = false,
audioPreview?: { executionId: string; path: string; dataUrl: string },
): Promise<void> {
await electronApp.evaluate(async (_, payload) => {
const { connection, featureComplete, managedCapabilities, removedModel, audioPreview } = 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;
workReady?: boolean;
workFailed?: boolean;
workGeneration?: number;
};
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,
});
const teacherDefinition = {schema_version:1,teacher_id:'coding-teacher',name:'代码智能体',description:'',avatar_id:'avatar-01',welcome_message:'一起理解代码',suggested_questions:['解释当前代码'],system_prompt:'教学',skills:[],model:{model_id:'model-a',reasoning_choice:{mode:'default'}},limits:{max_input_tokens:8000,max_output_tokens:1500}};
const teachers = [
{ teacher_id: 'teacher-code', version: 1, is_default: true, definition: { ...teacherDefinition, config_id: 'teacher-code', runtime: 'yuxi', system_prompt: '', yuxi: { agent_slug: 'code', agent_version: 2 } } },
{ teacher_id: 'teacher-algorithm', version: 9, is_default: false, definition: { ...teacherDefinition, config_id: 'teacher-algorithm', runtime: 'yuxi', name: '朋友', system_prompt: '', yuxi: { agent_slug: 'algorithm', agent_version: 4 } } },
];
const consultationTopics: Record<string, Record<string, unknown>> = {};
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';
if (audioPreview && path === '/api/coding/game-audio/' + audioPreview.executionId + '/outputs/0') {
return respond({ path: audioPreview.path, dataUrl: audioPreview.dataUrl });
}
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.endsWith('/teacher-check-in') && method === 'POST') {
const current = consultationTopics.agent ?? {schemaVersion:1,revision:0,id:'agent-topic',accountId:'e2e',projectId:project.id,sourceConversationId:'project',definition:teacherDefinition,version:1,createdAt:now,updatedAt:now,requests:[]};
consultationTopics.agent = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,intent:'check-in',text:'',references:[],sourceConversationId:body!.sourceConversationId,createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,status:'completed',response:'你刚才想加排行榜,我们一起想想比什么更有意思?'}]};
return respond({topic:consultationTopics.agent});
}
if (path === '/api/coding/teacher/teachers') return respond({ items: teachers });
if (path === '/api/coding/teacher/config') return respond({enabled:true,revision:1,published_version:1,definition:teacherDefinition});
const consultationPath = path.match(/\/(agent|teacher)-topics(?:\/|$)/)?.[1];
if (consultationPath) {
const topicBase = `/${consultationPath}-topics`;
const topicId = `${consultationPath}-topic`;
const currentTopic = consultationTopics[consultationPath];
if (path.endsWith(topicBase) && method === 'GET') return respond({items:currentTopic?[{id:currentTopic.id,title:'智能体话题',teacherId:(currentTopic.definition as {config_id?:string}).config_id}]:[],lastSelectedTopicId:currentTopic?.id??null});
if (path.endsWith(topicBase) && method === 'POST') {
const selected = teachers.find(item => item.version === body?.teacherVersion) ?? teachers[0];
consultationTopics[consultationPath]={schemaVersion:1,revision:0,id:topicId,accountId:'e2e',projectId:project.id,sourceConversationId:conversation.id,definition:selected.definition,version:selected.version,createdAt:now,updatedAt:now,requests:[]};
return respond(consultationTopics[consultationPath],201);
}
if (path.endsWith(`${topicBase}/${topicId}/messages`) && method === 'POST') {
const suggestionRequest = body!.intent === 'suggestions';
const teacherResponse = suggestionRequest ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
consultationTopics[consultationPath]={...currentTopic,revision:Number(currentTopic?.revision??0)+1,requests:[...(currentTopic?.requests as unknown[]??[]),{id:body!.requestId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response:teacherResponse,...(suggestionRequest?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
return respond(consultationTopics[consultationPath],202);
}
if (path.endsWith(`${topicBase}/${topicId}`)) return respond(currentTopic);
}
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: removedModel ? 'model-b' : 'model-a',
fallbackModels: removedModel ? [] : ['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/ensure-work' && method === 'POST') {
if (state.workFailed) return respond({ success: true, status: 'failed', message: '作品还没有打开,我可以检查启动情况,再帮你打开。' });
return respond(state.workReady === false
? { success: true, status: 'starting', conversation }
: { success: true, status: 'ready', browser: browserSnapshot({ browserId: 'browser-e2e', state: 'attached', generation: state.workGeneration ?? 1, url: 'http://127.0.0.1:4173/' }) });
}
if ((path === '/api/agent-browser/open' || path === '/api/agent-browser/navigate') && 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: state.workGeneration ?? 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 (removedModel && conversation.model.modelId === 'model-a') {
return respond({ success: false, code: 'CODING_MODEL_UNAVAILABLE', error: '所选模型当前不可用,请重新选择。' }, 409);
}
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') {
if (removedModel) {
conversation.model = body?.model as typeof configuredModel;
snapshot.conversation.model.model = conversation.model;
}
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, removedModel, audioPreview });
}
test('saved Game Audio offers click-only playable local preview in Electron', async ({ launchElectronApp }, testInfo) => {
const app = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(app);
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,
}));
const executionId = '00000000-0000-4000-8000-000000000601';
const relative = 'assets/generated/game-audio/' + executionId + '/sound-1.wav';
// Synthetic quarter-second PCM sample, never a claimed Meowa output.
const wav = Buffer.alloc(4044);
wav.write('RIFF', 0); wav.writeUInt32LE(4036, 4); wav.write('WAVEfmt ', 8);
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28);
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(4000, 40);
await installCodingFirstChatHost(app, connection, true, false, false, {
executionId, path: relative, dataUrl: 'data:audio/wav;base64,' + wav.toString('base64'),
});
await settleSnapshot(app);
await disableCodingEventSource(page);
await page.reload();
page = await getStableWindow(app);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
const snapshot = await page.evaluate(async () => {
const response = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
path: '/api/coding/conversations/conversation-pi-first-chat/snapshot', method: 'GET',
}) as { data: { json: { snapshot: Record<string, unknown> } } };
return response.data.json.snapshot;
});
const details = {
schema: 'makelore-capability.v1', plugin_id: 'makelore.game-audio', plugin_version: '1.0.0',
capability_id: 'game-audio.sound', operation: 'generate', request_id: 'pi:r:t',
success: true, status: 200, code: null, error: null, retryable: false, payload_schema: 'game-audio.v1',
billing: { mode: 'platform_metered', status: 'settled', reserved_points: '0.04',
actual_points: '0.04', usage_amount: 4, unit: 'half_second' },
data: { executionId, logicalOperationId: 'pi:r:t', projectId: 'project-e2e',
providerStatus: 'succeeded', deliveryStatus: 'saved', phase: 'saved', outputCount: 1,
files: [{ index: 0, path: relative, bytes: wav.length, mimeType: 'audio/wav' }] },
};
await emitCodingEvent(page, 'snapshot', {
type: 'snapshot', conversationId: 'conversation-pi-first-chat', workerGeneration: 1, seq: 10,
snapshot: { ...snapshot, cursor: { workerGeneration: 1, seq: 10 }, nodes: [{
kind: 'tool', id: 'audio-preview', toolCallId: 'audio-preview', toolName: 'game_sound_generate',
title: '生成游戏音效', status: 'complete', inputText: '', output: [], details,
}] },
});
await expect(page.getByTestId('tool-progress-preview')).toContainText('游戏音频 · 已保存');
await page.getByTestId('coding-process-group').locator('summary').first().click();
await page.locator('[data-node-id="audio-preview"] > summary').click();
await expect(page.locator('audio')).toHaveCount(0);
await page.getByRole('button', { name: '试听 sound-1.wav' }).click();
const player = page.getByLabel('试听 sound-1.wav', { exact: true });
await expect(player).toHaveAttribute('controls', '');
expect(await player.getAttribute('autoplay')).toBeNull();
await expect.poll(() => player.evaluate((element) => (element as HTMLAudioElement).duration)).toBe(0.25);
expect(await player.evaluate((element) => (element as HTMLAudioElement).paused)).toBe(true);
await page.screenshot({ path: testInfo.outputPath('audio-preview.png') });
});
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('project-conversations');
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: '查看归档对话' }).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.getByRole('button', { name: '查看归档对话' })).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('a removed saved model can be replaced after snapshot preparation fails', 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, false, true);
await settleSnapshot(electronApp);
await disableCodingEventSource(page);
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByText('所选模型当前不可用,请重新选择。', { exact: true })).toBeVisible();
const settings = page.getByRole('button', { name: /模型与思考设置/ });
await expect(settings).toBeEnabled();
await settings.click();
await page.getByRole('menuitem', { name: '模型 model-a', exact: true }).click();
await expect(page.getByRole('menuitemradio', { name: 'model-a(当前不可用)' })).toBeDisabled();
await page.getByRole('menuitemradio', { name: 'model-b', exact: true }).click();
await expect(settings).toContainText('model-b');
await expect(page.getByText('所选模型当前不可用,请重新选择。', { exact: true })).toHaveCount(0);
await expect(page.getByText('Durable user fork source', { exact: true })).toBeVisible();
await page.getByTestId('coding-process-group').locator('summary').first().click();
await expect(page.getByText('Durable assistant response', { exact: true })).toBeVisible();
const requests = (await readState(electronApp)).captured;
expect(requests.filter(request => request.path.endsWith('/model') && request.method === 'POST').map(request => request.body)).toEqual([
{ model: { accountId: 'account-e2e', modelId: 'model-b', thinkingLevel: 'off' } },
]);
expect(requests.some(request => request.path.endsWith('/prompt') || request.path.endsWith('/abort'))).toBe(false);
});
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';
});
await expect(page.getByRole('button', { name: 'Builder', exact: true })).toHaveCount(0);
const builderConversations = page.getByRole('group', { name: '项目会话' });
await expect(builderConversations).toBeVisible();
await expect(builderConversations.getByRole('button', { name: '新对话', exact: true })).toHaveCount(0);
expect((await readState(electronApp)).captured.filter(item => item.path === '/api/coding/projects/conversations' && item.method === 'POST')).toHaveLength(0);
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(/条消息已被本地 Agent 接收/)).toHaveCount(0);
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: '项目会话' });
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')).toHaveCount(0);
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('project-conversations')).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: '项目会话' });
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);
}
});
test('project consultations preserve student drafts and switch between work and chat without submitting advice', 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';});
await expect(page.getByTestId('project-conversations')).toBeVisible();
await expect(page.getByTestId('sidebar')).toHaveCSS('width', '171px');
const rail = page.getByTestId('sidebar');
const railResizer = page.getByRole('separator', { name: '调整左侧功能栏宽度' });
const handle = (await railResizer.boundingBox())!;
await page.mouse.move(handle.x + handle.width / 2, handle.y + 80);
await page.mouse.down();
await page.mouse.move(handle.x + 180, handle.y + 80, { steps: 8 });
await page.mouse.up();
await expect(rail).toHaveCSS('width', '256px');
await expect(page.getByTestId('titlebar-sidebar-surface')).toHaveCSS('width', '256px');
await expect(page.getByTestId('coding-conversation-header-titlebar')).toHaveCSS('left', '256px');
await railResizer.focus();
await railResizer.press('Home');
await expect(rail).toHaveCSS('width', '128px');
await railResizer.press('End');
await expect(rail).toHaveCSS('width', '256px');
await page.getByTestId('titlebar-sidebar-toggle').click();
await expect(railResizer).toHaveCount(0);
await page.getByTestId('titlebar-sidebar-toggle').click();
await expect(rail).toHaveCSS('width', '256px');
await page.reload(); page = await getStableWindow(electronApp);
await expect(rail).toHaveCSS('width', '256px');
await railResizer.dblclick({ position: { x: 3, y: 80 } });
await expect(rail).toHaveCSS('width', '171px');
await expect(page.getByTestId('coding-conversation-sidebar')).toHaveCount(0);
await expect(page.getByRole('tab',{name:'操作对话',exact:true})).toHaveAttribute('aria-selected','true');
await expect(page.getByRole('button',{name:/项目共识|记一下|记入共识/})).toHaveCount(0);
const composer=page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
await composer.fill('保留我的草稿');
await page.getByRole('tab',{name:'作品',exact:true}).click();
await expect(page.getByRole('tabpanel',{name:'作品',exact:true})).toBeVisible();
await expect(composer).toBeHidden();
await page.getByRole('tab',{name:'操作对话',exact:true}).click();
await expect(composer).toHaveValue('保留我的草稿');
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
const teacher=page.getByTestId('teacher-chat-panel');
await expect(page.getByRole('tab',{name:'作品',exact:true})).toHaveAttribute('aria-selected','true');
await expect(page.getByRole('tabpanel',{name:'作品',exact:true})).toBeVisible();
await expect(composer).toBeHidden();
await expect(composer).toHaveValue('保留我的草稿');
await expect(teacher.getByText('一起理解代码')).toBeVisible();
await expect(teacher.getByRole('combobox', { name: '新话题使用的智能体' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '与代码智能体聊天' })).toBeVisible();
await expect(page.getByRole('button', { name: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
await expect(teacher.getByTestId('consultation-composer-actions').getByRole('button', { name: '帮我看看', exact: true })).toBeVisible();
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toBeVisible();
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
await teacher.getByRole('button',{name:'帮我看看',exact:true}).click();
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
await teacher.getByRole('button',{name:'怎样观察别人玩游戏?',exact:true}).click();
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
await expect(teacher.getByRole('button',{name:'带回主会话草稿'})).toHaveCount(0);
await expect(teacher.getByRole('button',{name:'我去试一试'})).toHaveCount(0);
await expect(teacher.getByRole('button',{name:'复制',exact:true})).toHaveCount(0);
await expect(composer).toHaveValue('保留我的草稿');
await teacher.getByRole('button',{name:'我也说不清,你带我看看',exact:true}).click();
await expect(teacher.getByText('你最近做的哪一步,让你停下来想了一会儿?')).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
await page.getByRole('tab',{name:'操作对话',exact:true}).click();
await expect(composer).toBeVisible();
await teacher.getByRole('button',{name:'关闭智能体'}).click();
await page.getByRole('button', { name: '与代码智能体聊天' }).click();
await expect(teacher.getByRole('heading', { name: '代码智能体' })).toBeVisible();
await expect(teacher.getByRole('textbox', { name: '向智能体提问' })).toHaveValue('');
await teacher.getByRole('textbox', { name: '向智能体提问' }).fill('另一个智能体的草稿');
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
const requests=(await readState(electronApp)).captured;
expect(requests.filter(item=>item.path.endsWith('/prompt')&&item.method==='POST')).toHaveLength(0);
const consultationRequests=requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST');
expect(consultationRequests).toHaveLength(3);
expect(requests.find(item => item.path.endsWith('/agent-topics') && item.method === 'POST')?.body?.teacherVersion).toBe(9);
await expect(teacher.getByRole('heading', { name: '朋友', exact: true })).toBeVisible();
expect(consultationRequests[0].body).toMatchObject({intent:'suggestions',text:'帮我看看'});
expect(consultationRequests[1].body).toMatchObject({text:'怎样观察别人玩游戏?'});
expect(consultationRequests[1].body?.intent).toBeUndefined();
expect(consultationRequests[2].body).toMatchObject({intent:'guided-help',text:'我也说不清,你带我看看'});
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
await teacher.getByRole('button',{name:'关闭智能体'}).click();await expect(teacher).toHaveCount(0);
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
await expect(page.getByText('先理解状态如何随点击变化,再修改代码。',{exact:true})).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
} finally {await releaseSnapshot(electronApp);}
});
test('settled prompt releases the composer without a client receipt and keeps the next draft', 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 composer = page.getByTestId('coding-message-composer');
const input = composer.getByRole('textbox');
const send = composer.getByRole('button', { name: '发送', exact: true });
await input.fill('hello');
await send.click();
await expect.poll(async () => (await readState(electronApp)).captured.filter((item) => item.path.endsWith('/prompt') && item.method === 'POST').length).toBe(1);
await expect(page.getByTestId('coding-file-attachment-input')).toBeEnabled();
await input.fill('我的游戏在哪里哦');
await expect(send).toBeDisabled();
await expect(page.getByText(/条消息已被本地 Agent 接收/)).toHaveCount(0);
const snapshot = {
schemaVersion: 1,
conversation: { id: 'conversation-pi-first-chat', projectId: 'project-pi-first-chat', agentId: 'builder', title: '新对话', model: { model: { accountId: 'account-e2e', modelId: 'model-a', thinkingLevel: 'off' }, modelResolution: 'resolved' } },
nodes: [{ kind: 'message', id: 'durable-hello', sourceEntryId: 'durable-hello', role: 'user', status: 'complete', blocks: [{ kind: 'text', id: 'durable-hello:text', text: 'hello', status: 'complete' }] }],
run: { status: 'idle', runId: 'run-e2e-feature', mode: 'prompt', terminalReason: 'completed', settledAt: 2000 },
worker: { status: 'ready', generation: 1 },
cursor: { workerGeneration: 1, seq: 2 },
queue: { items: [] }, context: { usedTokens: 0, contextWindow: 4096, compaction: 'idle' }, pendingInteractions: [],
};
await emitCodingEvent(page, 'snapshot', { type: 'snapshot', conversationId: snapshot.conversation.id, workerGeneration: 1, seq: 2, snapshot });
// An older run's completion is not permission to send the accepted prompt twice.
await expect(send).toBeDisabled();
await emitCodingEvent(page, 'snapshot', { type: 'snapshot', conversationId: snapshot.conversation.id, workerGeneration: 1, seq: 3, snapshot: { ...snapshot, run: { ...snapshot.run, runId: 'run-e2e-1' }, cursor: { workerGeneration: 1, seq: 3 } } });
await expect(send).toBeEnabled();
await expect(input).toHaveValue('我的游戏在哪里哦');
await expect(page.getByText('正在提交…', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('coding-conversation-timeline').getByText('hello', { exact: true })).toHaveCount(1);
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/prompt') && item.method === 'POST')).toHaveLength(1);
} finally { await releaseSnapshot(electronApp); }
});
test('teacher stays in the top-right header and proactively checks in without taking over the student draft', 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);
await page.clock.install();
try {
await page.reload(); page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
const header = page.getByTestId('coding-conversation-header');
const companion = header.getByTestId('teacher-companion');
const teacherButton = companion.getByRole('button', { name: '智能体', exact: true });
await expect(teacherButton).toHaveText('代码智能体');
await expect(teacherButton).toBeVisible();
await expect(teacherButton.locator('img')).toHaveAttribute('src', /avatar-01/);
await expect(page.getByRole('checkbox', { name: '智能体偶尔来看看' })).toHaveCount(0);
await expect(page.getByTestId('teacher-companion')).toHaveCount(1);
const invitation = companion.getByTestId('teacher-invitation');
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
await expect(invitation).toContainText('一起理解代码');
await expect(invitation).toBeInViewport({ ratio: 1 });
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
await page.screenshot({ path: test.info().outputPath('teacher-entry-greeting.png') });
await expect(page.getByRole('button', { name: /举手/ })).toHaveCount(0);
const headerBounds = await header.boundingBox();
const teacherBounds = await teacherButton.boundingBox();
expect(headerBounds).not.toBeNull();
expect(teacherBounds).not.toBeNull();
expect(teacherBounds!.x).toBeGreaterThan(headerBounds!.x + headerBounds!.width / 2);
expect(teacherBounds!.y).toBeGreaterThanOrEqual(headerBounds!.y);
expect(teacherBounds!.y + teacherBounds!.height).toBeLessThanOrEqual(headerBounds!.y + headerBounds!.height + 1);
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
await composer.fill('我还在写自己的想法');
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(teacherButton).toBeVisible();
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await composer.focus();
await page.clock.fastForward(300_000);
await expect(invitation).toContainText('排行榜');
await expect(invitation).toHaveAttribute('data-bubble-kind', 'check-in');
await expect(invitation).toBeInViewport({ ratio: 1 });
const presence = companion.getByTestId('teacher-presence');
await expect(presence.getByRole('button', { name: '打开这条智能体消息' })).toBeVisible();
await expect(presence.getByRole('button', { name: '打开这条智能体消息' }).locator('img')).toHaveAttribute('src', await teacherButton.locator('img').getAttribute('src') as string);
const [invitationBounds, companionBounds, avatarBounds, presenceBounds, speakerBounds] = await Promise.all([
invitation.boundingBox(), companion.boundingBox(), teacherButton.locator('img').boundingBox(), presence.boundingBox(), presence.getByRole('button', { name: '打开这条智能体消息' }).boundingBox(),
]);
expect(invitationBounds).not.toBeNull();
expect(companionBounds).not.toBeNull();
expect(avatarBounds).not.toBeNull();
expect(invitationBounds!.y).toBeGreaterThanOrEqual(avatarBounds!.y + avatarBounds!.height);
expect(Math.abs(presenceBounds!.x + presenceBounds!.width - companionBounds!.x - companionBounds!.width)).toBeLessThanOrEqual(2);
expect(speakerBounds!.x - invitationBounds!.x - invitationBounds!.width).toBeLessThanOrEqual(10);
await expect(composer).toBeFocused();
await expect(composer).toHaveValue('我还在写自己的想法');
await expect(page.getByRole('tab', { name: '操作对话', exact: true })).toHaveAttribute('aria-selected', 'true');
await page.screenshot({ path: test.info().outputPath('teacher-proactive-invitation.png') });
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(invitation).toBeInViewport({ ratio: 1 });
const [workInvitationBounds, browserViewportBounds] = await Promise.all([
presence.boundingBox(), page.getByTestId('agent-browser-viewport').boundingBox(),
]);
expect(workInvitationBounds).not.toBeNull();
expect(browserViewportBounds).not.toBeNull();
// The native view uses this rectangle and cannot be covered by a DOM z-index.
expect(browserViewportBounds!.y).toBeGreaterThanOrEqual(workInvitationBounds!.y + workInvitationBounds!.height);
const workPane = page.getByRole('tabpanel', { name: '作品', exact: true });
const workPanel = page.getByTestId('agent-browser-panel');
// Account for existing DOM toolbars instead of reserving a full bubble row.
expect((await workPanel.boundingBox())!.y - (await workPane.boundingBox())!.y).toBeLessThan(65);
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await expect(composer).toHaveValue('我还在写自己的想法');
await page.clock.fastForward(300_000);
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(1);
await invitation.getByRole('button', { name: '和智能体聊聊' }).click();
await expect(invitation).toHaveCount(0);
await expect(presence).toHaveCount(0);
await expect(teacherButton).toBeVisible();
await expect(page.getByRole('tab', { name: '作品', exact: true })).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('tabpanel', { name: '作品', exact: true })).toBeVisible();
const teacher = page.getByTestId('teacher-chat-panel');
await expect(teacher.getByText('你刚才想加排行榜,我们一起想想比什么更有意思?')).toBeVisible();
await expect(teacher.locator('.consultation-user')).toHaveCount(0);
const teacherInput = teacher.getByRole('textbox', { name: '向智能体提问' });
await expect(teacherInput).toBeFocused();
await teacherInput.fill('我想比谁更了解自己的宠物');
await teacher.getByRole('button', { name: '关闭智能体' }).click();
await expect(page.getByRole('button', { name: '智能体', exact: true })).toBeFocused();
await page.getByRole('button', { name: '智能体', exact: true }).click();
await expect(teacherInput).toHaveValue('我想比谁更了解自己的宠物');
await teacherInput.fill('');
// Let the cleared draft propagate through the parent before advancing the check-in timer.
await page.clock.runFor(32);
await page.clock.fastForward(300_000);
await page.clock.runFor(15_000);
await expect(invitation).toBeVisible();
await expect(teacher).toBeVisible();
await expect(invitation.getByRole('button', { name: '看看智能体说的' })).toBeVisible();
// The message sits over the separate teacher pane and must not push Work down.
await expect.poll(async () => (await workPanel.boundingBox())!.y - (await workPane.boundingBox())!.y).toBe(20);
await page.screenshot({ path: test.info().outputPath('teacher-open-proactive-invitation.png') });
await expect(invitation).toBeInViewport({ ratio: 1 });
await invitation.getByRole('button', { name: '等会儿聊' }).click();
await expect(invitation).toHaveCount(0);
await expect(composer).toHaveValue('我还在写自己的想法');
expect((await readState(electronApp)).captured.filter((item) => /\/(messages|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
} finally { await releaseSnapshot(electronApp); }
});
test('work tab prepares the page automatically and keeps the student draft through repeated switches', 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 electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workReady: boolean } }).__makelorePiFirstChatE2E.workReady = false; });
await disableCodingEventSource(page);
await page.reload(); page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
const composer = page.getByTestId('coding-message-composer').getByRole('textbox');
await composer.fill('还没有发出的想法');
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveCount(0);
await expect(page.getByRole('button', { name: '展开调试面板' })).toHaveCount(0);
await expect(page.getByRole('status').filter({ hasText: '正在打开你的作品' })).toBeVisible();
await page.getByRole('button', { name: '回到操作对话', exact: true }).click();
await expect(composer).toHaveValue('还没有发出的想法');
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(page.getByText('正在打开你的作品…', { exact: true })).toBeVisible();
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workReady: boolean } }).__makelorePiFirstChatE2E.workReady = true; });
await expect(page.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
await expect(page.getByText('正在打开你的作品…', { exact: true })).toHaveCount(0);
// A manual address open must not permanently disable recovery checks.
await page.getByRole('textbox', { name: '网页地址' }).fill('http://127.0.0.1:4173/');
await page.getByRole('textbox', { name: '网页地址' }).press('Enter');
await expect(page.getByRole('button', { name: '刷新网页', exact: true })).toBeEnabled();
// Background sleep disposes the native view. Returning to the already-open
// Work tab must recover without another click or another prompt identity.
const opensBeforeSleep = (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work').length;
await electronApp.evaluate(({ BrowserWindow }) => {
(globalThis as unknown as { __makelorePiFirstChatE2E: { workGeneration: number } }).__makelorePiFirstChatE2E.workGeneration = 2;
BrowserWindow.getAllWindows()[0].webContents.send('agent-browser:state', {
browserId: null, projectId: 'project-pi-first-chat', projectPath: null, state: 'closed',
generation: 1, url: '', title: '', visible: false, bounds: null,
canGoBack: false, canGoForward: false, eventCursor: 0,
});
});
// Recover from the closed state itself, even if a focus event already happened.
await expect.poll(async () => (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work').length).toBeGreaterThan(opensBeforeSleep);
await expect(page.getByRole('button', { name: '刷新网页', exact: true })).toBeEnabled();
await expect(page.getByText('正在打开你的作品…', { exact: true })).toHaveCount(0);
await expect(page.getByText('开发浏览器已关闭。', { exact: true })).toHaveCount(0);
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await expect(composer).toHaveValue('还没有发出的想法');
const requests = (await readState(electronApp)).captured;
const opens = requests.filter((request) => request.path === '/api/agent-browser/ensure-work');
expect(opens.length).toBeGreaterThanOrEqual(2);
expect(new Set(opens.map((request) => request.body?.request_id)).size).toBe(1);
expect(requests.filter((request) => request.path.endsWith('/prompt'))).toHaveLength(0);
});
test('work failure offers a useful recovery and retains the student draft after opening', 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 electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workFailed: boolean } }).__makelorePiFirstChatE2E.workFailed = true; });
await disableCodingEventSource(page);
await page.reload(); page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
await composer.fill('我还没有写完的想法');
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(page.getByRole('button', { name: '帮我检查并打开' })).toBeVisible();
await expect(page.getByText('本次处理失败。', { exact: true })).toHaveCount(0);
await page.screenshot({ path: test.info().outputPath('work-actionable-recovery.png') });
const first = (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work');
expect(first).toHaveLength(1);
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workFailed: boolean } }).__makelorePiFirstChatE2E.workFailed = false; });
await page.getByRole('button', { name: '帮我检查并打开' }).click();
await expect(page.getByRole('button', { name: '帮我检查并打开' })).toHaveCount(0);
await expect(page.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await expect(composer).toHaveValue('我还没有写完的想法');
const requests = (await readState(electronApp)).captured;
const opens = requests.filter((item) => item.path === '/api/agent-browser/ensure-work');
expect(new Set(opens.map((item) => item.body?.request_id)).size).toBe(2);
expect(requests.filter((item) => item.path.endsWith('/prompt'))).toHaveLength(0);
});