// Repo-tracker join + write-enrollment via blind-pairing over Hyperswarm
// (the P2P replacement for the bearer invite + enrollFromInvite). Supersedes
// the in-memory repo-invite.test.js, which exercised the now-removed API.
//
// Proves, over a real testnet:
// - contributor: pairs → becomes a writer → its issue replicates to owner
// - reader: pairs → read-only mirror (sees owner's issues, NOT a writer)
import test from 'brittle'
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import createTestnet from '@hyperswarm/testnet'
import { mkdtempSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { HyperIssues, joinRepoPairing } from '../index.js'
let n = 0
const tmp = () => mkdtempSync(join(tmpdir(), 'rp-' + (n++) + '-'))
async function waitFor (pred, ms = 25000) {
const t0 = Date.now()
while (Date.now() - t0 < ms) {
try { if (await pred()) return true } catch {}
await new Promise((r) => setTimeout(r, 100))
}
return false
}
function makeNode (t, bootstrap) {
const store = new Corestore(tmp())
const swarm = new Hyperswarm({ bootstrap })
swarm.on('connection', (conn) => store.replicate(conn))
t.teardown(async () => { try { await swarm.destroy() } catch {} })
return { store, swarm }
}
test('contributor pairs into a repo tracker and becomes a writer (P2P, no HTTP)', async (t) => {
const testnet = await createTestnet(3, t.teardown)
const a = makeNode(t, testnet.bootstrap)
const b = makeNode(t, testnet.bootstrap)
const owner = new HyperIssues(a.store, { namespace: 'myrepo' })
await owner.ready()
t.teardown(async () => { try { await owner.close() } catch {} })
t.ok(owner.writable, 'owner (creator) is writable')
await owner.create({ title: 'owner issue', author: 'owner' })
const { invitation } = owner.createInvite({ name: 'myrepo', role: 'contributor', swarm: a.swarm })
t.ok(invitation.startsWith('roompair1:'), 'invite code minted')
const { tracker: joiner, role } = await joinRepoPairing(invitation, { corestore: b.store, swarm: b.swarm })
t.teardown(async () => { try { await joiner.close() } catch {} })
t.is(role, 'contributor', 'role echoed')
t.is(joiner.keyHex, owner.keyHex, 'joiner mounted the SAME tracker autobase')
const became = await waitFor(() => joiner.writable)
t.ok(became, 'contributor became a writer via pairing — over the swarm, no HTTP')
const id = await joiner.create({ title: 'joiner issue', author: 'joiner' })
const seen = await waitFor(async () => {
await owner.sync()
const iss = await owner.get(id)
return !!(iss && iss.title === 'joiner issue')
})
t.ok(seen, 'owner sees the joiner-authored issue (bidirectional)')
})
test('reader pairs into a repo tracker and stays a read-only mirror', async (t) => {
const testnet = await createTestnet(3, t.teardown)
const a = makeNode(t, testnet.bootstrap)
const b = makeNode(t, testnet.bootstrap)
const owner = new HyperIssues(a.store, { namespace: 'proj' })
await owner.ready()
t.teardown(async () => { try { await owner.close() } catch {} })
const id = await owner.create({ title: 'owner-only', author: 'owner' })
const { invitation } = owner.createInvite({ name: 'proj', role: 'reader', swarm: a.swarm })
const { tracker: reader, role } = await joinRepoPairing(invitation, { corestore: b.store, swarm: b.swarm })
t.teardown(async () => { try { await reader.close() } catch {} })
t.is(role, 'reader', 'role echoed as reader')
const sawRead = await waitFor(async () => {
await reader.sync()
const iss = await reader.get(id)
return !!(iss && iss.title === 'owner-only')
})
t.ok(sawRead, 'reader replicates the tracker read-only')
await new Promise((r) => setTimeout(r, 1500))
t.absent(reader.writable, 'reader is NOT enrolled as a writer')
})