// 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)