/* * gate-phase3.mjs * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Phase 3 gate: in a real browser, verify that (1) keys + conversations * survive a page reload AND a full browser restart (fresh context, same * storage dir), (2) export → clear → import restores exactly, (3) the * passphrase vault encrypts at rest and unlocks correctly. * Usage: node scripts/gate-phase3.mjs [baseURL] */ import { chromium } from 'playwright' import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' const baseURL = process.argv[2] ?? 'http://localhost:5173' const userDataDir = mkdtempSync(join(tmpdir(), 'zyquo-gate3-')) const results = [] const check = (name, ok) => { results.push([name, ok]) console.log(`${ok ? 'PASS' : 'FAIL'} ${name}`) } const conversationFixture = { id: 'gate3-conv', title: 'Gate 3 conversation', createdAt: 1000, updatedAt: 2000, modelID: 'gpt-5.6-terra', provider: 'openai', parameters: { temperature: 0.7 }, messages: [ { id: 'm1', role: 'user', text: 'hello persistence', createdAt: 1000 }, { id: 'm2', role: 'assistant', text: 'hello back', createdAt: 1500, usage: { inputTokens: 3, outputTokens: 2 } }, ], pinned: true, hasAutoTitle: false, } // --- Session 1: write data --- let browser = await chromium.launchPersistentContext(userDataDir, { baseURL }) let page = await browser.newPage() await page.goto(baseURL) await page.evaluate(async (conv) => { const z = window.__zyquo z.keys.setKey('openai', 'sk-test-persistence-1234') z.settings.saveSettings({ ...z.settings.loadSettings(), chatFontSize: 15, theme: 'light' }) await z.conversations.saveConversation(conv) }, conversationFixture) // Reload (same session) await page.reload() const afterReload = await page.evaluate(async () => { const z = window.__zyquo const convs = await z.conversations.loadConversations() return { key: z.keys.getKey('openai'), fontSize: z.settings.loadSettings().chatFontSize, conv: convs.find((c) => c.id === 'gate3-conv') ?? null, } }) check('keys survive reload', afterReload.key === 'sk-test-persistence-1234') check('settings survive reload', afterReload.fontSize === 15) check( 'conversation survives reload (full round-trip)', afterReload.conv !== null && afterReload.conv.messages.length === 2 && afterReload.conv.messages[1].usage.outputTokens === 2 && afterReload.conv.pinned === true ) // --- Browser restart (close everything, reopen same profile) --- await browser.close() browser = await chromium.launchPersistentContext(userDataDir, { baseURL }) page = await browser.newPage() await page.goto(baseURL) const afterRestart = await page.evaluate(async () => { const z = window.__zyquo const convs = await z.conversations.loadConversations() return { key: z.keys.getKey('openai'), count: convs.length, title: convs[0]?.title } }) check('keys survive browser restart', afterRestart.key === 'sk-test-persistence-1234') check( 'conversations survive browser restart', afterRestart.count === 1 && afterRestart.title === 'Gate 3 conversation' ) // --- Export → clear → import --- const roundTrip = await page.evaluate(async () => { const z = window.__zyquo const exported = await z.backup.exportAll(true) await z.backup.clearEverything() const emptyAfterClear = (await z.conversations.loadConversations()).length === 0 && z.keys.getKey('openai') === undefined const imported = await z.backup.importAll(exported) const convs = await z.conversations.loadConversations() return { emptyAfterClear, imported, key: z.keys.getKey('openai'), conv: convs.find((c) => c.id === 'gate3-conv') ?? null, fontSize: z.settings.loadSettings().chatFontSize, } }) check('clear empties everything', roundTrip.emptyAfterClear) check( 'export→clear→import restores exactly', roundTrip.key === 'sk-test-persistence-1234' && roundTrip.conv !== null && roundTrip.conv.messages.length === 2 && roundTrip.fontSize === 15 && roundTrip.imported.conversations === 1 ) // --- Passphrase vault --- const vaultResult = await page.evaluate(async () => { const z = window.__zyquo await z.vault.enableVault('correct horse battery staple') const raw = z.keys.rawKeysRecord() const encryptedAtRest = raw.includes('__zyquoVault') && !raw.includes('sk-test-persistence-1234') const readableWhileUnlocked = z.keys.getKey('openai') === 'sk-test-persistence-1234' z.vault.lockVault() const lockedNow = z.vault.isLocked() let wrongPassFailed = false try { await z.vault.unlockVault('wrong passphrase') } catch { wrongPassFailed = true } await z.vault.unlockVault('correct horse battery staple') const unlockedKey = z.keys.getKey('openai') await z.vault.disableVault('correct horse battery staple') const plaintextAgain = z.keys.rawKeysRecord().includes('sk-test-persistence-1234') return { encryptedAtRest, readableWhileUnlocked, lockedNow, wrongPassFailed, unlockedKey, plaintextAgain } }) check('vault encrypts keys at rest (AES-GCM)', vaultResult.encryptedAtRest) check('keys readable while unlocked', vaultResult.readableWhileUnlocked) check('lock drops the in-memory overlay', vaultResult.lockedNow) check('wrong passphrase rejected', vaultResult.wrongPassFailed) check('correct passphrase unlocks', vaultResult.unlockedKey === 'sk-test-persistence-1234') check('disable restores plaintext', vaultResult.plaintextAgain) await browser.close() const failures = results.filter(([, ok]) => !ok) if (failures.length === 0) { console.log(`GATE PASS: ${results.length}/${results.length} persistence checks green`) } else { console.error(`GATE FAIL: ${failures.length} failures`) process.exit(1) }