OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / joiner-resync-multidid.test.js

122 lines · javascript ref:
// The FULL loser-club attempt-2 topology: a resync-flagged joiner on the SECOND DID of a multi-DID daemon.
//
// test/joiner-resync-2node.test.js proved a resync side-store joiner converges when it is the ONLY hosted
// DID. Production is not that shape. stortron hosts wj62 (primary, listed FIRST) and loser-club second, so
// by the time loser-club boots the daemon already has open connections to the peer — and its side store
// must be attached to those EXISTING connections by the catch-up loop in bootMultiDevice:
//
//     const node = await bootSwarmDevice(...)
//     swarm.join(node.sb.base.discoveryKey, ...)
//     if (node._sideStore) for (const conn of swarm.connections) node._sideStore.replicate(conn)
//
// A single-DID test never reaches that loop — there are no connections yet when the only DID boots, so the
// forward path in the connection handler covers everything. Here both DIDs boot in ONE call with the
// primary first (a DID cannot be added to a running daemon; production edits the config and restarts,
// which is exactly this), so the room boots into a daemon that has already joined another topic.
//
// Whether a connection is actually open by then is a real race — DHT lookup vs the second DID's boot — so
// this test does not assert which path attached the side store. It asserts the OUTCOME production did not
// get: convergence. A pass narrows the cause; a failure hands over a live repro.
//
// hyper-social#16 P1. Run: bare test/joiner-resync-multidid.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 } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'

// Two ROOTS => two distinct DIDs (a DID derives from its root identity).
const ROOT_ROOM = '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 ROOT_OTHER = 'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title'
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 DEV_C = 'letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless'
const DEV_D = 'letter advice cage absurd amount doctor acoustic avoid letter advice cage above'

const tmp = () => { const d = '/tmp/hpp-mdid-' + 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 (root, 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 = HIGHER key so the joiner sorts below it — the role hazard the ceremony hit in production.
async function pair (root, handle) {
  const a = await bundle(root, DEV_A, handle)
  const b = await bundle(root, DEV_B, handle)
  const joinerIsA = a.keetPub < b.keetPub
  return { INC: joinerIsA ? b : a, JOIN: joinerIsA ? a : b, did: a.did }
}

test('multi-DID: a resync joiner on the SECOND DID converges after connections are already open', async (t) => {
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }

  const room = await pair(ROOT_ROOM, 'club.local')                      // the loser-club analog
  const oInc = await bundle(ROOT_OTHER, DEV_C, 'other.local')           // the wj62 analog, incumbent side
  const oJoin = await bundle(ROOT_OTHER, DEV_D, 'other.local')          // ...and joiner side
  const roomMembers = [{ key: room.INC.keetPub, role: 1 }, { key: room.JOIN.keetPub, role: 1 }]
  const otherMembers = [{ key: oInc.keetPub, role: 1 }, { key: oJoin.keetPub, role: 1 }]
  t.not(room.did, oInc.did, 'precondition: two DISTINCT hosted DIDs')

  // ── Incumbent daemon: hosts BOTH DIDs, the "primary" one FIRST (production's config order).
  const sInc = tmp()
  const nInc = await bootMultiDevice({
    storage: sInc,
    dids: [
      { ...oInc.entry, members: otherMembers, forceCreator: true, primary: true },
      { ...room.INC.entry, members: roomMembers, forceCreator: true }
    ],
    swarmOpts,
    signIntervalMs: 1000
  })
  t.teardown(async () => { try { await nInc.close() } catch {} ; await testnet.destroy() })
  const inc = nInc.byDid.get(room.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)

  // ── Joiner daemon: BOTH DIDs in one boot, primary FIRST — production's config order. The DIDs boot
  // sequentially inside bootMultiDevice, so by the time the room (second) boots the daemon has already
  // joined the first DID's topic and may already hold open connections to the peer. When it does, the
  // room's side store is attached by the CATCH-UP loop rather than by the connection handler — the half a
  // single-DID test can never reach. (A DID cannot be added to a running daemon: production edits the
  // config and restarts, which is exactly this.)
  const sJoin = tmp()
  const { gen } = resyncDid(sJoin, room.did)
  t.is(gen, 1, 'room flagged for resync BEFORE boot — the production attempt-2 state')

  const nJoin = await bootMultiDevice({
    storage: sJoin,
    dids: [
      { ...oJoin.entry, members: otherMembers, joiner: true, primary: true },
      { ...room.JOIN.entry, members: roomMembers, joiner: true }
    ],
    swarmOpts,
    signIntervalMs: 1000
  })
  t.teardown(async () => { try { await nJoin.close() } catch {} })

  t.is(nJoin.nodes.length, 2, 'the joiner daemon hosts BOTH DIDs, as production does')
  const join = nJoin.byDid.get(room.did)
  t.ok(join && join._sideStore, 'the room opened from a side store (resync), as production did')
  t.ok(nJoin.byDid.get(oInc.did), 'and the other DID is hosted alongside it on the same swarm + store')

  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 d = await join.device.membersDiag()
  const st = await join.device.state()
  t.ok(converged, `the side-store room CONVERGED over pre-existing connections (base=${d.indexedLength}/${d.baseLength}, records=${st.recordCount})`)
  t.alike(await keys(join), incKeys, 'and its record set is EXACTLY the incumbent\'s — no rival line')
})