import { EventEmitter, once } from 'node:events'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { startHostApiServer } from '@electron/api/server'; import type { HostApiContext } from '@electron/api/context'; import type { AgentBrowserLifecycleEvent } from '@electron/agent-browser/module'; import { PREVIEW_DATA_BUCKET_CAPACITY, PREVIEW_DATA_MAX_REQUEST_BYTES, createPreviewDataSessionManager, type PreviewDataSessionManager, } from '@electron/services/preview-data-session'; import type { DataServiceOperations } from '@electron/services/data-service-client'; import { clearWorksSquareSession, resetWorksSquareSessionForTests, storeWorksSquareSession, } from '@electron/services/works-square-session'; import type { DataServiceDocument, DataServiceHostResult } from '../../shared/data-service'; const roots: string[] = []; function success(data: T, status = 200): DataServiceHostResult { return { success: true, status, code: null, error: null, retryable: false, data }; } function operationSet(overrides: Partial>> = {}) { return { configure: vi.fn().mockResolvedValue(success(null)), inspect: vi.fn().mockResolvedValue(success(null)), listProjects: vi.fn().mockResolvedValue(success(null)), getDocument: vi.fn().mockResolvedValue(success(null)), listDocuments: vi.fn().mockResolvedValue(success(null)), putDocument: vi.fn().mockResolvedValue(success(null)), deleteDocument: vi.fn().mockResolvedValue(success(null, 204)), removeCollection: vi.fn().mockResolvedValue(success(null)), reset: vi.fn().mockResolvedValue(success(null)), removeProject: vi.fn().mockResolvedValue(success(null)), ...overrides, } as unknown as DataServiceOperations; } function request(method: string, origin: string, token: string): import('node:http').IncomingMessage { const req = new EventEmitter(); Object.assign(req, { method, headers: { origin, authorization: `Bearer ${token}`, host: '127.0.0.1', }, }); return req as import('node:http').IncomingMessage; } async function createManager( operations = operationSet(), now: () => number = () => 0, ): Promise<{ manager: PreviewDataSessionManager; operations: DataServiceOperations; projectPath: string }> { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-preview-data-')); roots.push(projectPath); const projects = { requireActiveRealProjectWithIdentity: vi.fn(async () => ({ project: { path: projectPath }, path: projectPath, projectId: '11111111-1111-4111-8111-111111111111', })), }; return { manager: createPreviewDataSessionManager({ projects, now }), operations, projectPath, }; } async function startServer(manager: PreviewDataSessionManager, operations: DataServiceOperations) { const context = { previewDataSession: manager, codingProducts: { dataService: operations, previewDataSession: manager }, } as unknown as HostApiContext; const server = startHostApiServer(context, 0); await once(server, 'listening'); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Preview test server failed to bind'); return { baseUrl: `http://127.0.0.1:${address.port}`, port: address.port, close: async () => await new Promise((resolve, reject) => { server.closeAllConnections(); server.close((error) => error ? reject(error) : resolve()); }), }; } afterEach(async () => { resetWorksSquareSessionForTests(); await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); describe('preview data session manager', () => { it('creates a fresh base64url capability bound to the active project and Origin', async () => { const { manager, projectPath } = await createManager(); const session = await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 4, }); const value = manager.getInjectionValue(13210); expect(session).toMatchObject({ projectPath, projectId: '11111111-1111-4111-8111-111111111111', origin: 'http://127.0.0.1:13210', browserGeneration: 4, }); expect(value).toMatchObject({ endpoint: 'http://127.0.0.1:13210/api/runtime/data/v1', contractVersion: 1, }); expect(value?.token).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(manager.getInjectionValue(0)).toBeNull(); manager.dispose(); expect(manager.getSnapshot()).toBeNull(); }); it('accepts HTTP/HTTPS loopback Origins, including their default ports', async () => { const { manager, projectPath } = await createManager(); for (const origin of [ 'http://localhost', 'http://localhost:80', 'https://localhost', 'https://localhost:443', 'http://127.0.0.1', 'https://127.0.0.1:443', 'http://[::1]', 'https://[::1]:443', ]) { await expect(manager.open({ projectPath, origin, browserGeneration: 1 })) .resolves.toMatchObject({ origin: new URL(origin).origin }); } await expect(manager.open({ projectPath, origin: 'https://example.com:443', browserGeneration: 1, })).rejects.toMatchObject({ code: 'invalid_origin' }); await expect(manager.open({ projectPath, origin: 'http://localhost:80/path', browserGeneration: 1, })).rejects.toMatchObject({ code: 'invalid_origin' }); }); it('requires matching browser generation before invalidating lifecycle events', async () => { const { manager, projectPath } = await createManager(); await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 2 }); manager.handleAgentBrowserLifecycle({ type: 'closed', projectId: '11111111-1111-4111-8111-111111111111', projectPath, generation: 1, url: 'http://127.0.0.1:13210/', }); expect(manager.getSnapshot()).not.toBeNull(); manager.handleAgentBrowserLifecycle({ type: 'closed', projectId: '11111111-1111-4111-8111-111111111111', projectPath, generation: 2, url: 'http://127.0.0.1:13210/', }); expect(manager.getSnapshot()).toBeNull(); }); it('invalidates on browser lifecycle events from the current generation', async () => { const { manager, projectPath } = await createManager(); const lifecycleTypes: AgentBrowserLifecycleEvent['type'][] = [ 'cross-origin-navigation', 'generation-replaced', 'detached', 'crashed', 'closed', ]; for (const type of lifecycleTypes) { await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 1 }); manager.handleAgentBrowserLifecycle({ type, projectId: '11111111-1111-4111-8111-111111111111', projectPath, generation: 1, url: 'http://127.0.0.1:13210/', }); expect(manager.getSnapshot()).toBeNull(); } }); it('charges authorized operations with a burst-30, five-per-second bucket while OPTIONS is free', async () => { const { manager, projectPath } = await createManager(undefined, () => 0); await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 1 }); const token = manager.getInjectionValue(13210)?.token; if (!token) throw new Error('Preview token was not created'); const preflight = request('OPTIONS', 'http://127.0.0.1:13210', token); for (let index = 0; index < 10; index += 1) { expect(manager.authorizeRequest(preflight)).toMatchObject({ ok: true }); } for (let index = 0; index < PREVIEW_DATA_BUCKET_CAPACITY; index += 1) { expect(manager.authorizeRequest(request('GET', 'http://127.0.0.1:13210', token))).toMatchObject({ ok: true }); } expect(manager.authorizeRequest(request('GET', 'http://127.0.0.1:13210', token))).toMatchObject({ ok: false, status: 429, code: 'rate_limited', }); }); it('invalidates on account replacement and session clear while retaining same-account refresh', async () => { storeWorksSquareSession({ accessToken: 'one', accountPartitionKey: 'a'.repeat(64) }); const { manager, projectPath } = await createManager(); await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 1 }); storeWorksSquareSession({ accessToken: 'two', accountPartitionKey: 'a'.repeat(64) }); expect(manager.getSnapshot()).not.toBeNull(); storeWorksSquareSession({ accessToken: 'three', accountPartitionKey: 'b'.repeat(64) }); expect(manager.getSnapshot()).toBeNull(); await manager.open({ projectPath, origin: 'http://127.0.0.1:13210', browserGeneration: 2 }); clearWorksSquareSession(); expect(manager.getSnapshot()).toBeNull(); }); }); describe('preview data loopback routes', () => { const document: DataServiceDocument = { id: 'todo-1', data: { done: false }, revision: 7, created_at: '2026-08-26T08:00:00Z', updated_at: '2026-08-26T08:00:00Z', }; it('dispatches direct DTO data operations before the Host gates and keeps general routes isolated', async () => { const operations = operationSet({ getDocument: vi.fn().mockResolvedValue(success(document)), listDocuments: vi.fn().mockResolvedValue(success({ items: [document], next_cursor: null, limit: 50 })), putDocument: vi.fn().mockResolvedValue(success({ ...document, revision: 8 })), deleteDocument: vi.fn().mockResolvedValue(success(null, 204)), }); const { manager, projectPath } = await createManager(operations); const server = await startServer(manager, operations); const origin = server.baseUrl; await manager.open({ projectPath, origin, browserGeneration: 1 }); const value = manager.getInjectionValue(server.port); if (!value) throw new Error('Preview token was not created'); const headers = { Origin: origin, Authorization: `Bearer ${value.token}` }; try { const get = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { headers }); expect(get.status).toBe(200); expect(get.headers.get('access-control-allow-origin')).toBe(origin); expect(get.headers.get('access-control-allow-credentials')).toBeNull(); expect(get.headers.get('etag')).toBe('"7"'); await expect(get.json()).resolves.toEqual(document); const list = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents?limit=50`, { headers }); expect(list.status).toBe(200); await expect(list.json()).resolves.toEqual({ items: [document], next_cursor: null, limit: 50 }); const put = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { method: 'PUT', headers: { ...headers, 'Content-Type': 'application/json', 'If-Match': '"7"' }, body: JSON.stringify({ data: { done: true } }), }); expect(put.status).toBe(200); expect(operations.putDocument).toHaveBeenCalledWith({ collection: 'todos', document_id: 'todo-1', data: { done: true }, if_revision: 7, }, projectPath); const deleted = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { method: 'DELETE', headers, }); expect(deleted.status).toBe(204); expect(operations.deleteDocument).toHaveBeenCalledWith({ collection: 'todos', document_id: 'todo-1', confirmed: true, }, projectPath); const options = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { method: 'OPTIONS', headers: { Origin: origin, 'Access-Control-Request-Method': 'PUT', 'Access-Control-Request-Headers': 'authorization, content-type, if-match', }, }); expect(options.status).toBe(204); expect(options.headers.get('access-control-allow-methods')).toBe('GET, PUT, DELETE, OPTIONS'); expect(options.headers.get('access-control-allow-headers')).toBe('Authorization, Content-Type, If-Match'); const general = await fetch(`${origin}/api/works/data-service/projects`, { headers }); expect(general.status).toBe(401); const projectIdInLocalPath = await fetch( `${origin}/api/runtime/data/v1/projects/11111111-1111-4111-8111-111111111111`, { headers }, ); expect(projectIdInLocalPath.status).toBe(404); } finally { manager.dispose(); await server.close(); } }); it('rejects wrong Origin, malformed headers, oversized bodies, and oversized responses locally', async () => { const oversized = { data: { payload: 'x'.repeat(1_400_000) } }; const operations = operationSet({ getDocument: vi.fn().mockResolvedValue(success({ ...document, ...oversized })), }); const { manager, projectPath } = await createManager(operations); const server = await startServer(manager, operations); const origin = server.baseUrl; await manager.open({ projectPath, origin, browserGeneration: 1 }); const token = manager.getInjectionValue(server.port)?.token; if (!token) throw new Error('Preview token was not created'); try { const wrongOrigin = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { headers: { Origin: 'http://127.0.0.1:9999', Authorization: `Bearer ${token}` }, }); expect(wrongOrigin.status).toBe(403); expect(wrongOrigin.headers.get('access-control-allow-origin')).toBeNull(); const malformedHeader = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { headers: { Origin: origin, Authorization: `Bearer ${token}`, 'X-Not-Allowed': '1', }, }); expect(malformedHeader.status).toBe(400); expect(operations.getDocument).not.toHaveBeenCalled(); const tooLarge = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { method: 'PUT', headers: { Origin: origin, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: { payload: 'x'.repeat(PREVIEW_DATA_MAX_REQUEST_BYTES) } }), }); expect(tooLarge.status).toBe(413); const responseTooLarge = await fetch(`${origin}/api/runtime/data/v1/collections/todos/documents/todo-1`, { headers: { Origin: origin, Authorization: `Bearer ${token}` }, }); expect(responseTooLarge.status).toBe(502); await expect(responseTooLarge.json()).resolves.toMatchObject({ detail: { code: 'upstream_invalid_response' }, }); } finally { manager.dispose(); await server.close(); } }); });