// REPRO of the canary failure: in the MULTI-DID daemon path the StateBases run network:false (the
// daemon owns ONE swarm + store.replicate + wakeup and drives replication — see hpp device-node
// bootMultiDevice). So StateBaseV3's OWN swarm connection handler NEVER runs, and the __members__
// re-projection connection-hook I added there NEVER FIRES. Only the promote-timer path is left.
//
// This test mirrors multi-did.test.js's daemon wiring (network:false + external store.replicate +
// shared wakeup) and the FIX scenario (a single projectMembers call that NO-OPS at first because the
// creator isn't writable yet). It asserts what the canary saw: the member is NOT admitted via the
// connection-hook (it can't fire). The fix is to ALSO drive _ensureMembersProjected from the daemon's
// connection handler (device-node) AND make the timer path robust.
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import Hyperswarm from 'hyperswarm'
import ProtomuxWakeup from 'protomux-wakeup'
import createTestnet from '@hyperswarm/testnet'
import { StateBaseV3 } from '../lib/state-base.js'
import { openCorestore } from 'atproto-store/lib/corestore.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-mdid-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function setup () {
const rootPub = await rootIdentityPublicKey(ROOT)
const da = await deviceKeetKeyPair(DEVA); const pa = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: da.publicKey })
const db = await deviceKeetKeyPair(DEVB); const 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 }
const members = [{ key: b4a.toString(A.dev.publicKey, 'hex'), role: 1 }, { key: b4a.toString(B.dev.publicKey, 'hex'), role: 1 }]
const roomKey = crypto.randomBytes(32); const roomSeed = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
// network:false — exactly the multi-DID daemon mode. NO StateBase swarm ⇒ NO connection-hook.
const mk = (dir, dev, proof, store, wakeup) => new StateBaseV3(null, roomKey, {
store, wakeup, network: false, roomSeed, encryptionKey,
selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
getMembers: async () => members, promoteRetryMs: 2000
})
return { A, B, members, mk, roomKey }
}
// daemon-style wiring: ONE swarm, store.replicate + wakeup.addStream per conn (like device-node bootMultiDevice)
function wireDaemon (swarm, store, wakeup) {
swarm.on('connection', (conn) => {
conn.on('error', () => {})
try { wakeup.addStream(conn) } catch {}
try { store.replicate(conn) } catch {}
})
}
test('REPRO: multi-DID network:false ⇒ connection-hook cannot fire; timer path alone must admit', { timeout: 120000 }, async (t) => {
const testnet = await createTestnet(3)
const { members, mk, A, B, roomKey } = await setup()
const storeC = openCorestore(tmp()); await storeC.ready(); const wkC = new ProtomuxWakeup()
const storeM = openCorestore(tmp()); await storeM.ready(); const wkM = new ProtomuxWakeup()
const swarmC = new Hyperswarm({ bootstrap: testnet.bootstrap })
const swarmM = new Hyperswarm({ bootstrap: testnet.bootstrap })
const sbA = mk('C', A.dev, A.proof, storeC, wkC)
// single projectMembers BEFORE ready (no-op; retains roster) — models the creator not-writable-yet.
await sbA.projectMembers(members)
await sbA.ready()
const sbB = mk('M', B.dev, B.proof, storeM, wkM); await sbB.ready()
wireDaemon(swarmC, storeC, wkC)
wireDaemon(swarmM, storeM, wkM)
swarmC.join(sbA.base.discoveryKey, { server: true, client: true })
swarmM.join(sbA.base.discoveryKey, { server: true, client: true })
await swarmC.flush().catch(() => {}); await swarmM.flush().catch(() => {})
t.teardown(async () => { try { await sbA.close(); await sbB.close(); await swarmC.destroy(); await swarmM.destroy() } catch {} ; await testnet.destroy() })
// Give the timer path (2s promoteRetryMs) chances + drive updates like the daemon does. Log the
// creator's __members__/projected state each round so we see whether the TIMER ever projects.
let admitted = false
for (let i = 0; i < 12 && !admitted; i++) {
const r = await sbB.requestWriter({ timeoutMs: 2000 }).catch(() => ({ writable: false }))
admitted = !!r.writable
const mv = await sbA.getNow('__members__').catch(() => null)
console.log(`[repro] round ${i}: creatorWritable=${sbA.base && sbA.base.writable} __members__=${mv ? 'present' : 'ABSENT'} membersProjected=${sbA._membersProjected} roster=${sbA._roster ? sbA._roster.length : 'null'} memberAdmitted=${admitted}`)
if (!admitted) { await sbA.base.update().catch(() => {}); await sbB.base.update().catch(() => {}); await sleep(500) }
}
const membersInView = await sbA.get('__members__').catch(() => null)
console.log('[repro] __members__ in creator view:', membersInView ? Object.keys(membersInView || {}).length : 'ABSENT', '| member admitted:', admitted)
t.comment('membersInView=' + JSON.stringify(membersInView) + ' membersProjected=' + sbA._membersProjected + ' writable=' + (sbA.base && sbA.base.writable))
// This is the DIAGNOSIS assertion — record the actual behavior. If admitted=true the timer path
// suffices (bug is elsewhere); if false, the connection-hook was load-bearing and multi-DID needs
// the daemon to drive _ensureMembersProjected.
t.ok(true, 'diagnostic recorded: admitted=' + admitted + ' __members__=' + (membersInView ? 'present' : 'ABSENT'))
})