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 { HyperOverlayV2 } from '../lib/overlay-v2.js'
import { OverlayProtocolV2 } from '../lib/protocol-v2.js'
import { FileIndex } from '../lib/file-index.js'
import { TransferManager } from '../lib/transfer.js'
// Attach a SECOND OverlayProtocolV2 to the same mux, exactly how hyper-deploy's
// DeployServer does on the shared forge swarm (same protocol 'hyper-overlay/v2',
// keyed to a topic). This is the production mux condition the daemon's own
// HyperOverlayV2 (id=null) coexists with — the variable the clean E2E never had.
async function attachDeployStyleOverlay (store, mux, topic) {
const idx = new FileIndex(store.namespace('deploy-' + topic.toString('hex').slice(0, 6)))
await idx.ready()
const tm = new TransferManager(idx)
const stub = { _fileIndex: idx, feedKey: '0'.repeat(64), feed: { length: 0, async get () { return null } }, async markSynced () {}, async logPut () {} }
const proto = new OverlayProtocolV2(stub, tm, { key: topic, autoSync: false })
proto.attach(mux)
return proto
}
// #66 root-cause probe: the existing overlay-v2-fetch tests attach the overlay
// channel on a BARE duplex (Protomux.from(stream) with nothing else on it). The
// LIVE hyper-svc forge path is different (transports/swarm.js _onConnection):
//
// 1. corestore.replicate(conn) ← claims a Protomux on the stream +
// opens corestore's own channels FIRST
// 2. mux = Protomux.from(conn) ← REUSES that same protomux
// 3. instance.attachProtocol(mux) ← overlay channel created on the reused mux
//
// On the live forge swarm, stortron's overlayV2 ends up with ZERO peers (no
// `[proto] channel opened`, fetchFile bails at the no-peer check) while
// tabertron's overlayV2 is active. This test reproduces the live ORDERING in
// memory to prove/disprove that "corestore.replicate claims the mux first"
// breaks overlay channel pairing.
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]
}
const settle = (ms = 400) => new Promise(r => setTimeout(r, ms))
test('CONTROL: bare-mux attach pairs + fetches (matches existing passing tests)', async (t) => {
const storeA = tmpStore('ov2-ctl-a'); const storeB = tmpStore('ov2-ctl-b')
const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-ctl-a-dest') })
const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-ctl-b-dest') })
await alice.ready(); await bob.ready()
t.teardown(async () => { try { await alice.close() } catch {}; try { await bob.close() } catch {} })
const [sa, sb] = makeDuplex()
alice.attachProtocol(Protomux.from(sa))
bob.attachProtocol(Protomux.from(sb))
await settle()
t.is(alice.peerCount, 1, 'alice has 1 overlay peer')
t.is(bob.peerCount, 1, 'bob has 1 overlay peer')
const content = crypto.randomBytes(200 * 1024)
const diskPath = path.join(tmpDir('ov2-ctl-files'), 'f.bin')
fs.writeFileSync(diskPath, content)
const oid = crypto.data(content).toString('hex')
await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })
const got = await bob.fetchFile(oid)
t.ok(got && got.local === false, 'bob fetched from peer')
t.is(Buffer.compare(got.data, content), 0, 'bytes match')
})
test('LIVE ORDERING: corestore.replicate(conn) BEFORE attachProtocol — does overlay still pair?', async (t) => {
const storeA = tmpStore('ov2-live-a'); const storeB = tmpStore('ov2-live-b')
const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-live-a-dest') })
const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-live-b-dest') })
await alice.ready(); await bob.ready()
t.teardown(async () => { try { await alice.close() } catch {}; try { await bob.close() } catch {} })
// ── reproduce hyper-svc _onConnection ordering FAITHFULLY ──
// conn is a @hyperswarm/secret-stream — EXACTLY what Hyperswarm hands
// _onConnection. Then, per swarm.js: corestore.replicate(conn) FIRST, then
// Protomux.from(conn) for the overlay channel.
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))
])
storeA.replicate(sa) // corestore claims the conn's mux first
storeB.replicate(sb)
alice.attachProtocol(Protomux.from(sa)) // overlay on Protomux.from(conn)
bob.attachProtocol(Protomux.from(sb))
await settle()
// The decisive assertions: do the overlay channels pair under this ordering?
t.is(alice.peerCount, 1, 'alice overlay peer present under corestore-first ordering')
t.is(bob.peerCount, 1, 'bob overlay peer present under corestore-first ordering')
const content = crypto.randomBytes(200 * 1024)
const diskPath = path.join(tmpDir('ov2-live-files'), 'f.bin')
fs.writeFileSync(diskPath, content)
const oid = crypto.data(content).toString('hex')
await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })
// Channels register a peer, but with corestore sharing the mux the overlay
// channel never OPENS (no sync-state exchange), so the fetch can't complete.
// Bounded so the test fails fast instead of hanging. This currently FAILS —
// it documents the bug + guards the fix (it should pass once channel-open
// works under the shared-mux ordering).
const got = await bob.fetchFile(oid, { timeout: 6000 })
t.ok(got, 'bob resolved the file under live (corestore-replicate-first) ordering')
if (got) t.is(Buffer.compare(got.data, content), 0, 'bytes match')
})
// PRODUCTION MUX: the daemon's HyperOverlayV2 (id=null) coexists on the same
// forge connection with hyper-deploy's OverlayProtocolV2 (SAME protocol
// 'hyper-overlay/v2', keyed to a topic). Reproduce that second same-protocol
// channel and see whether the daemon overlay (id=null) still pairs + fetches.
test('LOADED MUX: a 2nd hyper-overlay/v2 channel (hyper-deploy-style) on the same mux', async (t) => {
const storeA = tmpStore('ov2-load-a'); const storeB = tmpStore('ov2-load-b')
const alice = new HyperOverlayV2(storeA, { namespace: 'overlay-v2', destDir: tmpDir('ov2-load-a-dest') })
const bob = new HyperOverlayV2(storeB, { namespace: 'overlay-v2', destDir: tmpDir('ov2-load-b-dest') })
await alice.ready(); await bob.ready()
const extra = []
t.teardown(async () => {
for (const p of extra) { try { p.destroy() } catch {} }
try { await alice.close() } catch {}; try { await bob.close() } catch {}
})
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))])
storeA.replicate(sa); storeB.replicate(sb)
const muxA = Protomux.from(sa)
const muxB = Protomux.from(sb)
// Each daemon, like production, runs BOTH its own HyperOverlayV2 (id=null) AND
// a hyper-deploy DeployServer overlay (id=topic) on the SAME connection. The
// two daemons' deploy overlays use DIFFERENT topics (each = its own identity),
// matching prod (tabertron's deploy topic ≠ stortron's).
const topicA = crypto.randomBytes(32)
const topicB = crypto.randomBytes(32)
extra.push(await attachDeployStyleOverlay(storeA, muxA, topicA)) // alice's deploy overlay
extra.push(await attachDeployStyleOverlay(storeB, muxB, topicB)) // bob's deploy overlay
alice.attachProtocol(muxA) // alice's daemon overlay (id=null)
bob.attachProtocol(muxB) // bob's daemon overlay (id=null)
await settle()
t.is(alice.peerCount, 1, 'alice daemon-overlay peer present with a 2nd same-protocol channel on the mux')
t.is(bob.peerCount, 1, 'bob daemon-overlay peer present with a 2nd same-protocol channel on the mux')
const content = crypto.randomBytes(2 * 1024 * 1024)
const diskPath = path.join(tmpDir('ov2-load-files'), 'f.bin')
fs.writeFileSync(diskPath, content)
const oid = crypto.data(content).toString('hex')
await alice.registerFile('/attachments/' + oid, diskPath, { contentHash: oid, size: content.length })
const gotN = await bob.fetchFile(oid, { timeout: 8000 })
t.ok(gotN, 'bob daemon-overlay (id=null) fetched DESPITE a 2nd hyper-overlay/v2 channel on the mux')
if (gotN) t.is(Buffer.compare(gotN.data, content), 0, 'bytes match')
})