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();
}