// 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')
})