SPB Git

spb/zyquo-cloud-web Public MIT

Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.

TypeScript 81.9% CSS 8.9% JavaScript 7.5% Shell 1.1% HTML 0.6%
5.6 KB · 156 lines javascript
Raw Blame History
1/*2 *  gate-phase3.mjs3 *  Zyquo Cloud Web4 *5 *  Author: Simon-Pierre Boucher6 *  Mail: contact@spboucher.ai7 *8 *  Phase 3 gate: in a real browser, verify that (1) keys + conversations9 *  survive a page reload AND a full browser restart (fresh context, same10 *  storage dir), (2) export → clear → import restores exactly, (3) the11 *  passphrase vault encrypts at rest and unlocks correctly.12 *  Usage: node scripts/gate-phase3.mjs [baseURL]13 */1415import { chromium } from 'playwright'16import { mkdtempSync } from 'node:fs'17import { tmpdir } from 'node:os'18import { join } from 'node:path'1920const baseURL = process.argv[2] ?? 'http://localhost:5173'21const userDataDir = mkdtempSync(join(tmpdir(), 'zyquo-gate3-'))22const results = []23const check = (name, ok) => {24  results.push([name, ok])25  console.log(`${ok ? 'PASS' : 'FAIL'}  ${name}`)26}2728const conversationFixture = {29  id: 'gate3-conv',30  title: 'Gate 3 conversation',31  createdAt: 1000,32  updatedAt: 2000,33  modelID: 'gpt-5.6-terra',34  provider: 'openai',35  parameters: { temperature: 0.7 },36  messages: [37    { id: 'm1', role: 'user', text: 'hello persistence', createdAt: 1000 },38    { id: 'm2', role: 'assistant', text: 'hello back', createdAt: 1500, usage: { inputTokens: 3, outputTokens: 2 } },39  ],40  pinned: true,41  hasAutoTitle: false,42}4344// --- Session 1: write data ---45let browser = await chromium.launchPersistentContext(userDataDir, { baseURL })46let page = await browser.newPage()47await page.goto(baseURL)48await page.evaluate(async (conv) => {49  const z = window.__zyquo50  z.keys.setKey('openai', 'sk-test-persistence-1234')51  z.settings.saveSettings({ ...z.settings.loadSettings(), chatFontSize: 15, theme: 'light' })52  await z.conversations.saveConversation(conv)53}, conversationFixture)5455// Reload (same session)56await page.reload()57const afterReload = await page.evaluate(async () => {58  const z = window.__zyquo59  const convs = await z.conversations.loadConversations()60  return {61    key: z.keys.getKey('openai'),62    fontSize: z.settings.loadSettings().chatFontSize,63    conv: convs.find((c) => c.id === 'gate3-conv') ?? null,64  }65})66check('keys survive reload', afterReload.key === 'sk-test-persistence-1234')67check('settings survive reload', afterReload.fontSize === 15)68check(69  'conversation survives reload (full round-trip)',70  afterReload.conv !== null &&71    afterReload.conv.messages.length === 2 &&72    afterReload.conv.messages[1].usage.outputTokens === 2 &&73    afterReload.conv.pinned === true74)7576// --- Browser restart (close everything, reopen same profile) ---77await browser.close()78browser = await chromium.launchPersistentContext(userDataDir, { baseURL })79page = await browser.newPage()80await page.goto(baseURL)81const afterRestart = await page.evaluate(async () => {82  const z = window.__zyquo83  const convs = await z.conversations.loadConversations()84  return { key: z.keys.getKey('openai'), count: convs.length, title: convs[0]?.title }85})86check('keys survive browser restart', afterRestart.key === 'sk-test-persistence-1234')87check(88  'conversations survive browser restart',89  afterRestart.count === 1 && afterRestart.title === 'Gate 3 conversation'90)9192// --- Export → clear → import ---93const roundTrip = await page.evaluate(async () => {94  const z = window.__zyquo95  const exported = await z.backup.exportAll(true)96  await z.backup.clearEverything()97  const emptyAfterClear =98    (await z.conversations.loadConversations()).length === 0 && z.keys.getKey('openai') === undefined99  const imported = await z.backup.importAll(exported)100  const convs = await z.conversations.loadConversations()101  return {102    emptyAfterClear,103    imported,104    key: z.keys.getKey('openai'),105    conv: convs.find((c) => c.id === 'gate3-conv') ?? null,106    fontSize: z.settings.loadSettings().chatFontSize,107  }108})109check('clear empties everything', roundTrip.emptyAfterClear)110check(111  'export→clear→import restores exactly',112  roundTrip.key === 'sk-test-persistence-1234' &&113    roundTrip.conv !== null &&114    roundTrip.conv.messages.length === 2 &&115    roundTrip.fontSize === 15 &&116    roundTrip.imported.conversations === 1117)118119// --- Passphrase vault ---120const vaultResult = await page.evaluate(async () => {121  const z = window.__zyquo122  await z.vault.enableVault('correct horse battery staple')123  const raw = z.keys.rawKeysRecord()124  const encryptedAtRest = raw.includes('__zyquoVault') && !raw.includes('sk-test-persistence-1234')125  const readableWhileUnlocked = z.keys.getKey('openai') === 'sk-test-persistence-1234'126  z.vault.lockVault()127  const lockedNow = z.vault.isLocked()128  let wrongPassFailed = false129  try {130    await z.vault.unlockVault('wrong passphrase')131  } catch {132    wrongPassFailed = true133  }134  await z.vault.unlockVault('correct horse battery staple')135  const unlockedKey = z.keys.getKey('openai')136  await z.vault.disableVault('correct horse battery staple')137  const plaintextAgain = z.keys.rawKeysRecord().includes('sk-test-persistence-1234')138  return { encryptedAtRest, readableWhileUnlocked, lockedNow, wrongPassFailed, unlockedKey, plaintextAgain }139})140check('vault encrypts keys at rest (AES-GCM)', vaultResult.encryptedAtRest)141check('keys readable while unlocked', vaultResult.readableWhileUnlocked)142check('lock drops the in-memory overlay', vaultResult.lockedNow)143check('wrong passphrase rejected', vaultResult.wrongPassFailed)144check('correct passphrase unlocks', vaultResult.unlockedKey === 'sk-test-persistence-1234')145check('disable restores plaintext', vaultResult.plaintextAgain)146147await browser.close()148149const failures = results.filter(([, ok]) => !ok)150if (failures.length === 0) {151  console.log(`GATE PASS: ${results.length}/${results.length} persistence checks green`)152} else {153  console.error(`GATE FAIL: ${failures.length} failures`)154  process.exit(1)155}156