// Proof test for the per-repo issue tracker mirror-writer flow.
//
// HYPOTHESIS: stortron's "Not writable" error on issues comes from
// `setupMirrorWriterCoordination`'s announce side only emitting
// `{ orgs: orgs.localKeyHex }` (lib/pear-git.js:1478). The watcher
// already handles `issues:repo:<name>` and `issues:org:<name>`
// namespaces (lib/pear-git.js:1422-1441), so adding those keys to
// the announce is sufficient.
//
// Test models the END-TO-END flow with TWO real startPearGit
// daemons (primary + mirror) sharing identity via mounts.
//
// Phase 1 — control: NO mirror-writer announce. Confirms B's
// per-repo tracker is NOT writable. Reproduces the production bug.
//
// Phase 2 — fix: B announces { 'issues:repo:demo': trackerB.localKeyHex }
// via identityRegistry.registerMirrorWriters. A's
// setupMirrorWriterCoordination watcher fires, calls
// tracker.addWriter on its own tracker with B's localKeyHex.
// After replication, B's tracker.base.writable === true.
// B writes an issue, A sees it.
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-issues-mw-' + (n++) + '-'))
}
function tmpSockPath () {
return join(mkdtempSync(join(tmpdir(), 'pgv3-issues-mw-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('per-repo tracker: mirror is NOT writable without registering its localKeyHex (control)', async (t) => {
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-issues-mw-control')
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 creates a per-repo tracker (= primary, owns the autobase root)
const trackerA = await nodeA.issueRegistry.getForRepo('demo')
await trackerA.create({ title: 'a from primary', author: 'alice' })
const demoKey = trackerA.keyHex
// B mounts A's tracker bootstrap, but does NOT announce its localKeyHex
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')
// Wait for replication so B sees A's issue (read path works)
const replicated = await waitFor(async () => {
const c = await trackerB.counts()
return c.total === 1
}, 6000)
t.ok(replicated, 'B receives A\'s issue via swarm replication')
// But B is NOT a writer on this tracker — its localKeyHex was never
// addWritered by A. Direct write must fail.
let writeFailed = false
let errMessage = null
try {
await trackerB.create({ title: 'b without enrollment', author: 'bob' })
} catch (err) {
writeFailed = true
errMessage = err.message
}
t.ok(writeFailed, 'B.tracker.create FAILS when B is not enrolled as writer')
t.is(errMessage, 'Not writable',
'error message matches production symptom: "Not writable"')
})
test('per-repo tracker: A.tracker.addWriter(B.localKeyHex) makes B writable (mechanism proof)', async (t) => {
// Direct proof of the underlying mechanism the watcher invokes.
// Skips the identityRegistry layer — calls tracker.addWriter
// directly with B's tracker.localKeyHex. If this works, the fix
// (announce side adding issue keys, watcher already calling
// addWriter on the matching tracker) is mechanically sound.
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-issues-mw-mech')
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 {} })
const trackerA = await nodeA.issueRegistry.getForRepo('demo')
await trackerA.create({ title: 'a from primary', author: 'alice' })
const demoKey = trackerA.keyHex
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 () => {
const c = await trackerB.counts()
return c.total === 1
}, 6000)
t.absent(trackerB.base.writable, 'B starts NOT writable on the tracker')
// ── Direct mechanism: A addWriters B's localKeyHex on the tracker ──
// This is exactly what setupMirrorWriterCoordination does in
// pear-git.js:1425 when it sees a register-mirror-writers entry
// with `issues:repo:demo: <hex>`.
await trackerA.addWriter('mirror-b', {
writerKey: trackerB.localKeyHex,
role: 'owner',
addedBy: 'a'.repeat(64)
})
// Wait for the ADD_WRITER op to replicate back to B
const becameWritable = await waitFor(
async () => trackerB.base.writable === true,
8000
)
t.ok(becameWritable,
'B.tracker.base.writable === true after A.tracker.addWriter(B.localKeyHex)')
// B writes — HyperIssues.create returns the assigned id (integer)
const id = await trackerB.create({ title: 'b enrolled', author: 'bob' })
t.ok(typeof id === 'number' && id > 0, 'B.tracker.create returns an id')
// A sees it
const seen = await waitFor(async () => {
const c = await trackerA.counts()
return c.total === 2
}, 8000)
t.ok(seen, 'A sees B\'s issue via replication (round-trip works)')
})
test('per-org issue (mirror mode): OrgEventLog → mountOrg replicates the org ISSUES via the record autobase (linked-core #42)', async (t) => {
// Linked-core: org issues live IN the org RECORD autobase. The mirror's
// OrgPuller mountOrg()s that autobase from OrgEventLog's orgBootstrapKey,
// so the org's ISSUES replicate WITH the record — no separate issue
// tracker key conveyed or bound. Mirror-mode analog of
// org-issues-linked-core.test.js (which proves the peer-mode path).
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-org-issue-mirror')
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 {} })
// Create the org through the real handler so create-org appends the
// org-record key (orgBootstrapKey) to OrgEventLog. issueBootstrapKey is
// null now — there is no separate tracker.
const { Client } = await import('hyper-svc')
const ca = await Client.connectUds(nodeA.svc.uds.path)
await ca.call('create-org', { name: 'shared-org', type: 'group', displayName: 'Shared' })
await ca.close()
const orgKey = nodeA.orgs.getOrgRoom('shared-org').keyHex
// The org issue tracker IS the org record autobase (no separate tracker).
const trackerA = await nodeA.issueRegistry.getForOrg('shared-org')
t.is(trackerA.keyHex, orgKey,
'org issue tracker == org record autobase (no separate tracker)')
// Create an ORG ISSUE in it (lives in the org record autobase).
const id = await trackerA.create({ title: 'org issue', body: 'b', author: 'owner' })
// B is a mirror — gets A's orgEventLogKey via origin config. No
// mounts.orgs (that single-autobase surface is gone).
const nodeB = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false,
origin: { serverKey: 'a'.repeat(64), orgEventLogKey: nodeA.orgEventLog.keyHex }
})
t.teardown(async () => { try { await nodeB.close() } catch {} })
// OrgPuller streams the log, mountOrg()s the record autobase; the org's
// ISSUES replicate with it. B sees the issue via getForOrg — no separate
// tracker key was ever conveyed or bound.
const saw = await waitFor(async () => {
const trB = await nodeB.issueRegistry.getForOrg('shared-org')
const iss = await trB.get(id)
return !!(iss && iss.title === 'org issue')
}, 10000)
t.ok(saw,
'mirror received the org ISSUE via OrgEventLog → mountOrg (record autobase carries issues)')
// Same autobase root, not a sibling.
const trackerB = await nodeB.issueRegistry.getForOrg('shared-org')
t.is(trackerB.keyHex, orgKey,
'mirror org issue tracker == primary org record autobase')
})
test('per-org RECORD: OrgEventLog carries the org autobase key → mirror mountOrg replicates the record (#37 item 3)', async (t) => {
// Per-org-autobase model: there is no single orgs autobase to mount.
// The mirror-mode loop is: primary's create-org appends the org's OWN
// autobase key to OrgEventLog; the mirror's OrgPuller mountOrg()s it,
// so the org RECORD replicates over the shared swarm — same shape as
// RepoEventLog/RepoPuller for repos.
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-org-record-prop')
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 {} })
// Create the org through the real handler (UDS bypasses auth) so the
// create-org append carries the org-record key, not just the issue key.
const { Client } = await import('hyper-svc')
const ca = await Client.connectUds(nodeA.svc.uds.path)
await ca.call('create-org', { name: 'recorg', type: 'group', displayName: 'RecOrg' })
await ca.close()
const orgKey = nodeA.orgs.getOrgRoom('recorg').keyHex
// B is a mirror — gets A's orgEventLogKey via origin config. No
// mounts.orgs (that single-autobase surface is gone).
const nodeB = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false,
origin: { serverKey: 'a'.repeat(64), orgEventLogKey: nodeA.orgEventLog.keyHex }
})
t.teardown(async () => { try { await nodeB.close() } catch {} })
// OrgPuller streams the log, mountOrg()s the record autobase, and the
// record replicates over the swarm.
const replicated = await waitFor(async () => {
const org = await nodeB.orgs.get('recorg')
return !!(org && org.displayName === 'RecOrg')
}, 8000)
t.ok(replicated, 'mirror received the org RECORD via OrgEventLog → mountOrg')
// The mounted org is the SAME autobase root (not a sibling).
const roomB = nodeB.orgs.getOrgRoom('recorg')
t.ok(roomB, 'mirror tracks the org')
t.is(roomB.keyHex, orgKey, 'mirror mounted the primary autobase root')
// HA writer-enrollment (mechanism the mirror-writer announce automates):
// the primary enrolls the mirror's per-org local key → the mirror becomes
// a writer on that org and its writes replicate back.
await nodeA.orgs.addWriter('recorg', 'mirror-b', {
writerKey: roomB.localKeyHex, role: 'owner'
})
const writable = await waitFor(async () => {
await nodeB.orgs.sync()
const r = nodeB.orgs.getOrgRoom('recorg')
return !!(r && r.writable)
}, 8000)
t.ok(writable, 'mirror becomes a writer on the org after primary addWriter')
await nodeB.orgs.addResource('recorg', 'repo-from-mirror')
const seenBack = await waitFor(async () => {
await nodeA.orgs.sync()
const o = await nodeA.orgs.get('recorg')
return !!(o && o.resources.includes('repo-from-mirror'))
}, 8000)
t.ok(seenBack, 'mirror write replicated back to primary (HA symmetric writer)')
})
test('per-org scope over swarm: peer joins ONE org, a primary with 3 orgs does NOT crash, no leak (#37 item 4)', async (t) => {
// Regression for the cross-machine crash: SwarmTransport._onConnection
// read `orgs.base`, but the per-org getter THROWS on >1 org, aborting
// the daemon on the first incoming connection. (Single-org tests never
// hit it.) Fix: the transport uses orgs.replicate(conn). This also
// proves peer-mode scope isolation over a real swarm.
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-scope-peer-swarm')
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 {} })
await nodeA.orgs.create('alpha', 'group', { displayName: 'Alpha' })
await nodeA.orgs.create('beta', 'group', {})
await nodeA.orgs.create('gamma', 'group', {})
await nodeA.orgs.sync()
const { invitation } = await nodeA.orgs.createOrgInvite('alpha', { role: 'reader', swarm: nodeA.svc.swarm.swarm })
const nodeB = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false
})
t.teardown(async () => { try { await nodeB.close() } catch {} })
// Peer joins ONLY alpha. The ensuing swarm connection would, pre-fix,
// abort nodeA via the throwing orgs.base getter.
const joined = await nodeB.orgs.joinOrg(invitation, { swarm: nodeB.svc.swarm.swarm })
t.is(joined.name, 'alpha', 'peer joined alpha')
const sawAlpha = await waitFor(async () => {
await nodeB.orgs.sync()
return !!(await nodeB.orgs.get('alpha'))
}, 8000)
t.ok(sawAlpha, 'peer replicated alpha over the swarm')
// Primary survived the incoming connection (no SIGABRT) and still has all 3.
t.is((await nodeA.orgs.list()).length, 3, 'primary alive with 3 orgs after peer connect')
// Scope isolation: peer never mounted beta/gamma.
t.absent(await nodeB.orgs.get('beta'), 'beta not visible (never joined)')
t.absent(await nodeB.orgs.get('gamma'), 'gamma not visible (never joined)')
t.alike(nodeB.orgs.listOrgKeys().map((o) => o.name), ['alpha'], 'peer tracks only alpha')
})
test.skip('resync-from-origin writes refreshed origin-bundle.json and hot-boots OrgPuller (0.1.55) — skipped pending swarm-bootstrap plumbing in Client.connectSwarm', async (t) => {
// Mirror was enrolled before OrgEventLog existed (origin-bundle.json
// has no orgEventLog field). Calling pgh app resync should:
// 1. Pull dump-keys from origin
// 2. Persist a refreshed bundle to disk (now includes orgEventLog.coreKey)
// 3. Hot-boot OrgPuller in-process so future orgs auto-propagate
// No identity rotation, no daemon restart.
const fs = await import('#node:fs')
const path = await import('#node:path')
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-resync-bundle-test')
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 {} })
await nodeA.orgs.create('hyper-fleeky-dev', 'group', {})
await nodeA.issueRegistry.getForOrg('hyper-fleeky-dev')
// STALE bundle: has orgs/identity/swarm but NO orgEventLog field.
// Mirrors the production state of a peer that joined before 0.1.54.
const bStorage = tmpStorage()
fs.default.mkdirSync(bStorage, { recursive: true })
const bundlePath = path.default.join(bStorage, 'origin-bundle.json')
const staleBundle = {
orgs: { bootstrapKey: nodeA.svc.bootstrapKeys.orgs },
identity: { bootstrapKey: nodeA.svc.bootstrapKeys.identity },
issueRegistry: { repos: {}, orgs: {} }, // empty — stale
repoEventLog: { coreKey: nodeA.svc.bootstrapKeys.repoEventLog },
// orgEventLog: intentionally absent
swarm: { topic: topic.toString('hex') },
sourceServerKey: nodeA.identity.publicKeyHex
}
fs.default.writeFileSync(bundlePath, JSON.stringify(staleBundle, null, 2))
// Pass the bundle explicitly via deploymentBundle — production
// server.js reads it from disk at startup and sets this opt; the
// test bypasses that, so wire it directly.
const nodeB = await startPearGit({
storage: bStorage,
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false,
deploymentBundle: staleBundle
})
t.teardown(async () => { try { await nodeB.close() } catch {} })
// Pre-condition: orgSync was NOT booted (no orgEventLogKey at boot).
t.absent(nodeB.svc._orgSync, 'OrgPuller not started at boot (stale bundle)')
// Call resync-from-origin via the daemon's UDS — same path the
// `pgh app resync` CLI takes.
const { Client } = await import('hyper-svc')
const client = await Client.connectUds(nodeB.svc.uds.path)
const result = await client.call('resync-from-origin', {})
await client.close()
t.ok(result.bundleWritten, 'resync reports bundleWritten=true')
t.ok(result.orgPullerStarted, 'resync reports orgPullerStarted=true')
// Bundle on disk now has orgEventLog.coreKey
const refreshed = JSON.parse(fs.default.readFileSync(bundlePath, 'utf8'))
t.ok(refreshed.orgEventLog && refreshed.orgEventLog.coreKey,
'origin-bundle.json now contains orgEventLog.coreKey')
t.is(refreshed.orgEventLog.coreKey, nodeA.svc.bootstrapKeys.orgEventLog,
'orgEventLog.coreKey matches origin')
// Identity NOT changed — still the original
t.is(refreshed.identity.bootstrapKey, nodeA.svc.bootstrapKeys.identity,
'identity.bootstrapKey preserved (no rotation)')
// OrgPuller is now running in-process
t.ok(nodeB.svc._orgSync, 'OrgPuller booted live (no restart needed)')
// hyper-fleeky-dev's tracker key was bound + tracker opened
t.ok(result.orgsAdded.includes('hyper-fleeky-dev'),
'orgsAdded reports hyper-fleeky-dev')
const trackerB = await nodeB.issueRegistry.getForOrg('hyper-fleeky-dev')
const trackerA = await nodeA.issueRegistry.getForOrg('hyper-fleeky-dev')
t.is(trackerB.keyHex, trackerA.keyHex,
'mirror tracker joined origin autobase root (not sibling)')
})
test.skip('per-repo tracker: full announce-via-identity-registry flow (TODO: needs device-invite scaffold)', async (t) => {
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-issues-mw-fix')
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 {} })
const trackerA = await nodeA.issueRegistry.getForRepo('demo')
await trackerA.create({ title: 'a from primary', author: 'alice' })
const demoKey = trackerA.keyHex
// B is configured as a mirror — gets origin config for setupMirrorWriterCoordination
// and shares A's identityRegistry via mounts.identity.
const nodeB = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false,
mounts: {
identity: { bootstrapKey: nodeA.svc.bootstrapKeys.identity },
issueRepos: { demo: demoKey }
},
origin: { serverKey: 'a'.repeat(64), repoEventLogKey: 'a'.repeat(64) }
})
t.teardown(async () => { try { await nodeB.close() } catch {} })
const trackerB = await nodeB.issueRegistry.getForRepo('demo')
// Wait for swarm to connect both peers' identityRegistry views
await waitFor(async () => nodeA.svc.swarm.connections >= 1, 6000)
// Wait for B's identityRegistry to become writable (A's pre-auth
// ADD_WRITER for orgs/identity propagates via replication first).
// For this test we'll just announce after a short replication settle.
await waitFor(async () => trackerB.localKeyHex && true, 4000)
await new Promise((r) => setTimeout(r, 1000))
// ── THE FIX in test form ─────────────────────────────────────
// Announce B's per-repo tracker localKeyHex. This is what the
// proposed patch in setupMirrorWriterCoordination's announce side
// would do automatically.
const myDeviceHex = b4a.toString(nodeB.svc.identity.publicKey, 'hex')
const keys = {
orgs: nodeB.orgs.localKeyHex,
'issues:repo:demo': trackerB.localKeyHex
}
// B's identityRegistry must be writable for the announce to land.
// If primary's pre-auth hasn't replicated, skip with diagnostic.
if (!nodeB.svc.identityRegistry.base.writable) {
t.comment('B.identityRegistry not yet writable — skipping (would need full device-invite flow)')
t.pass('test scaffold acknowledges identity-writer setup gap')
return
}
await nodeB.svc.identityRegistry.registerMirrorWriters(myDeviceHex, keys)
// Wait for A's setupMirrorWriterCoordination watcher to enroll B
// on the per-repo tracker. trackerB becomes writable once the
// ADD_WRITER op replicates back.
const enrolled = await waitFor(async () => trackerB.base.writable, 8000)
t.ok(enrolled, 'B.tracker.base.writable becomes true after announce')
// Now B can write
const result = await trackerB.create({ title: 'b enrolled', author: 'bob' })
t.is(result.title, 'b enrolled', 'B.tracker.create succeeds after enrollment')
// A sees B's issue via replication
const seen = await waitFor(async () => {
const c = await trackerA.counts()
return c.total === 2
}, 6000)
t.ok(seen, 'A sees B\'s issue via swarm replication')
})