import type { ElectronApplication } from 'playwright-core'; import { expect, getStableWindow, test } from './fixtures/electron'; type CapturedRequest = { path: string; method: string; body?: Record; }; async function installFirstChatHost(electronApp: ElectronApplication): Promise { await electronApp.evaluate(async () => { const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron'); type MainState = { captured: CapturedRequest[]; promptPosted: boolean; releaseInitialHistory: (() => void) | null; }; const mainGlobal = globalThis as typeof globalThis & { __makeloreFirstChatE2EState?: MainState; }; const state: MainState = { captured: [], promptPosted: false, releaseInitialHistory: null, }; mainGlobal.__makeloreFirstChatE2EState = state; const project = { id: 'prj_first_chat_e2e', path: 'D:/e2e/first-chat', name: 'first-chat', createdAt: '2026-08-12T00:00:00.000Z', updatedAt: '2026-08-12T00:00:00.000Z', lastOpenedAt: '2026-08-12T00:00:00.000Z', }; const agent = { id: 'game-development', avatarId: 'avatar-01', roleName: '游戏开发', name: '游戏开发', builtIn: false, enabled: true, model: 'niancode-user-models/qwen3.7-plus', skillIds: [], responsibility: { mission: '实现并验证项目功能。', owns: [], boundaries: [], collaborators: [], principles: [], }, prompt: '', archivedAt: null, pinned: false, }; const config = { schemaVersion: 1, projectType: 'custom', initialized: true, defaultModel: 'niancode-user-models/qwen3.7-plus', agents: [agent], knowledgeDirectory: 'knowledge', createdAt: '2026-08-12T00:00:00.000Z', updatedAt: '2026-08-12T00:00:00.000Z', }; const session = { id: 'ses_first_chat_e2e', title: '新对话', agent: agent.id, updatedAt: '2026-08-12T00:00:00.000Z', }; let conversationState = { schemaVersion: 1, sessions: [] as Array>, updatedAt: '2026-08-12T00:00:00.000Z', }; const respond = (json: unknown, responseStatus = 200) => ({ ok: true, data: { status: responseStatus, ok: responseStatus >= 200 && responseStatus < 300, json, }, }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async ( _event, request: { path?: string; method?: string; body?: string | null }, ) => { const path = request.path ?? ''; const method = request.method ?? 'GET'; const body = request.body ? JSON.parse(request.body) as Record : undefined; state.captured.push({ path, method, ...(body ? { body } : {}) }); if (path === '/api/opencode/status') { return respond({ state: 'running', port: 4096, url: 'http://127.0.0.1:4096' }); } if (path === '/api/opencode/health') { return respond({ ok: true, status: { state: 'running', port: 4096, url: 'http://127.0.0.1:4096' } }); } if (path === '/api/opencode/projects' || path.startsWith('/api/opencode/projects?')) { return respond({ projects: [project], activeProject: project }); } if (path === '/api/opencode/projects/active' && method === 'GET') { return respond({ projects: [project], activeProject: project }); } if (path.startsWith('/api/opencode/projects/config?')) { return respond({ status: 'valid', config, knowledgeFiles: [] }); } if (path.startsWith('/api/opencode/projects/template?')) { return respond({ status: 'missing' }); } if (path.startsWith('/api/opencode/projects/conversations?')) { return respond({ state: conversationState }); } if (path === '/api/opencode/projects/conversations' && method === 'POST') { conversationState = { schemaVersion: 1, sessions: [{ sessionId: session.id, agentId: agent.id, archivedAt: null, unreadCount: 0, createdAt: '2026-08-12T00:00:00.000Z', updatedAt: '2026-08-12T00:00:00.000Z', }], updatedAt: '2026-08-12T00:00:00.000Z', }; return respond({ success: true, state: conversationState }); } if (path === '/api/opencode/config-summary') { return respond({ model: 'niancode-user-models/qwen3.7-plus', smallModel: null, providerIds: ['niancode-user-models'], enabledProviderIds: ['niancode-user-models'], providerCount: 1, }); } if (path === '/api/provider-accounts') { return respond([{ id: 'niancode-user-models', vendorId: 'custom', label: 'Makelore Models', authMode: 'api_key', model: 'qwen3.7-plus', enabled: true, isDefault: true, createdAt: '2026-08-12T00:00:00.000Z', updatedAt: '2026-08-12T00:00:00.000Z', }]); } if (path === '/api/provider-accounts/key-info') { return respond([{ accountId: 'niancode-user-models', hasKey: true, keyMasked: 'sk-***', }]); } if (path === '/api/provider-vendors') return respond([]); if (path === '/api/provider-accounts/default') return respond({ accountId: 'niancode-user-models' }); if (path === '/api/opencode/sessions' && method === 'POST') { return respond({ success: true, session }); } if (path === '/api/opencode/sessions') return respond({ sessions: [] }); if (path === '/api/opencode/sessions/status') { return respond({ statuses: { [session.id]: { type: state.promptPosted ? 'idle' : 'busy' }, }, }); } if (path === `/api/opencode/sessions/${session.id}/messages` && method === 'GET') { await new Promise((resolve) => { state.releaseInitialHistory = resolve; }); return respond({ messages: [] }); } if (path === `/api/opencode/sessions/${session.id}/messages` && method === 'POST') { state.promptPosted = true; return respond({ success: true }, 202); } if (path.endsWith('/todos')) return respond({ todos: [] }); if (path.endsWith('/diff')) return respond({ diffs: [] }); if (path === '/api/opencode/questions') return respond({ questions: [] }); if (path === '/api/opencode/permissions') return respond({ permissions: [] }); if (path === '/api/opencode/files/status') return respond({ files: [] }); if (path === '/api/opencode/commands') return respond({ commands: [], shareEnabled: true }); throw new Error(`Unexpected hostapi request: ${method} ${path}`); }); }); } async function capturedRequests(electronApp: ElectronApplication): Promise { return await electronApp.evaluate(() => { const mainGlobal = globalThis as typeof globalThis & { __makeloreFirstChatE2EState?: { captured: CapturedRequest[] }; }; return structuredClone(mainGlobal.__makeloreFirstChatE2EState?.captured ?? []); }); } async function releaseInitialHistory(electronApp: ElectronApplication): Promise { await electronApp.evaluate(() => { const mainGlobal = globalThis as typeof globalThis & { __makeloreFirstChatE2EState?: { releaseInitialHistory: (() => void) | null }; }; mainGlobal.__makeloreFirstChatE2EState?.releaseInitialHistory?.(); }); } test('submits the first prompt without waiting for the known-empty session history', async ({ launchElectronApp, }) => { const electronApp = await launchElectronApp({ skipSetup: true }); await installFirstChatHost(electronApp); try { let page = await getStableWindow(electronApp); await page.reload(); page = await getStableWindow(electronApp); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-programming').click(); const agent = page.getByTestId('project-agent-chat-game-development'); await agent.click(); await expect(agent).toHaveAttribute('aria-pressed', 'true'); const composer = page.getByRole('textbox'); await composer.fill('Build the first playable scene'); await expect(composer).toHaveValue('Build the first playable scene'); await page.getByTestId('opencode-message-composer').evaluate( (form: HTMLFormElement) => form.requestSubmit(), ); await expect.poll(async () => (await capturedRequests(electronApp)).map( (request) => `${request.method} ${request.path}`, )).toContain('POST /api/opencode/sessions'); await expect.poll(async () => { const requests = await capturedRequests(electronApp); const historyPending = requests.some((request) => ( request.path === '/api/opencode/sessions/ses_first_chat_e2e/messages' && request.method === 'GET' )); const promptPosted = requests.some((request) => ( request.path === '/api/opencode/sessions/ses_first_chat_e2e/messages' && request.method === 'POST' )); return { historyPending, promptPosted }; }).toEqual({ historyPending: true, promptPosted: true }); } finally { await releaseInitialHistory(electronApp); } });