// REGRESSION: a CREATOR (lowest-key device) that RESYNCS from a fresh corestore must CONVERGE to the
// co-owned DID's authoritative view WITHOUT forking. This is the wj62/stortron case the shipped resync
// suite never covered (it only resyncs the higher-key NON-creator). Root cause proven in
// test/resync-creator-fork.repro.mjs (4/4): a creator's genesis writer key is deterministic, so a fresh
// corestore re-opens the SAME writer EMPTY + immediately `writable` — and the boot-write block
// (migratePointers / restore / fresh-account author) then appends into that empty genesis core BEFORE
// replication backfills it → forks it (the df0c052a split-brain). The fix (device-node.js): on a resync
// (resyncStore present) write NOTHING at boot — converge by replication, forward-only sign() continues.
//
// This drives the REAL bootMultiDevice path across two nodes (the deployment topology), makes the creator
// stale (peer advances while it's down), resyncs it, and asserts clean convergence + continued liveness.
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 M = {
communityRoot: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
device1: 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote',
device2: 'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will'
}
const tmp = () => { const d = '/tmp/hpp-creatorresync-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function bundle (rootMnem, devMnem, handle) {
const dk = await deviceKeetKeyPair(devMnem)
const b = await provisionDevice({ rootMnemonic: rootMnem, 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 }
}
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: new Date().toISOString() } })
const has = (st, rk) => st.records.some(r => r.rkey === rk)
test('CREATOR resync from a fresh corestore converges without forking (the wj62/stortron heal)', { timeout: 300000 }, async (t) => {
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const sCre = tmp(); const sPeer = tmp()
// Co-owned DID, two admin devices. The CREATOR is the LOWEST keetPub — assign it to the node we resync.
const d1 = await bundle(M.communityRoot, M.device1, 'club.local')
const d2 = await bundle(M.communityRoot, M.device2, 'club.local')
t.is(d1.did, d2.did, 'co-owned — same DID on both nodes')
const creatorLowest = d1.keetPub < d2.keetPub
const CRE = creatorLowest ? d1 : d2 // lowest key = creator (gets resynced)
const PEER = creatorLowest ? d2 : d1 // higher key = non-creator authoritative peer (stays up)
const DID = CRE.did
const members = [{ key: d1.keetPub, role: 1 }, { key: d2.keetPub, role: 1 }]
CRE.entry.members = members; PEER.entry.members = members
let nCre = await bootMultiDevice({ storage: sCre, dids: [CRE.entry], swarmOpts, signIntervalMs: 1000 })
let nPeer = await bootMultiDevice({ storage: sPeer, dids: [PEER.entry], swarmOpts, signIntervalMs: 1000 })
let creClosed = false; let peerClosed = false
t.teardown(async () => { if (!creClosed) { try { await nCre.close() } catch {} } ; if (!peerClosed) { try { await nPeer.close() } catch {} } ; await testnet.destroy() })
const cre = () => nCre.byDid.get(DID)
let peer = nPeer.byDid.get(DID)
// ── Phase 1: both write; converge. The creator (genesis writer) is writable immediately; the peer
// must first be ADMITTED (the doorbell), so retry its write until it lands. ──
await cre().device.applyWrite(post('r-creator', 'from creator'))
let peerWritten = false
for (let i = 0; i < 60 && !peerWritten; i++) {
await cre().sb.base.update().catch(() => {}); await peer.sb.base.update().catch(() => {})
try { await peer.device.applyWrite(post('r-peer1', 'from peer 1')); peerWritten = true }
catch { await peer.sb.set('__ping__', { i }).catch(() => {}); await sleep(1000) }
}
t.ok(peerWritten, 'phase1: peer admitted (doorbell) + wrote')
for (let i = 0; i < 40; i++) { await cre().sb.base.update().catch(() => {}); await peer.sb.base.update().catch(() => {}); await sleep(400) }
t.ok(cre().sb.base.writable, 'phase1: creator is writable (genesis writer)')
let stC = await cre().device.state(); let stP = await peer.device.state()
t.ok(has(stC, 'r-creator') && has(stC, 'r-peer1'), 'phase1: creator converged both writes')
t.ok(has(stP, 'r-creator') && has(stP, 'r-peer1'), 'phase1: peer converged both writes')
// ── Phase 2: creator goes DOWN; peer ADVANCES (creator's local state is now STALE) ──
await nCre.close(); creClosed = true
await peer.device.applyWrite(post('r-peer2', 'peer advanced while creator down'))
for (let i = 0; i < 20; i++) { await peer.sb.base.update().catch(() => {}); await sleep(300) }
t.ok(has(await peer.device.state(), 'r-peer2'), 'phase2: peer advanced past the creator while it was down')
// ── Phase 3: RESYNC the CREATOR — flag its DID, reboot from a fresh side corestore ──
const { gen, sidePath } = resyncDid(sCre, DID)
t.is(gen, 1, 'phase3: creator DID flagged for fresh-corestore re-join (gen 1)')
t.is(readResyncIndex(sCre)[DID], 1, 'phase3: directive persisted')
nCre = await bootMultiDevice({ storage: sCre, dids: [CRE.entry], swarmOpts, signIntervalMs: 1000 })
creClosed = false
t.ok(fs.existsSync(sidePath), 'phase3: fresh side corestore created at the resync path')
// ── Phase 4: the resynced CREATOR (opened as a forced non-creator) converges to the peer's
// authoritative view — the deterministic genesis is now a REMOTE writer that linearizes normally. ──
let converged = null
for (let i = 0; i < 80 && !converged; i++) {
await cre().sb.base.update().catch(() => {})
const st = await cre().device.state()
if (has(st, 'r-creator') && has(st, 'r-peer1') && has(st, 'r-peer2')) converged = st
else await sleep(750)
}
t.ok(converged, 'phase4: the resynced CREATOR RE-CONVERGED to the full authoritative record set (r-creator + r-peer1 + r-peer2) — NO fork')
// ── Phase 5: liveness continues — a NEW peer write reaches the resynced creator (base is not stuck) ──
await peer.device.applyWrite(post('r-peer3', 'post-heal write'))
let live = false
for (let i = 0; i < 60 && !live; i++) { await cre().sb.base.update().catch(() => {}); live = has(await cre().device.state(), 'r-peer3'); if (!live) await sleep(750) }
t.ok(live, 'phase5: post-heal — a NEW peer write converges to the resynced creator (chain is live, not forked/frozen)')
// The heal opens the former creator as a FRESH non-creator writer (keet model): its local writer is a
// NEW key, NOT the un-sheddable deterministic genesis (== base key). It converges READ-ONLY until an
// indexer re-admits it — write-participation (signing new commits) is the deferred peer-indexer follow-up.
t.not(b4a.toString(cre().sb.base.local.key, 'hex'), b4a.toString(cre().sb.base.key, 'hex'), 'phase5: resynced creator opened a FRESH local writer (not the deterministic genesis) — the keet fresh-writer rejoin')
t.absent(cre().sb.base.writable, 'phase5: resynced creator is a read-only replica pending re-admission (write-participation = the notary revival, proven separately in rebuild-from-authoritative.test.js), NOT a re-fork')
})