import { chromium } from '@playwright/test'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer as createViteServer } from 'vite'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const memoryKey = '__bonsai_persistence_probe__'; const conversationId = '__bonsai_conversation_probe__'; const artifactId = '__bonsai_artifact_persistence_probe__'; let browser; let page; let vite; try { vite = await createViteServer({ root, logLevel: 'error', server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'persistence-probe-page', configureServer(server) { server.middlewares.use('/persistence-probe.html', (_request, response) => { response.setHeader('Content-Type', 'text/html; charset=utf-8'); response.end('Persistence probe'); }); }, }], }); await vite.listen(); const address = vite.httpServer?.address(); if (!address || typeof address === 'string') throw new Error('Vite persistence probe did not expose a TCP port'); browser = await chromium.launch({ headless: true }); page = await browser.newPage(); await page.goto(`http://127.0.0.1:${address.port}/persistence-probe.html`); await page.evaluate(async ({ memoryKey, conversationId, artifactId }) => { const idb = await import('/src/lib/idb.ts'); const { evaluateHtmlArtifact, prepareArtifactDeployment, } = await import('/src/lib/artifact-runtime.ts'); await idb.deleteMemory(memoryKey); await idb.putMemory(memoryKey, 'survives reload'); const artifact = prepareArtifactDeployment([ { path: 'index.html', content: '
Loading
', }, { path: 'src/main.js', content: 'const data = await fetch("../data/state.json").then((response) => response.json()); document.querySelector("#app").textContent = data.message;', }, { path: 'data/state.json', content: '{"message":"workspace survives reload"}' }, ], 'Persistent workspace', 'index.html', artifactId); artifact.evaluation = await evaluateHtmlArtifact(artifact); if (artifact.evaluation.status !== 'passed') { throw new Error(`Artifact setup failed: ${JSON.stringify(artifact.evaluation)}`); } const staleRecoverySnapshot = { ...artifact, files: artifact.files.map((file) => file.path === 'data/state.json' ? { ...file, content: '{"message":"stale conversation snapshot"}', bytes: new TextEncoder().encode('{"message":"stale conversation snapshot"}').byteLength, } : file), }; await idb.saveConversation({ id: conversationId, title: 'Persistence probe', modelId: 'bonsai-1.7b', toolsEnabled: true, updatedAt: 0, messages: [ { role: 'user', content: 'keep locally' }, { role: 'assistant', content: '', artifacts: [staleRecoverySnapshot] }, ], }); }, { memoryKey, conversationId, artifactId }); await page.reload({ waitUntil: 'domcontentloaded' }); const persisted = await page.evaluate(async ({ memoryKey, conversationId }) => { const idb = await import('/src/lib/idb.ts'); const { evaluateHtmlArtifact, hydrateArtifactDocument, } = await import('/src/lib/artifact-runtime.ts'); const memory = await idb.getMemory(memoryKey); const conversation = await idb.loadConversation(conversationId); const snapshot = conversation?.messages?.[1]?.artifacts?.[0]; if (!snapshot) throw new Error('Saved conversation lost its artifact snapshot'); const artifact = await hydrateArtifactDocument(snapshot); const evaluation = await evaluateHtmlArtifact(artifact); return { memory, conversation, artifact: { id: artifact.id, storage: artifact.workspaceStorage, files: artifact.files?.map(({ path, content }) => ({ path, content })), sandboxContainsCurrent: artifact.sandboxedSource.includes('workspace survives reload'), sandboxContainsStale: artifact.sandboxedSource.includes('stale conversation snapshot'), evaluation, }, }; }, { memoryKey, conversationId }); if (persisted.memory?.value !== 'survives reload') { throw new Error(`Memory did not survive reload: ${JSON.stringify(persisted.memory)}`); } if (persisted.conversation?.messages?.[0]?.content !== 'keep locally') { throw new Error(`Conversation did not survive reload: ${JSON.stringify(persisted.conversation)}`); } if (persisted.conversation?.toolsEnabled !== true) { throw new Error(`Per-chat tools state did not survive reload: ${JSON.stringify(persisted.conversation)}`); } if (persisted.artifact?.id !== artifactId || persisted.artifact.storage !== 'opfs' || persisted.artifact.files?.length !== 3 || !persisted.artifact.files.some((file) => file.path === 'data/state.json' && file.content.includes('survives reload')) || persisted.artifact.sandboxContainsCurrent !== true || persisted.artifact.sandboxContainsStale !== false || persisted.artifact.evaluation?.status !== 'passed' || persisted.artifact.evaluation?.dom?.textPreview !== 'workspace survives reload') { throw new Error(`Artifact workspace did not survive reload: ${JSON.stringify(persisted.artifact)}`); } console.log('Persistence passed: memory, per-chat tools state, conversation, and its three-file OPFS artifact survived reload and re-evaluated'); } finally { if (page) { await page.evaluate(async ({ memoryKey, conversationId, artifactId }) => { const idb = await import('/src/lib/idb.ts'); const { deleteArtifactDocumentStorage } = await import('/src/lib/artifact-runtime.ts'); await idb.deleteMemory(memoryKey).catch(() => undefined); await deleteArtifactDocumentStorage(artifactId).catch(() => undefined); const request = indexedDB.open('bonsai-webgpu-chat', 1); await new Promise((resolve) => { request.onsuccess = () => { const database = request.result; const transaction = database.transaction('conversations', 'readwrite'); transaction.objectStore('conversations').delete(conversationId); transaction.oncomplete = () => { database.close(); resolve(); }; transaction.onerror = () => { database.close(); resolve(); }; }; request.onerror = () => resolve(); }); }, { memoryKey, conversationId, artifactId }).catch(() => undefined); } await browser?.close(); await vite?.close(); }