// state-base-v3-persist.test.js — proves StateBaseV3 game-state survives a
// full close/reopen cycle (the "does it persist to disk across restart?" test).
// Standalone (no brittle): run with `bare test/state-base-v3-persist.test.js`.
// network:false → no swarm, pure local disk round-trip.
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import { StateBaseV3 as StateBase } from '../lib/state-base-v3.js'
let failures = 0
function ok (cond, msg) { console.log((cond ? 'ok ' : 'FAIL ') + msg); if (!cond) failures++ }
const testDir = path.join(os.tmpdir(), 'sbv3-persist-' + crypto.randomBytes(4).toString('hex'))
const roomKey = crypto.randomBytes(32) // the per-room dir key (stable)
const roomSeed = crypto.randomBytes(32) // members-only secret (stable across restarts)
const encryptionKey = roomSeed
const selfKeyPair = crypto.keyPair() // our keet identity (admin)
const selfKey = selfKeyPair.publicKey
// admin (role 1) → writable creator; deterministic single-member election picks us.
const getMembers = async () => [{ key: selfKey.toString('hex'), role: 1 }]
function mk () {
return new StateBase(testDir, roomKey, {
network: false,
roomSeed,
encryptionKey,
selfKey,
selfKeyPair,
getMembers
})
}
async function main () {
// ---- session 1: write ----
let sb = mk()
await sb.ready()
ok(sb.writable, 'session1: writable=true (admin creator)')
await sb.set('world:players:me', { pos: [1, 2, 3], ts: 100 })
await sb.set('world:terrain:edit:me_0', { pos: [4, 5, 6], ext: [1, 1, 1], str: 0.5 })
await sb.set('world:creatures:registry:crawler_0', { type: 'crawler', seed: 42, pos: [7, 8, 9] })
const live = await sb.list('')
ok(Object.keys(live).length >= 3, 'session1: 3 keys visible in-session (got ' + Object.keys(live).length + ')')
ok(live['world:players:me'] && live['world:players:me'].pos[0] === 1, 'session1: player pos readable in-session')
await sb.close()
// ---- session 2: reopen SAME dir + seed (simulates restart) ----
sb = mk()
await sb.ready()
ok(sb.writable, 'session2: writable=true after reopen')
const player = await sb.get('world:players:me')
ok(player !== null && player.pos && player.pos[1] === 2, 'PERSIST: player position survived restart')
const edit = await sb.get('world:terrain:edit:me_0')
ok(edit !== null && edit.str === 0.5, 'PERSIST: terrain edit survived restart')
const creature = await sb.get('world:creatures:registry:crawler_0')
ok(creature !== null && creature.seed === 42, 'PERSIST: creature registry survived restart')
const all = await sb.list('')
ok(Object.keys(all).filter(k => k !== '__members__').length === 3, 'PERSIST: exactly 3 game keys after restart (got ' + Object.keys(all).length + ' incl __members__)')
// ---- a fresh write after reopen also persists + is readable ----
await sb.set('world:players:me', { pos: [9, 9, 9] })
const updated = await sb.get('world:players:me')
ok(updated && updated.pos[0] === 9, 'session2: new write after reopen applied (LWW by set()-stamped ts)')
await sb.close()
try { fs.rmSync(testDir, { recursive: true, force: true }) } catch {}
console.log(failures === 0 ? '\nPASS — game-state persists across restart' : '\nFAIL — ' + failures + ' assertion(s) failed')
Bare.exit(failures === 0 ? 0 : 1)
}
main().catch(err => { console.error('THREW', err && err.stack || err); Bare.exit(1) })