OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / joiner-resync-2node.test.js

101 lines · javascript ref:
// The loser-club attempt-2 shape: a joiner whose DID is ALSO flagged for RESYNC.
//
// hyper-social#16 P1 reports that re-running the co-owner ceremony left the joiner converging NOTHING —
// base=0/0 flat for 160s, stuck rather than slow, 0 conflicts since boot — while coming up correctly as
// isCreator=false / forced-non-creator. test/coowner-join-role.test.js already covers a joiner and it
// PASSES, so the failure is in some delta between that test and production. Forensics on stortron
// (2026-07-28) found one: its statebase-resync/index.json still flags loser-club at gen 1, so attempt 2
// did NOT boot the way the passing test does — it opened the DID from a RESYNC SIDE STORE.
//
// That path is materially different: `store: resyncStore` puts the base in a corestore OUTSIDE the shared
// store.replicate(conn) mux, so it converges only via the explicit _sideStore.replicate calls in
// bootMultiDevice. Nothing exercised that end to end over a real network until this test.
//
// Deliberately mirrors production rather than the convenient case: the resync flag is written BEFORE boot,
// exactly as `hyper-proto-peer resync-did` leaves it.
//
// Run: bare test/joiner-resync-2node.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { bootMultiDevice, provisionDevice, resyncDid, readResyncIndex } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'

const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'
const DEV_A = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote'
const DEV_B = 'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will'

const tmp = () => { const d = '/tmp/hpp-joinresync-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: '2026-07-27T00:00:00.000Z' } })
const keys = async (n) => (await n.device.state()).records.map((r) => r.collection + '/' + r.rkey).sort()
const waitFor = async (fn, ms = 60000) => { const end = Date.now() + ms; while (Date.now() < end) { if (await fn()) return true; await sleep(500) } return false }

async function bundle (devMnem, handle) {
  const dk = await deviceKeetKeyPair(devMnem)
  const b = await provisionDevice({ rootMnemonic: ROOT, devicePublicKey: dk.publicKey, handle, pdsUrl: 'https://pds.local' })
  return { entry: { deviceMnemonic: devMnem, did: b.did, signingKey: b.signingKey, deviceProof: b.deviceProof, rootIdentityKey: b.rootIdentityKey, handle }, keetPub: b4a.toString(dk.publicKey, 'hex'), did: b.did }
}

// Incumbent = the HIGHER key, so the joiner sorts below it and the election would flip the role if the
// explicit joiner posture were not honoured — the same hazard the ceremony hit in production.
async function roles () {
  const a = await bundle(DEV_A, 'club.local')
  const b = await bundle(DEV_B, 'club.local')
  const joinerIsA = a.keetPub < b.keetPub
  return { INC: joinerIsA ? b : a, JOIN: joinerIsA ? a : b, did: a.did }
}

test('a joiner flagged for RESYNC converges the incumbent (side store over a real connection)', async (t) => {
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }
  const { INC, JOIN, did } = await roles()
  const members = [{ key: INC.keetPub, role: 1 }, { key: JOIN.keetPub, role: 1 }]

  // Incumbent: pinned creator, holding real content.
  const sInc = tmp()
  const nInc = await bootMultiDevice({ storage: sInc, dids: [{ ...INC.entry, members, forceCreator: true }], swarmOpts, signIntervalMs: 1000 })
  t.teardown(async () => { try { await nInc.close() } catch {} ; await testnet.destroy() })
  const inc = nInc.byDid.get(did)
  await inc.device.applyWrite(post('3aaa', 'incumbent one'))
  await inc.device.applyWrite(post('3bbb', 'incumbent two'))
  await waitFor(async () => (await inc.device.state()).recordCount >= 2)
  const incKeys = await keys(inc)
  t.ok(incKeys.includes('app.bsky.feed.post/3aaa'), 'incumbent holds its records')

  // Joiner: flag the DID for resync BEFORE boot, exactly as `hyper-proto-peer resync-did` leaves it, so
  // the daemon opens it from a fresh side corestore instead of the shared store.
  const sJoin = tmp()
  const { gen, sidePath } = resyncDid(sJoin, did)
  t.is(gen, 1, 'resync flag written (gen 1) — the production attempt-2 state')
  t.alike(readResyncIndex(sJoin), { [did]: 1 }, 'and it is what the daemon will read at boot')

  const nJoin = await bootMultiDevice({ storage: sJoin, dids: [{ ...JOIN.entry, members, joiner: true }], swarmOpts, signIntervalMs: 1000 })
  t.teardown(async () => { try { await nJoin.close() } catch {} })
  const join = nJoin.byDid.get(did)

  t.ok(fs.existsSync(sidePath), 'the DID really did open from the side corestore')
  t.ok(join._sideStore, 'and the node reports a _sideStore for the daemon to replicate explicitly')

  const jdiag = await join.device.membersDiag()
  t.is(jdiag.amCreator, false, 'the joiner is not the creator')
  t.is(jdiag.forcedRole, 'forced-non-creator', 'forced non-creator (resync and joiner both demand it)')

  // THE ASSERTION THIS TEST EXISTS FOR: does a side-store joiner actually converge, or sit at 0/0?
  const converged = await waitFor(async () => {
    const s = await join.device.state()
    return s.records.some((r) => r.rkey === '3aaa') && s.records.some((r) => r.rkey === '3bbb')
  }, 90000)
  const jstate = await join.device.state()
  const jdiag2 = await join.device.membersDiag()
  t.ok(converged, `the side-store joiner CONVERGED (base=${jdiag2.indexedLength}/${jdiag2.baseLength}, records=${jstate.recordCount})`)
  t.alike(await keys(join), incKeys, 'and its record set is EXACTLY the incumbent\'s — no rival line')

  // Live afterwards: a write on the incumbent must reach it.
  await inc.device.applyWrite(post('3ccc', 'after the join'))
  t.ok(await waitFor(async () => (await join.device.state()).records.some((r) => r.rkey === '3ccc'), 90000),
    'and a post-join write on the incumbent replicates to the side-store joiner')
})