// ★ Bytes over hyper-overlay — the half of replication the log does not carry.
//
// Until now the replication tests SHARED a records directory between two peers, standing in for
// this. That stand-in is what made "a record arrived" ambiguous: the log converges pointers, and a
// peer with the log and no byte path holds a pointer it cannot resolve. Here the two peers have
// SEPARATE directories and the bytes have to actually travel.
//
// The overlay rides the same connection as the feed, over Protomux — one socket, both protocols,
// which is how the deployed peer does it (hyper-proto-peer device-node.js).
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import Protomux from 'protomux'
import { HyperOverlay } from 'hyper-overlay'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { POST } from '../lib/peer.js'
import { Duplex } from 'streamx'
import { attest } from './helpers.js'
// A connected pair of duplexes, standing in for one socket between two peers.
function duplexPair () {
const a = new Duplex({ write (data, cb) { b.push(data); cb() }, final (cb) { b.push(null); cb() } })
const b = new Duplex({ write (data, cb) { a.push(data); cb() }, final (cb) { a.push(null); cb() } })
return [a, b]
}
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-ov-'))
const rec = (body) => ({ frontmatter: { createdAt: '2026-09-06T00:00:00Z' }, body })
async function until (fn, { timeout = 15000 } = {}) {
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
const v = await fn()
if (v) return v
await new Promise((r) => setTimeout(r, 50))
}
return null
}
// A feed plus its own overlay, on its own corestore and its own records dir.
async function node (key = null, channelKey = null) {
const store = new Corestore(tmp())
const root = tmp()
// `key` is the per-instance protocol CHANNEL ID, not a crypto key: it lets N overlays coexist on
// ONE mux — "without it they collide on (PROTOCOL, null) and only the first channel opens"
// (overlay.js:79-83). Passed here because that is the real topology (one overlay per feed sharing
// a connection), and both sides must name the same channel to pair.
//
// ⚠ It is NOT what made this test pass. Measured by removing it: still green. The failure it was
// blamed for was self-inflicted — an edit had replaced `attachProtocol` with a call to a
// `replicate` method hyper-overlay does not have, so the overlays were never connected at all and
// the bytes had no path. A missing key would matter with several overlays on one mux; there is
// one here.
const overlay = new HyperOverlay(store.namespace('overlay'), {
namespace: 'favela-records',
key: channelKey,
destDir: join(root, '.fetch')
})
await overlay.ready()
const feed = new Feed(store.namespace('feed'), key, {
store: new RecordStore(root, { overlay })
})
await feed.ready()
return { store, root, overlay, feed }
}
// ⚠ SKIPPED, with a diagnosis rather than a shrug. The bytes do not arrive and the requester just
// hangs — which is the documented signature of hyper-overlay's silent-drop path: when
// `_onContentRequest` cannot resolve a hash in its index it drops the request, and there is no
// per-request timeout (`protocol-v2.js:430-432`, and the serve map is IN-MEMORY, populated only by
// registerFile — `project_hyper-overlay-serve-map-lost-on-restart-2026-07-25`).
//
// The most likely cause is this constructor, not the protocol: the deployed peer passes
// `{ namespace, key: roomKey, destDir }` and this passes no `key`, so the two overlays may never
// pair on a channel at all. `overlay.js:189` logs exactly that case ("attachProtocol did NOT attach
// — this connection will move NO bytes"), and checking that log is step one.
//
// Unskip when the pairing is understood. It is the last unproven link in the MVP.
test('★ a record\'s BYTES cross the wire, with separate stores on each side', async (t) => {
const channel = Buffer.alloc(32, 7) // the same channel on both sides; in production, the feed key
const a = await node(null, channel)
await a.feed.addDevice(await attest(a.feed))
await a.feed.update()
await a.feed.put(POST, 'p1', rec('these bytes had to travel'))
await a.feed.update()
const b = await node(a.feed.key, channel)
t.teardown(async () => { await a.feed.close(); await b.feed.close() })
// ONE raw socket per side, both protocols on it — the deployed shape (device-node.js attaches the
// overlay to Protomux.from(conn) where conn is the swarm connection the cores also replicate over).
// Attaching a mux over the base's own replication stream instead does not connect the two
// overlays: the stream already carries hypercore's mux, and wrapping it makes a second one.
// TWO connections: the cores replicate over one, the overlay attaches its protocol to a Protomux
// on the other. The DEPLOYED shape muxes both onto a single swarm socket
// (`overlay.attachProtocol(Protomux.from(conn))`, hyper-proto-peer device-node.js) — that is a
// transport concern and is exercised in production, not here. What this proves is the byte path.
const f1 = a.feed.replicate(true)
const f2 = b.feed.replicate(false)
f1.pipe(f2).pipe(f1)
const [sock1, sock2] = duplexPair()
a.overlay.attachProtocol(Protomux.from(sock1))
b.overlay.attachProtocol(Protomux.from(sock2))
t.teardown(() => { f1.destroy(); f2.destroy(); sock1.destroy(); sock2.destroy() })
const got = await until(async () => {
await b.feed.update()
return b.feed.get(POST, 'p1')
})
t.ok(got, 'the record resolved on a peer that never had the file')
t.is(got && got.body, 'these bytes had to travel\n', 'byte-identical')
t.absent(a.root === b.root, 'and it was NOT a shared directory')
})
test('the pointer carries a real oid once an overlay is wired', async (t) => {
const a = await node()
await a.feed.addDevice(await attest(a.feed))
await a.feed.update()
await a.feed.put(POST, 'p1', rec('content addressed'))
await a.feed.update()
t.teardown(() => a.feed.close())
const r = await a.feed.resolve(POST, 'p1')
t.is(r.status, 'ok')
t.ok(typeof r.pointer.$oid === 'string' && r.pointer.$oid.length > 0,
'an oid, where without an overlay it was null — which is why the earlier tests could only ever share a directory')
})