// 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')
})