// Regression for the optimistic-content SILENT-LOSS fix (#133 follow-up, 2026-07-12).
// StateBaseV3.set() must NOT fuse durable content into a pre-admission optimistic block. Instead it
// drives a content-free admission doorbell, blocks until genuinely admitted, then writes content
// non-optimistically. Two guarantees:
// 1) a SINGLE set() from a not-yet-admitted device (indexer online) is DURABLE and the AUTHOR can
// read it back — before the fix, B.get() returned null (rolled-back optimistic block).
// 2) set() with NO indexer reachable THROWS (never a silent success on a write that won't commit).
// See memory/project_statebase-optimistic-content-silent-loss-2026-07-12 + skills/autobase.
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { StateBaseV3 } from '../lib/state-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-doorbell-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function setup (testnet) {
const rootPub = await rootIdentityPublicKey(ROOT)
let da = await deviceKeetKeyPair(DEVA); let pa = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: da.publicKey })
let db = await deviceKeetKeyPair(DEVB); let pb = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: db.publicKey })
let A = { dev: da, proof: pa }, B = { dev: db, proof: pb }
if (b4a.compare(A.dev.publicKey, B.dev.publicKey) > 0) { const t = A; A = B; B = t } // creator = lowest key
const members = [
{ key: b4a.toString(A.dev.publicKey, 'hex'), role: 1 },
{ key: b4a.toString(B.dev.publicKey, 'hex'), role: 1 }
]
const roomSeed = crypto.randomBytes(32); const roomKey = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
const mk = (dev, proof) => new StateBaseV3(tmp(), roomKey, {
roomSeed, encryptionKey, swarmOpts: { bootstrap: testnet.bootstrap },
selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
getMembers: async () => members
})
return { A, B, members, mk }
}
test('a single pre-admission set() is DURABLE + author reads it back (no optimistic-content loss)', { timeout: 120000 }, async (t) => {
const testnet = await createTestnet(3)
const { members, mk, A, B } = await setup(testnet)
const sbA = mk(A.dev, A.proof); await sbA.ready(); await sbA.projectMembers(members)
const sbB = mk(B.dev, B.proof); await sbB.ready()
t.teardown(async () => { try { await sbA.close(); await sbB.close() } catch {} ; await testnet.destroy() })
// ONE set() from B (not yet admitted). The doorbell blocks until admitted, then writes durably.
await sbB.set('post/1', { text: 'important-post' })
t.ok(sbB.base.writable, 'B became a genuinely admitted writer (blocked until admission)')
for (let i = 0; i < 12; i++) { await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(300) }
const bOwn = await sbB.get('post/1')
t.alike(bOwn, { text: 'important-post' }, 'B reads its OWN write back (was null before the fix)')
const aSaw = await sbA.get('post/1')
t.alike(aSaw, { text: 'important-post' }, 'A converged with B\'s durable write')
})
test('set() with NO indexer reachable THROWS (never silently succeeds)', { timeout: 60000 }, async (t) => {
const testnet = await createTestnet(3)
const { mk, B } = await setup(testnet)
const sbB = mk(B.dev, B.proof); await sbB.ready() // NO A (indexer) started
t.teardown(async () => { try { await sbB.close() } catch {} ; await testnet.destroy() })
await t.exception(
() => sbB.set('post/1', { text: 'important-post' }),
/not admitted as a writer|silently lost/,
'set() throws instead of silently losing the write when no indexer can admit us'
)
t.absent(await sbB.get('post/1'), 'nothing was written')
}, { timeout: 60000 })