import type { ElectronApplication, Page } from 'playwright-core'; import { expect, getStableWindow, test } from './fixtures/electron'; type CapturedRequest = { path: string; method: string; body?: Record; byteLength?: number; contentType?: string; at: number; }; type HostConnection = { baseUrl: string; token: string; }; async function disableCodingEventSource(page: Page): Promise { await page.addInitScript(() => { 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; queueMicrotask(() => this.onopen?.(new Event('open'))); } close(): void { this.readyState = LocalEventSource.CLOSED; } } Object.defineProperty(window, 'EventSource', { configurable: true, writable: true, value: LocalEventSource, }); }); } async function installCodingFirstChatHost( electronApp: ElectronApplication, hostConnection: HostConnection, ): Promise { await electronApp.evaluate(async (_, connection) => { const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron'); type MainState = { captured: CapturedRequest[]; releaseSnapshot: (() => void) | null; snapshotPending: boolean; }; const mainGlobal = globalThis as typeof globalThis & { __makelorePiFirstChatE2E?: MainState; }; const state: MainState = { captured: [], releaseSnapshot: null, snapshotPending: false, }; mainGlobal.__makelorePiFirstChatE2E = state; const now = '2026-08-24T00:00:00.000Z'; const project = { id: 'project-pi-first-chat', name: 'PI first chat', createdAt: now, updatedAt: now, lastOpenedAt: now, }; const legacyProject = { ...project, path: 'D:/e2e/pi-first-chat' }; const agent = { id: 'builder', avatarId: 'avatar-01', roleName: '实现者', name: 'Builder', builtIn: false, enabled: true, model: null, modelResolution: 'required', 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', legacyConversationNotice: 'none', createdAt: now, updatedAt: now, }; const legacyConfig = { schemaVersion: 1, projectType: 'custom', initialized: true, defaultModel: null, agents: [{ ...agent, model: null }], knowledgeDirectory: 'knowledge', createdAt: now, updatedAt: now, }; const conversation = { id: 'conversation-pi-first-chat', agentId: agent.id, title: '新对话', model: null, modelResolution: 'required', archivedAt: null, unread: false, createdAt: now, updatedAt: now, }; const snapshot = { schemaVersion: 1, conversation: { id: conversation.id, projectId: project.id, agentId: agent.id, title: conversation.title, model: { model: null, modelResolution: 'required' }, }, nodes: [], run: { status: 'idle' }, queue: { items: [] }, context: { usedTokens: 0, contextWindow: 0, compaction: 'idle' }, pendingInteractions: [], worker: { status: 'ready', generation: 1 }, cursor: { workerGeneration: 1, seq: 0 }, }; const respond = (json: unknown, status = 200) => ({ ok: true, data: { status, ok: status >= 200 && status < 300, json }, }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async ( _event, request: { path?: string; method?: string; headers?: Record; body?: unknown; }, ) => { const path = request.path ?? ''; const method = request.method ?? 'GET'; const body = typeof request.body === 'string' && request.body ? JSON.parse(request.body) as Record : undefined; const binaryBody = request.body instanceof ArrayBuffer ? new Uint8Array(request.body) : ArrayBuffer.isView(request.body) ? new Uint8Array( request.body.buffer, request.body.byteOffset, request.body.byteLength, ) : undefined; state.captured.push({ path, method, ...(body ? { body } : {}), ...(binaryBody ? { byteLength: binaryBody.byteLength } : {}), ...(request.headers?.['Content-Type'] ? { contentType: request.headers['Content-Type'] } : {}), at: Date.now(), }); if (path === '/api/coding/attachments' || /^\/api\/coding\/attachments\/[^/]+\/content$/.test(path)) { const response = await fetch(`${connection.baseUrl}${path}`, { method, headers: { Authorization: `Bearer ${connection.token}`, ...(request.headers ?? {}), }, ...(binaryBody ? { body: binaryBody } : {}), }); const responseContentType = response.headers.get('content-type') ?? ''; if (responseContentType.includes('application/json')) { return { ok: true, data: { status: response.status, ok: response.ok, json: await response.json(), transport: 'loopback', }, }; } return { ok: true, data: { status: response.status, ok: response.ok, bytes: new Uint8Array(await response.arrayBuffer()), contentType: responseContentType.split(';', 1)[0]?.trim(), transport: 'loopback', }, }; } if (path === '/api/coding/projects') { return respond({ projects: [project], activeProjectId: project.id }); } if (path === `/api/coding/projects/config?projectId=${project.id}`) { return respond({ snapshot: { project, config, knowledgeFiles: [] } }); } if (path === `/api/coding/projects/conversations?projectId=${project.id}`) { return respond({ conversations: [] }); } if (path === '/api/coding/projects/conversations' && method === 'POST') { return respond({ conversation }, 201); } if (path === `/api/coding/conversations/${conversation.id}/snapshot`) { state.snapshotPending = true; await new Promise((resolve) => { state.releaseSnapshot = resolve; }); state.snapshotPending = false; return respond({ snapshot }); } 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: 'prompt', }, }, 202); } if (path.startsWith('/api/opencode/status')) { return respond({ state: 'stopped', port: null, url: null }); } if (path === '/api/opencode/projects' || path.startsWith('/api/opencode/projects?')) { return respond({ projects: [legacyProject], activeProject: legacyProject }); } if (path === '/api/opencode/projects/active') { return respond({ projects: [legacyProject], activeProject: legacyProject }); } if (path.startsWith('/api/opencode/projects/config?')) { return respond({ status: 'valid', config: legacyConfig, knowledgeFiles: [] }); } if (path.startsWith('/api/opencode/projects/conversations?')) { return respond({ state: { schemaVersion: 1, sessions: [], updatedAt: now }, }); } return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404); }); }, hostConnection); } 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 { await electronApp.evaluate(() => { const mainGlobal = globalThis as typeof globalThis & { __makelorePiFirstChatE2E?: { releaseSnapshot: (() => void) | null }; }; mainGlobal.__makelorePiFirstChatE2E?.releaseSnapshot?.(); }); } 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 = '/opencode-chat'; }); 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 pixelPng = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64', ); await page.getByTestId('coding-file-attachment-input').setInputFiles({ name: 'pixel.png', mimeType: 'image/png', buffer: pixelPng, }); await composer.fill('Build the first PI scene'); await expect(page.getByRole('button', { name: '发送' })).toBeEnabled(); await page.getByTestId('coding-message-composer').evaluate( (form: HTMLFormElement) => form.requestSubmit(), ); await expect.poll(async () => { const state = await readState(electronApp); return { snapshotPending: state.snapshotPending, promptPosted: state.captured.some((request) => ( request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt' && request.method === 'POST' )), }; }).toEqual({ snapshotPending: true, promptPosted: true }); await expect( page.getByTestId('coding-conversation-timeline').getByText('Build the first PI scene'), ).toBeVisible(); await expect(page.getByRole('img', { name: '对话图片附件' })).toBeVisible(); await expect(page.getByText('1 条消息已被本地 Agent 接收。')).toBeVisible(); const state = await readState(electronApp); const uploads = state.captured.filter((request) => ( request.path === '/api/coding/attachments' && request.method === 'POST' )); expect(uploads).toHaveLength(1); expect(uploads[0]).toMatchObject({ byteLength: pixelPng.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(state.captured)).not.toContain(pixelPng.toString('base64')); } finally { await releaseSnapshot(electronApp); } });