// Phase-2 admission-durability mechanism test (hyper-social #6).
//
// Deterministic proof that the sign/read/admission path NEVER hangs on a stalled base.update() —
// which is the live tabertron failure (get-state hung >8s; the sign loop's first read hung, so the
// re-admission doorbell never re-rang). We simulate the stall by replacing base.update() with a
// promise that NEVER resolves, then assert every liveness-critical call still returns quickly and
// the doorbell block is still appended. This isolates the FIX from the (hard-to-reproduce) exact
// convergence fork that causes the real stall — the guarantee we ship is "a stalled drain can't
// freeze these paths", and that is what this proves.
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import { StateBaseV3 } from '../lib/state-base.js'
import { Lease } from '../lib/lease.js'
import { RepoBase } from '../lib/repo-base.js'
import { makeAttest, makeVerifyAttest, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from '../lib/membership.js'
const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVA = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const DEVB = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const tmp = () => { const d = '/tmp/hpp-phase2-nb-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
// Resolve p, but fail the assertion if it takes longer than ms (i.e. it HUNG).
async function within (t, label, p, ms) {
const HUNG = Symbol('hung'); const t0 = Date.now()
const r = await Promise.race([p.then(v => ({ v })).catch(e => ({ err: e })), new Promise(res => setTimeout(() => res(HUNG), ms))])
const dt = Date.now() - t0
t.ok(r !== HUNG, `${label} returned in ${dt}ms (< ${ms}ms) — did not hang`)
return r === HUNG ? undefined : (r.err ? { threw: r.err } : r.v)
}
test('a stalled base.update() cannot freeze the read / lease / doorbell path', { timeout: 60000 }, async (t) => {
const rootPub = await rootIdentityPublicKey(ROOT)
const da = await deviceKeetKeyPair(DEVA); const pa = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: da.publicKey })
const db = await deviceKeetKeyPair(DEVB); const pb = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: db.publicKey })
// member = the NOT-lowest key, so it opens writable=false (member, no indexer present) — exactly
// the un-converged plain-writer posture tabertron reboots into.
let A = da, B = db
if (b4a.compare(A.publicKey, B.publicKey) > 0) { const t2 = A; A = B; B = t2 } // ensure A<B so B = higher key => member (opens writable=false)
const members = [
{ key: b4a.toString(A.publicKey, 'hex'), role: 1 },
{ key: b4a.toString(B.publicKey, 'hex'), role: 1 }
]
const roomSeed = crypto.randomBytes(32); const roomKey = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
const sb = new StateBaseV3(tmp(), roomKey, {
roomSeed, encryptionKey, network: false, // no swarm — isolated, so it can never converge (writable stays false)
selfKey: B.publicKey, attest: makeAttest(B, pb), verifyAttest: makeVerifyAttest(rootPub),
getMembers: async () => members
})
await sb.ready()
t.teardown(async () => { try { await sb.close() } catch {} })
t.absent(sb.base.writable, 'B opens writable=false (un-converged plain writer)')
// ── HARD-STALL base.update(): replace it with a promise that never resolves. Any code path that
// AWAITS base.update() directly (the pre-fix bug) now hangs forever; the fix must route through
// _updateBounded / snapshot reads so these calls still return.
const realUpdate = sb.base.update.bind(sb.base)
sb.base.update = () => new Promise(() => {}) // never resolves
t.teardown(() => { try { sb.base.update = realUpdate } catch {} })
// control: the explicitly non-blocking snapshot reads (already shipped) must be instant
await within(t, 'sb.getNow()', sb.getNow('nope'), 2000)
await within(t, 'sb.listNow()', sb.listNow(''), 2000)
// the sign-loop first read: Lease.current() → must be snapshot-based, not base.update()
const lease = new Lease(sb, { self: b4a.toString(B.publicKey, 'hex') })
await within(t, 'lease.current()', lease.current(), 2000)
await within(t, 'lease.holds()', lease.holds(), 2000)
// get-state's record read: RepoBase.listRecords({ now:true }) → snapshot, not base.update()
const repo = new RepoBase(sb, { did: 'did:plc:test' })
await within(t, 'repo.listRecords({now:true})', repo.listRecords(null, { now: true }), 2000)
// THE DOORBELL: requestWriter must (a) append the content-free optimistic admit block and
// (b) RETURN within its timeout even though base.update() is hung — so the sign loop keeps
// re-ringing instead of freezing. It will not become writable (isolated, no indexer), so it
// loops the full timeoutMs then returns.
const lenBefore = sb.base.local.length
const r = await within(t, 'requestWriter({timeoutMs:1500})', sb.requestWriter({ timeoutMs: 1500 }), 8000)
t.ok(r && r.writable === false, 'requestWriter returned writable=false (no indexer) — but returned, not hung')
t.ok(sb.base.local.length > lenBefore, `doorbell optimistic block was appended (local.length ${lenBefore} -> ${sb.base.local.length})`)
// ensureAdmitted (the boot re-admission trigger) must also return, and be guarded (2nd concurrent
// call is a no-op that returns immediately).
await within(t, 'ensureAdmitted()', sb.ensureAdmitted({ timeoutMs: 1200 }), 8000)
})