OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / repo-add-writer-rpc.test.js

113 lines · javascript ref:
// Proof for the repo-add-writer / repo-local-key RPCs — org-admin-managed
// peer write authority.
//
// THE GAP (verified from source): a daemon that mounts a per-repo tracker
// READ-ONLY (e.g. via the org-resource path) cannot become a writer through
// `repo join` — joinRepo short-circuits when the repo is already open. So an
// org peer with full git-push ACL still can't append refs into the shared
// tracker view (the observed asymmetry).
//
// THE FIX (this test): the OWNER pulls the peer's tracker local key via the
// new read-only `repo-local-key` RPC, then calls the new admin-gated
// `repo-add-writer` RPC. Once the add-writer op replicates, the peer's own
// local key is in the writer set, its autobase becomes `writable`, and its
// writes round-trip back to the owner — symmetric P2P writes on the SAME
// shared autobase, with no re-join and no teardown.

import test from 'brittle'
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
function tmpStorage () {
  return mkdtempSync(join(tmpdir(), 'pgv3-radd-' + (n++) + '-'))
}
function tmpSockPath () {
  return join(mkdtempSync(join(tmpdir(), 'pgv3-radd-sock-' + (n++) + '-')), 'svc.sock')
}
async function waitFor (predicate, timeoutMs = 8000) {
  const t0 = Date.now()
  while (Date.now() - t0 < timeoutMs) {
    try { if (await predicate()) return true } catch {}
    await new Promise((r) => setTimeout(r, 50))
  }
  return false
}

test('repo-add-writer RPC: admin enrolls a read-only peer as a tracker writer (no re-join)', async (t) => {
  const testnet = await createTestnet(3, t.teardown)
  const topic = b4a.alloc(32, 'pgv3-repo-add-writer')
  const { Client } = await import('hyper-svc')

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

  // A owns the per-repo tracker (autobase root)
  const trackerA = await nodeA.issueRegistry.getForRepo('demo')
  await trackerA.create({ title: 'seed from owner', author: 'alice' })
  const demoKey = trackerA.keyHex

  // B mounts A's tracker READ-ONLY — models the org-grouped (read-only) mount
  const nodeB = await startPearGit({
    storage: tmpStorage(),
    uds: tmpSockPath(),
    http: { port: 0 },
    swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
    deploy: false,
    sync: false,
    mounts: { issueRepos: { demo: demoKey } }
  })
  t.teardown(async () => { try { await nodeB.close() } catch {} })

  const trackerB = await nodeB.issueRegistry.getForRepo('demo')
  await waitFor(async () => (await trackerB.counts()).total === 1, 6000)
  t.absent(trackerB.base.writable, 'B starts NOT writable (read-only mount)')

  // ── 1. Admin pulls B's tracker local key via the new repo-local-key RPC ──
  const cb = await Client.connectUds(nodeB.svc.uds.path)
  const bKey = await cb.call('repo-local-key', { name: 'demo' })
  await cb.close()
  t.is(bKey.localKey, trackerB.localKeyHex, 'repo-local-key returns B\'s tracker local key')
  t.absent(bKey.writable, 'repo-local-key reports B not yet writable')

  // ── 2. Admin grants B push authority via the new repo-add-writer RPC ──
  //    (UDS transport ⇒ requireAdmin bypass — the local operator is the owner)
  const ca = await Client.connectUds(nodeA.svc.uds.path)
  const res = await ca.call('repo-add-writer', {
    name: 'demo',
    userId: 'peer-b',
    writerKey: bKey.localKey,
    role: 'owner'
  })
  await ca.close()
  t.ok(res.added, 'repo-add-writer reports added')

  // ── 3. After the add-writer op replicates, B becomes writable ──
  const becameWritable = await waitFor(async () => trackerB.base.writable === true, 8000)
  t.ok(becameWritable, 'B.tracker.base.writable === true after admin repo-add-writer')

  // ── 4. B writes; A sees it (symmetric round-trip) ──
  const id = await trackerB.create({ title: 'write from peer', author: 'bob' })
  t.ok(typeof id === 'number' && id > 0, 'B can now write to the shared tracker')
  const seen = await waitFor(async () => (await trackerA.counts()).total === 2, 8000)
  t.ok(seen, 'A sees B\'s write — symmetric P2P propagation')

  // ── 5. The new writer is visible via repo-list-writers ──
  const ca2 = await Client.connectUds(nodeA.svc.uds.path)
  const w = await ca2.call('repo-list-writers', { name: 'demo' })
  await ca2.close()
  t.ok(w.writers.some((x) => x.userId === 'peer-b'), 'peer-b listed as a writer')
})