OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / probe-state-migration.js

74 lines · javascript ref:
// Probe (room-state v2): version-gated migration v1 → v2 (NEVER lossy).
// Phase 1 writes to a v1 (random-founder) base. Phase 2 re-opens the SAME
// storage WITH a roomSeed (v2 deterministic). On boot, _migrateIfNeeded
// snapshots the v1 view and replays it onto the derived v2 base; the v1
// corestore data is never deleted. Verify the v1 writes survive on v2.
//
// Local only (network:false) — migration is an on-disk transform.
// Run: bare test/probe-state-migration.js
// PASS = after migration, v2 base.key is the derived key AND v1 keys are present.

import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import { StateBase } from '../lib/state-base.js'

const ROOM_ID = 'probe-mig-' + Math.random().toString(36).slice(2, 10)
const roomTag = crypto.hash(Buffer.from(ROOM_ID, 'utf-8'))
const roomSeed = crypto.randomBytes(32)
const dir = path.join(os.tmpdir(), 'probe-mig-' + Date.now())
fs.mkdirSync(dir, { recursive: true })

let exitCode = 1
const fail = (m) => console.error('\n✗ FAIL:', m)

try {
  // ── Phase 1: v1 base (no roomSeed) — write legacy data, capture its key ──
  console.log('[mig] phase 1: v1 base, writing legacy keys…')
  const v1 = new StateBase(dir, roomTag, { network: false })
  await v1.ready()
  await v1.set('world:old', 42.0)
  await v1.set('presence:alice', 'online')
  const v1Key = v1.stateBaseKey.toString('hex')
  console.log('[mig] v1 base.key:', v1Key.slice(0, 16) + '…')
  await v1.close().catch(() => {})

  // ── Phase 2: re-open SAME storage WITH roomSeed → migrate to v2 ──
  console.log('[mig] phase 2: re-open with roomSeed → migrate…')
  const v2 = new StateBase(dir, roomTag, { network: false, roomSeed })
  await v2.ready()
  const v2Key = v2.stateBaseKey ? v2.stateBaseKey.toString('hex') : null
  console.log('[mig] v2 base.key:', v2Key && v2Key.slice(0, 16) + '…', '(should differ from v1)')

  // give the replay (fires on writable) a moment
  for (let i = 0; i < 20; i++) {
    const a = await v2.get('world:old').catch(() => null)
    const b = await v2.get('presence:alice').catch(() => null)
    if (a && a.value === 42.0 && b && b.value === 'online') break
    await new Promise(r => setTimeout(r, 300))
  }
  const got1 = await v2.get('world:old').catch(() => null)
  const got2 = await v2.get('presence:alice').catch(() => null)
  console.log('[mig] v2.get(world:old)=', got1 && got1.value, ' v2.get(presence:alice)=', got2 && got2.value)
  const markerExists = fs.existsSync(path.join(dir, 'state-bases', roomTag.toString('hex'), 'migrated-v2'))
  console.log('[mig] migration marker written:', markerExists)

  const keyChanged = v2Key && v2Key !== v1Key
  const dataPreserved = got1 && got1.value === 42.0 && got2 && got2.value === 'online'
  if (keyChanged && dataPreserved && markerExists) {
    console.log('\n✓ migration v1→v2: base.key switched to derived key, legacy writes preserved, marker stamped')
    exitCode = 0
  } else {
    fail('keyChanged=' + keyChanged + ' dataPreserved=' + dataPreserved + ' marker=' + markerExists)
  }
  await v2.close().catch(() => {})
} catch (err) {
  fail(err.message); if (err.stack) console.error(err.stack)
} finally {
  try { fs.rmSync(dir, { recursive: true, force: true }) } catch {}
}

console.log(exitCode === 0 ? '\nPASS' : '\nFAIL')
Bare.exit(exitCode)