// CHARACTERIZATION — cross-peer fetch when the overlay is constructed WITH a channel `key`.
//
// Why this file exists: `overlay-v2-fetch.test.js` proves cross-peer fetch works, but every peer in it
// is built WITHOUT `opts.key`, so the channel opens on id=null. PRODUCTION never does that — the
// multi-DID atproto daemon builds one overlay per DID keyed by that DID's roomKey
// (hyper-proto-peer/lib/device-node.js: `new HyperOverlay(ovStore, { namespace: 'atproto-repo-overlay:'+did,
// key: roomKey, ... })`), which is precisely what `opts.key` was added for:
//
// "Per-instance protocol channel id (opts.key) — lets N overlays coexist on ONE shared mux ...
// Without it they collide on (PROTOCOL, null) and only the first channel opens." (overlay.js)
//
// So the keyed path is the one that ships, and no test covered a keyed cross-peer FETCH. Two live
// co-owners of the same DID have been unable to move record bytes between them; this pins down whether
// the library is at fault or the daemon's wiring is.
//
// Each test is a controlled variant against the known-good baseline (test 1).
import test from 'brittle'
import Protomux from 'protomux'
import SecretStream from '@hyperswarm/secret-stream'
import { Duplex } from 'streamx'
import crypto from 'hypercore-crypto'
import { tmpStore, tmpDir, fs, path } from './helpers.js'
import { HyperOverlay } from '../lib/overlay.js'
function makeDuplex () {
let aWrite, bWrite
const a = new Duplex({ write (d, cb) { bWrite(d); cb() }, read () {} })
const b = new Duplex({ write (d, cb) { aWrite(d); cb() }, read () {} })
aWrite = (d) => a.push(d)
bWrite = (d) => b.push(d)
return [a, b]
}
// `key` mirrors production: a 32-byte roomKey buffer. null = the baseline the existing suite covers.
async function makePeer (label, key = null) {
const ov = new HyperOverlay(tmpStore('ovk-' + label), {
namespace: 'atproto-repo-overlay:did:plc:testtesttest',
key,
destDir: tmpDir('ovk-' + label + '-dest')
})
await ov.ready()
return ov
}
// Write a real file on `seeder` and register it, returning its oid.
async function seed (ov, label, bytes, registerOpts = {}) {
const dir = tmpDir('ovk-' + label + '-files')
const p = path.join(dir, 'rec.dagcbor')
fs.writeFileSync(p, bytes)
const oid = crypto.data(bytes).toString('hex')
const reg = await ov.registerFile('/records/app.bsky.feed.post/' + label, p, { size: bytes.length, ...registerOpts })
return { oid, reg, diskPath: p }
}
const ROOM_KEY = crypto.hash([Buffer.from('atproto-base/v1/room-key'), Buffer.from('did:plc:testtesttest')])
// Records are small — the live blocker was 152 bytes. Size matters: the chunker/serve path behaves
// differently for sub-chunk files than for the 200KB multi-chunk case the existing test uses.
const SMALL = Buffer.from(JSON.stringify({ $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-07-26T00:00:00.000Z' }))
test('BASELINE (control): unkeyed peers — cross-peer fetch of a SMALL record', async (t) => {
const a = await makePeer('a'); const b = await makePeer('b')
t.teardown(async () => { try { await a.close() } catch {}; try { await b.close() } catch {} })
const [sa, sb] = makeDuplex()
a.attachProtocol(Protomux.from(sa)); b.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 200))
t.is(b._protocol._peers.size, 1, 'b attached one peer')
const { oid } = await seed(a, 'a', SMALL)
const got = await b.fetchFile(oid, { destPath: path.join(tmpDir('ovk-b-out'), 'rec.dagcbor') })
t.ok(got, 'CONTROL: unkeyed cross-peer fetch of a small record works')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
test('KEYED (production shape): both peers keyed with the SAME roomKey', async (t) => {
const a = await makePeer('ka', ROOM_KEY); const b = await makePeer('kb', ROOM_KEY)
t.teardown(async () => { try { await a.close() } catch {}; try { await b.close() } catch {} })
const [sa, sb] = makeDuplex()
a.attachProtocol(Protomux.from(sa)); b.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 200))
t.is(b._protocol._peers.size, 1, 'b registered a peer (note: _peers.set happens BEFORE channel.open, so this alone does not prove the remote opened)')
const { oid, reg } = await seed(a, 'ka', SMALL)
t.ok(reg && reg.contentHash, 'seeder registered the file and got a contentHash')
const got = await b.fetchFile(oid, { destPath: path.join(tmpDir('ovk-kb-out'), 'rec.dagcbor') })
t.ok(got, 'KEYED cross-peer fetch resolves — if this fails while the control passes, opts.key breaks fetch')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
test('KEYED + prepare:false (what reindexOverlay does) — is the lazy serve path real?', async (t) => {
const a = await makePeer('pa', ROOM_KEY); const b = await makePeer('pb', ROOM_KEY)
t.teardown(async () => { try { await a.close() } catch {}; try { await b.close() } catch {} })
const [sa, sb] = makeDuplex()
a.attachProtocol(Protomux.from(sa)); b.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 200))
// registerFile's comment claims prepare:false is safe because "_onContentRequest chunks lazily on
// first request". reindexOverlay relies on that claim for every record it repairs.
const { oid } = await seed(a, 'pa', SMALL, { prepare: false })
const got = await b.fetchFile(oid, { destPath: path.join(tmpDir('ovk-pb-out'), 'rec.dagcbor') })
t.ok(got, 'a file registered with prepare:false is still servable to a peer')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
test('MISMATCHED keys must NOT resolve (proves the id actually scopes the channel)', async (t) => {
const a = await makePeer('ma', ROOM_KEY)
const b = await makePeer('mb', crypto.hash([Buffer.from('different')]))
t.teardown(async () => { try { await a.close() } catch {}; try { await b.close() } catch {} })
const [sa, sb] = makeDuplex()
a.attachProtocol(Protomux.from(sa)); b.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 200))
const { oid } = await seed(a, 'ma', SMALL)
const got = await b.fetchFile(oid, { destPath: path.join(tmpDir('ovk-mb-out'), 'rec.dagcbor'), peerWaitMs: 500 })
t.is(got, null, 'different channel ids are isolated — no cross-talk')
})
test('MULTI-DID on ONE mux: two keyed overlays per side, each fetches only its own DID', async (t) => {
// The real daemon shape: ONE connection carries N per-DID overlays (device-node.js:490 loops every
// hosted DID's overlay over the same mux). If a second keyed channel on the same mux breaks the first,
// a multi-DID node would silently lose byte transfer for some DIDs.
const KEY1 = ROOM_KEY
const KEY2 = crypto.hash([Buffer.from('atproto-base/v1/room-key'), Buffer.from('did:plc:second')])
const a1 = await makePeer('m1a', KEY1); const a2 = await makePeer('m2a', KEY2)
const b1 = await makePeer('m1b', KEY1); const b2 = await makePeer('m2b', KEY2)
t.teardown(async () => { for (const o of [a1, a2, b1, b2]) { try { await o.close() } catch {} } })
const [sa, sb] = makeDuplex()
const muxA = Protomux.from(sa); const muxB = Protomux.from(sb)
a1.attachProtocol(muxA); a2.attachProtocol(muxA) // both DIDs share ONE mux, as the daemon does
b1.attachProtocol(muxB); b2.attachProtocol(muxB)
await new Promise(r => setTimeout(r, 250))
const one = await seed(a1, 'm1a', SMALL)
const twoBytes = Buffer.from('second did record bytes')
const two = await seed(a2, 'm2a', twoBytes)
const g1 = await b1.fetchFile(one.oid, { destPath: path.join(tmpDir('ovk-m1b-out'), 'r.bin') })
const g2 = await b2.fetchFile(two.oid, { destPath: path.join(tmpDir('ovk-m2b-out'), 'r.bin') })
t.ok(g1, 'DID #1 resolved over the shared mux')
t.ok(g2, 'DID #2 resolved over the SAME mux (N overlays coexist — the reason opts.key exists)')
})
// ─────────────────────────────────────────────────────────────────────────────
// LIVE ORDERING — the variable every passing test above omits.
//
// The daemon does NOT attach to a bare duplex. hyper-proto-peer/lib/device-node.js:470-490 does:
// swarm.on('connection', conn => {
// store.replicate(conn) // corestore claims a Protomux on the stream FIRST
// const mux = Protomux.from(conn) // REUSES that protomux
// n.overlay.attachProtocol(mux) // overlay channel on the reused mux (in a silent try/catch)
// })
//
// test/overlay-v2-corestore-mux.test.js captured this exact hypothesis for issue #66 — "stortron's
// overlayV2 ends up with ZERO peers ... the overlay channel never OPENS (no sync-state exchange)" — and
// recorded that it FAILED. That file imports lib/overlay-v2.js + lib/protocol-v2.js, which no longer
// exist after the 0.3.0 rename, so it has been dead at module-load ever since and the regression was
// never guarded. These re-run it against the CURRENT modules, in the production keyed shape.
// ─────────────────────────────────────────────────────────────────────────────
async function makeKeyedPeer (label, key) {
const store = tmpStore('ovk-' + label)
const ov = new HyperOverlay(store, {
namespace: 'atproto-repo-overlay:did:plc:testtesttest',
key,
destDir: tmpDir('ovk-' + label + '-dest')
})
await ov.ready()
return { ov, store }
}
async function secretPair () {
const [da, db] = makeDuplex()
const sa = new SecretStream(true, da)
const sb = new SecretStream(false, db)
await Promise.all([
new Promise(r => sa.on('connect', r)),
new Promise(r => sb.on('connect', r))
])
return [sa, sb]
}
test('LIVE ORDERING: corestore.replicate(conn) BEFORE a KEYED attachProtocol — does the channel open?', async (t) => {
const A = await makeKeyedPeer('la', ROOM_KEY)
const B = await makeKeyedPeer('lb', ROOM_KEY)
t.teardown(async () => { try { await A.ov.close() } catch {}; try { await B.ov.close() } catch {} })
const [sa, sb] = await secretPair()
A.store.replicate(sa) // ← corestore claims the mux first, exactly as the daemon does
B.store.replicate(sb)
A.ov.attachProtocol(Protomux.from(sa)) // ← overlay channel on the REUSED mux
B.ov.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 600))
const { oid } = await seed(A.ov, 'la', SMALL)
const got = await B.ov.fetchFile(oid, { destPath: path.join(tmpDir('ovk-lb-out'), 'rec.dagcbor'), peerWaitMs: 2000 })
t.ok(got, 'DECISIVE: keyed overlay fetch under corestore-replicate-first ordering (the live daemon shape)')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
test('ORDERING CONTROL: attachProtocol BEFORE corestore.replicate (same parts, order swapped)', async (t) => {
const A = await makeKeyedPeer('oa', ROOM_KEY)
const B = await makeKeyedPeer('ob', ROOM_KEY)
t.teardown(async () => { try { await A.ov.close() } catch {}; try { await B.ov.close() } catch {} })
const [sa, sb] = await secretPair()
A.ov.attachProtocol(Protomux.from(sa)) // ← overlay first this time
B.ov.attachProtocol(Protomux.from(sb))
A.store.replicate(sa)
B.store.replicate(sb)
await new Promise(r => setTimeout(r, 600))
const { oid } = await seed(A.ov, 'oa', SMALL)
const got = await B.ov.fetchFile(oid, { destPath: path.join(tmpDir('ovk-ob-out'), 'rec.dagcbor'), peerWaitMs: 2000 })
// If this PASSES while the test above FAILS, the fault is ordering and the daemon fix is a one-liner:
// attach the overlay before handing the connection to corestore.
t.ok(got, 'overlay fetch works when the overlay attaches BEFORE corestore replicates')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
test('SECRETSTREAM ONLY: keyed attach over a SecretStream, no corestore (isolates the transport)', async (t) => {
const A = await makeKeyedPeer('sa', ROOM_KEY)
const B = await makeKeyedPeer('sb2', ROOM_KEY)
t.teardown(async () => { try { await A.ov.close() } catch {}; try { await B.ov.close() } catch {} })
const [sa, sb] = await secretPair()
A.ov.attachProtocol(Protomux.from(sa))
B.ov.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 600))
const { oid } = await seed(A.ov, 'sa', SMALL)
const got = await B.ov.fetchFile(oid, { destPath: path.join(tmpDir('ovk-sb2-out'), 'rec.dagcbor'), peerWaitMs: 2000 })
// Separates "SecretStream vs plain Duplex" from "corestore shares the mux".
t.ok(got, 'keyed fetch works over SecretStream when corestore is NOT on the mux')
if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), SMALL), 0, 'bytes match')
})
// ─────────────────────────────────────────────────────────────────────────────
// ASYMMETRY — what does peerCount look like when only ONE side attaches?
//
// Live finding: tabertron reports overlayPeers:0 for every DID, persistently, while attachProtocol
// never throws. protocol.js attach() does `_peers.set(mux, peer)` UNCONDITIONALLY before channel.open(),
// so a successful local attach should leave peerCount >= 1 no matter what the remote does — UNLESS
// protomux closes the channel when the remote has no matching (protocol, id), in which case onclose
// deletes the peer and the count falls back to 0.
//
// Those two readings demand different fixes, so pin which one the library actually does.
// ─────────────────────────────────────────────────────────────────────────────
test('ASYMMETRY: only one side attaches — does the attacher keep its peer?', async (t) => {
const A = await makeKeyedPeer('asy-a', ROOM_KEY)
const B = await makeKeyedPeer('asy-b', ROOM_KEY)
t.teardown(async () => { try { await A.ov.close() } catch {}; try { await B.ov.close() } catch {} })
const [sa, sb] = await secretPair()
// Both sides replicate corestore (so the connection is live and busy, as in prod)...
A.store.replicate(sa)
B.store.replicate(sb)
// ...but ONLY A attaches its overlay. B never does — the asymmetric case.
A.ov.attachProtocol(Protomux.from(sa))
await new Promise(r => setTimeout(r, 800))
const n = A.ov.peerCount
t.comment('A.peerCount with an unattached remote = ' + n)
// Whichever way this lands, it decodes the live 0:
// n === 1 -> a local attach SURVIVES an unattached remote, so the live 0 means the local attach
// never ran at all (look at the daemon's call path / `nodes` timing).
// n === 0 -> protomux tears the channel down when the remote has no match, so the live 0 is
// equally consistent with "the OTHER node isn't attaching" (look at stortron).
t.ok(n === 0 || n === 1, 'peerCount is a definite 0 or 1 (recorded above)')
// And the symmetric completion: once B attaches, a peer must appear on both sides.
B.ov.attachProtocol(Protomux.from(sb))
await new Promise(r => setTimeout(r, 800))
t.is(A.ov.peerCount, 1, 'A has a peer once B attaches')
t.is(B.ov.peerCount, 1, 'B has a peer once it attaches')
const { oid } = await seed(A.ov, 'asy-a', SMALL)
const got = await B.ov.fetchFile(oid, { destPath: path.join(tmpDir('ovk-asy-out'), 'r.bin'), peerWaitMs: 2000 })
t.ok(got, 'and a late attach still yields a working fetch (no permanent damage from the asymmetric phase)')
})