// probe-state-membership-projection.js
//
// D1 increment 1: StateBase.projectMembers() stores keet-room membership as
// deterministic LOCAL state (the `_members` apply op → `__members__` key) that
// apply can later gate admission on (D1b) without an external lookup (Rule #3).
// Snapshot-replace handles add / role-change / remove uniformly.
import os from 'os'
import path from 'path'
import fs from 'fs'
import crypto from 'hypercore-crypto'
import { StateBase } from '../lib/state-base.js'
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'memproj-'))
let failures = 0
const check = (label, cond) => { console.log((cond ? 'PASS ' : 'FAIL ') + label); if (!cond) failures++ }
const exit = (code) => (globalThis.Bare?.exit ?? globalThis.process?.exit ?? (() => {}))(code)
async function main () {
const roomKey = crypto.hash(Buffer.from('membership-projection-test'))
const sb = new StateBase(ROOT, roomKey, { network: false })
await sb.ready()
await sb.ensureFounder() // lone daemon mints + becomes writable
check('0. base writable (lone founder)', sb.writable === true)
// 1. project two members with roles
await sb.projectMembers([{ memberId: 'alice', role: 1 }, { id: 'bob', role: 3 }])
check('1a. alice projected as admin (role 1)', (await sb.getProjectedRole('alice')) === 1)
check('1b. bob projected as peer (role 3)', (await sb.getProjectedRole('bob')) === 3)
// 2. role change (map form)
await sb.projectMembers({ alice: 1, bob: 2 })
check('2. role change propagates (bob → mod 2)', (await sb.getProjectedRole('bob')) === 2)
// 3. removal (snapshot without bob)
await sb.projectMembers({ alice: 1 })
check('3a. removed member gone (bob → null)', (await sb.getProjectedRole('bob')) === null)
check('3b. remaining member stays (alice = 1)', (await sb.getProjectedRole('alice')) === 1)
// 4. unknown id → null
check('4. unknown id → null', (await sb.getProjectedRole('mallory')) === null)
// 5. projection survives a reopen (deterministic LOCAL state, persisted)
await sb.close()
const sb2 = new StateBase(ROOT, roomKey, { network: false })
await sb2.ready()
await sb2.ensureFounder()
check('5. projected membership survives reopen (alice = 1)', (await sb2.getProjectedRole('alice')) === 1)
await sb2.close()
try { fs.rmSync(ROOT, { recursive: true, force: true }) } catch {}
console.log('\n' + (failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'))
exit(failures === 0 ? 0 : 1)
}
main().catch((e) => { console.error('THREW:', e); exit(2) })