Files
makelore/tests/e2e/work-entry-restoration.spec.ts
T

75 lines
5.0 KiB
TypeScript

import type { Page } from '@playwright/test';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';
import { expect, getStableWindow, test } from './fixtures/electron';
import type { WorkPreviewResult } from '../../shared/coding-work-preview';
async function hostRequest<T>(page: Page, path: string, body?: unknown): Promise<T> {
const response = await page.evaluate(async (request) => await window.electron.ipcRenderer.invoke('hostapi:fetch', request) as {
ok: boolean; data?: { ok: boolean; json?: unknown };
}, { path, method: body === undefined ? 'GET' : 'POST', body });
expect(response, `${path}: ${JSON.stringify(response)}`).toMatchObject({ ok: true, data: { ok: true } });
return response.data!.json as T;
}
test('existing work survives a full app restart and restarts a stopped service without choosing a model', async ({ launchElectronApp, electronTestResources }) => {
const projectPath = join(electronTestResources.rootDir, 'remembered-work');
await mkdir(projectPath);
const html = '<!doctype html><title>Remembered project work</title><h1>My existing game</h1>';
await writeFile(join(projectPath, 'index.html'), html);
const server = createServer((_request, response) => {
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.end(html);
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Missing test port');
const url = `http://127.0.0.1:${address.port}/`;
let serverClosed = false;
try {
const first = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(first);
const { project } = await hostRequest<{ project: { id: string } }>(page, '/api/coding/projects/open', { projectPath });
const listPath = `/api/coding/projects/conversations?project_id=${project.id}`;
const before = await hostRequest<{ conversations: unknown[] }>(page, listPath);
await page.evaluate(() => window.electron.ipcRenderer.invoke('lifecycle:activity', { visible: true, module: 'programming' }));
await hostRequest(page, '/api/agent-browser/open', { project_id: project.id, url, visible: false,
bounds: { x: 20, y: 70, width: 420, height: 300 } });
const record = JSON.parse(await readFile(join(projectPath, '.makelore/work-entry.json'), 'utf8'));
expect(record).toMatchObject({ projectId: project.id, url, launch: { kind: 'static', directory: '.', entry: 'index.html' } });
await first.close();
const restarted = await launchElectronApp({ skipSetup: true });
page = await getStableWindow(restarted);
await hostRequest(page, '/api/coding/projects/open', { projectPath });
const restored = await hostRequest<WorkPreviewResult>(page, '/api/agent-browser/ensure-work', { project_id: project.id, request_id: 'app-restarted' });
expect(restored).toMatchObject({ status: 'ready', browser: { url, state: 'attached' } });
expect(await hostRequest(page, listPath)).toEqual(before);
expect(await restarted.evaluate(({ webContents }, expectedUrl) => webContents.getAllWebContents().some((contents) => contents.getURL() === expectedUrl && contents.getTitle() === 'Remembered project work'), url)).toBe(true);
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
serverClosed = true;
const started = await hostRequest<WorkPreviewResult>(page, '/api/agent-browser/ensure-work', { project_id: project.id, request_id: 'service-stopped' });
expect(started).toMatchObject({ status: 'ready', browser: { state: 'attached' } });
expect(await (await fetch(started.browser!.url)).text()).toBe(html);
expect(await hostRequest(page, listPath)).toEqual(before);
// Use the real native presentation route; this fixture has no authenticated
// cloud account and must not fabricate one to enter the guarded Code UI.
await hostRequest(page, '/api/agent-browser/present', { project_id: project.id, visible: true,
bounds: { x: 20, y: 70, width: 420, height: 300 } });
expect(await restarted.evaluate(async ({ webContents }, expectedUrl) => {
const contents = webContents.getAllWebContents().find((item) => item.getURL() === expectedUrl);
return await contents?.executeJavaScript('document.querySelector("h1")?.textContent');
}, started.browser!.url)).toBe('My existing game');
await page.screenshot({ path: test.info().outputPath('restored-work-without-model.png') });
} finally {
if (!serverClosed) await new Promise<void>((resolve) => server.close(() => resolve()));
const log = await readFile(join(projectPath, '.makelore/logs/static-preview.log'), 'utf8').catch(() => '');
for (const match of log.matchAll(/Makelore launch pid=(\d+) /g)) {
const pid = Number(match[1]);
try { process.kill(process.platform === 'win32' ? pid : -pid, 'SIGTERM'); } catch { /* Test child has already exited. */ }
}
}
});