OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / daemon-pairing.test.js

98 lines · javascript ref:
// Daemon-level proof: org + repo join + write-enrollment happen over the
// swarm via blind-pairing through the rewired RPCs (create-org/repo-invite +
// join-org/repo). No HTTP, no enroll-writer RPC. Two daemons on DIFFERENT
// identity topics — they rendezvous purely via the per-room pairing topic.

import test from 'brittle'
import process from '#node:process'
import { mkdtempSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'

import { startPearGit } from '../index.js'

let n = 0
const tmpStorage = () => mkdtempSync(join(tmpdir(), 'pgv3-dp-' + (n++) + '-'))
const tmpSock = () => join(mkdtempSync(join(tmpdir(), 'pgv3-dp-sock-' + (n++) + '-')), 'svc.sock')

async function waitFor (pred, ms = 30000) {
  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 issuesOf (list) {
  return (list && list.issues) ? list.issues : (Array.isArray(list) ? list : [])
}

test('org + repo join over the swarm via daemon RPCs (blind-pairing, no HTTP)', async (t) => {
  process.env.PGV3_USE_RAW_IDENTITY = '1'
  const testnet = await createTestnet(3, t.teardown)

  const A = await startPearGit({
    storage: tmpStorage(), uds: tmpSock(), http: { port: 0 },
    swarm: { swarm: { bootstrap: testnet.bootstrap } }, deploy: false, sync: false
  })
  t.teardown(async () => { try { await A.close() } catch {} })
  const B = await startPearGit({
    storage: tmpStorage(), uds: tmpSock(), http: { port: 0 },
    swarm: { swarm: { bootstrap: testnet.bootstrap } }, deploy: false, sync: false
  })
  t.teardown(async () => { try { await B.close() } catch {} })

  t.not(
    b4a.toString(A.svc.swarm.topic, 'hex'),
    b4a.toString(B.svc.swarm.topic, 'hex'),
    'daemons are on DIFFERENT identity topics'
  )

  const { Client } = await import('hyper-svc')
  const ca = await Client.connectUds(A.svc.uds.path)
  const cb = await Client.connectUds(B.svc.uds.path)
  t.teardown(async () => { try { await ca.close() } catch {}; try { await cb.close() } catch {} })

  // ── ORG ──────────────────────────────────────────────────────
  await ca.call('create-org', { name: 'palpha', type: 'group', displayName: 'PAlpha' })
  const { invitation: orgInv } = await ca.call('create-org-invite', { name: 'palpha', role: 'contributor' })
  t.ok(typeof orgInv === 'string' && orgInv.startsWith('roompair1:'), 'org pairing invite minted (RPC)')

  const oj = await cb.call('join-org', { invitation: orgInv })
  t.is(oj.name, 'palpha', 'B joined the org via join-org RPC')

  const orgWritable = await waitFor(() => { const r = B.orgs.getOrgRoom('palpha'); return r && r.writable })
  t.ok(orgWritable, 'B became an org writer via pairing — over the swarm, no HTTP')

  await cb.call('create-issue', { org: 'palpha', title: 'org issue from B', author: 'b' })
  const aSeesOrg = await waitFor(async () => {
    const arr = issuesOf(await ca.call('list-issues', { org: 'palpha' }))
    return arr.some((x) => x && x.title === 'org issue from B')
  })
  t.ok(aSeesOrg, 'A sees the B-authored org issue (bidirectional)')

  // ── REPO ─────────────────────────────────────────────────────
  await A.issueRegistry.getForRepo('prepo') // mint the tracker on the owner
  const { invitation: repoInv } = await ca.call('create-repo-invite', { name: 'prepo', role: 'contributor' })
  t.ok(typeof repoInv === 'string' && repoInv.startsWith('roompair1:'), 'repo pairing invite minted (RPC)')

  const rj = await cb.call('join-repo', { invitation: repoInv })
  t.is(rj.name, 'prepo', 'B joined the repo via join-repo RPC')

  const repoWritable = await waitFor(async () => {
    const tr = await B.issueRegistry.getForRepo('prepo').catch(() => null)
    return tr && tr.writable
  })
  t.ok(repoWritable, 'B became a repo writer via pairing — over the swarm, no HTTP')

  await cb.call('create-issue', { repo: 'prepo', title: 'repo issue from B', author: 'b' })
  const aSeesRepo = await waitFor(async () => {
    const arr = issuesOf(await ca.call('list-issues', { repo: 'prepo' }))
    return arr.some((x) => x && x.title === 'repo issue from B')
  })
  t.ok(aSeesRepo, 'A sees the B-authored repo issue (bidirectional)')
})