OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room / test / doorbell-durable-write.test.js

74 lines · javascript ref:
// 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 })