// Tests for mirror-writer watcher enrollment fan-out.
//
// Bug claim (issue #5 medium-priority item, observed today):
// When a mirror calls identityRegistry.registerMirrorWriters with N
// namespace keys, the primary's `setupMirrorWriterCoordination`
// watcher should call `tracker.addWriter` (or `orgs.addWriter`) on
// each of the N. In production, after stortron's re-pair we observed
// only ONE log line ("enrolled 3e30cf02 on issues:repo:hyperswarm-peer")
// despite the announce carrying ~97 namespace keys.
//
// Hypothesis: silent-skip in the per-namespace try/catch (debug-only
// logging hides errors), or partial-process loop exit after some
// number of iterations.
//
// Test approach: spin up a SINGLE primary daemon. Open 5 per-repo issue
// trackers and create 2 org rooms = 7 namespaces. The announce uses the
// two canonical namespace forms (see _doAnnounce in lib/pear-git.js):
// `org:<name>` per per-org RECORD autobase (org issues live there too,
// linked-core #42) and `issues:repo:<name>` per per-repo tracker — there
// is no global "orgs" autobase and no `issues:org:` key post-#37. Submit
// a single registerMirrorWriters call with all 7 keys to a FAKE
// devicePubkey (not the primary's). Wait for the watcher to fan out
// enrollments. Verify every namespace's autobase has the fake mirror as
// a writer.
//
// If the watcher is healthy, all 8 enrollments land. If it isn't,
// the test reveals the gap.
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-mw-enroll-' + (n++) + '-'))
}
function tmpSockPath () {
return join(mkdtempSync(join(tmpdir(), 'pgv3-mw-enroll-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
}
// Generate a 64-char hex string. Doesn't have to be a real autobase
// writer key — Room.addWriter accepts any hex; the op gets written to
// the view, listWriters reads it back. We're testing fan-out, not
// replication.
function fakeKey (seed) {
return seed.toString(16).padStart(64, '0')
}
test('mirror-writer watcher: registers all N namespace keys from one announce (fan-out)', async (t) => {
t.timeout(60000)
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-mw-fanout')
const node = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false
})
t.teardown(async () => { try { await node.close() } catch {} })
// Open 5 per-repo trackers and create 2 org rooms = 7 namespaces.
// Orgs must be created so the watcher's orgs.addWriter(name, …) finds a
// room (it calls _requireRoom under the hood).
const repos = ['repo-1', 'repo-2', 'repo-3', 'repo-4', 'repo-5']
const orgs = ['org-a', 'org-b']
for (const name of repos) await node.issueRegistry.getForRepo(name)
for (const name of orgs) await node.orgs.create(name, 'group', {})
// Build the announce. Each value is a unique 64-char hex. Namespaces
// are `issues:repo:<name>` and `org:<name>` — the two forms the
// watcher recognises (lib/pear-git.js).
const fakeDevicePubkey = '1'.padEnd(64, 'a')
const expectedUserId = 'mirror-' + fakeDevicePubkey.slice(0, 12)
const keys = {}
for (let i = 0; i < repos.length; i++) {
keys['issues:repo:' + repos[i]] = fakeKey(0xc00000 + i)
}
for (let i = 0; i < orgs.length; i++) {
keys['org:' + orgs[i]] = fakeKey(0xd00000 + i)
}
const N = Object.keys(keys).length // 5 + 2 = 7
await node.identityRegistry.registerMirrorWriters(fakeDevicePubkey, keys)
// Wait for the watcher to addWriter the mirror on every namespace.
const allEnrolled = await waitFor(async () => {
for (const name of repos) {
const tr = await node.issueRegistry.getForRepo(name)
if (!(await tr.listWriters()).some(w => w.userId === expectedUserId)) return false
}
for (const name of orgs) {
if (!(await node.orgs.listWriters(name)).some(w => w.userId === expectedUserId)) return false
}
return true
}, 25000)
// Diagnostic dump if it failed — shows exactly how many enrolled vs
// how many should have, by namespace.
if (!allEnrolled) {
const repoW = []
for (const name of repos) {
const tr = await node.issueRegistry.getForRepo(name)
repoW.push({ name, enrolled: (await tr.listWriters()).filter(w => w.userId === expectedUserId).length })
}
const orgW = []
for (const name of orgs) {
orgW.push({ name, enrolled: (await node.orgs.listWriters(name)).filter(w => w.userId === expectedUserId).length })
}
console.error('[diag] watcher enrollment results:', JSON.stringify({ repos: repoW, orgs: orgW }, null, 2))
}
t.ok(allEnrolled, `watcher enrolled mirror on all ${N} namespaces`)
})
test('mirror-writer watcher: a malformed key mid-announce does NOT abort the rest', async (t) => {
// Defensive test: if one of the addWriter calls throws (e.g. a
// malformed writerKey), the watcher must still process the OTHER
// namespaces. The current code wraps each ns in try/catch, so this
// SHOULD pass — but the log shape only emits one line per success
// and swallows failures (debug-only), so a bug here would be silent.
t.timeout(60000)
const testnet = await createTestnet(3, t.teardown)
const topic = b4a.alloc(32, 'pgv3-mw-defensive')
const node = await startPearGit({
storage: tmpStorage(),
uds: tmpSockPath(),
http: { port: 0 },
swarm: { topic, swarm: { bootstrap: testnet.bootstrap } },
deploy: false,
sync: false
})
t.teardown(async () => { try { await node.close() } catch {} })
await node.issueRegistry.getForRepo('good-1')
await node.issueRegistry.getForRepo('good-2')
await node.orgs.create('good-org', 'group', {})
const fakeDevicePubkey = '2'.padEnd(64, 'b')
const expectedUserId = 'mirror-' + fakeDevicePubkey.slice(0, 12)
const keys = {
'org:good-org': fakeKey(0xb1),
'issues:repo:good-1': fakeKey(0xc1),
'issues:repo:good-2': fakeKey(0xc2),
// A namespace whose addWriter THROWS: the org room was never created,
// so orgs.addWriter -> _requireRoom throws. The watcher's per-namespace
// try/catch must absorb it and keep processing the siblings.
'org:ghost-org': fakeKey(0xee),
// A bogus namespace the watcher doesn't recognize — should silently
// skip without affecting siblings.
'unknown:foo': fakeKey(0xff),
// A namespace that points at a not-yet-open tracker — getForRepo
// mints it (permissive on primary), which works.
'issues:repo:fresh': fakeKey(0xc3)
}
await node.identityRegistry.registerMirrorWriters(fakeDevicePubkey, keys)
const goodOnesEnrolled = await waitFor(async () => {
const a = (await node.orgs.listWriters('good-org')).some(w => w.userId === expectedUserId)
const b = (await (await node.issueRegistry.getForRepo('good-1')).listWriters())
.some(w => w.userId === expectedUserId)
const c = (await (await node.issueRegistry.getForRepo('good-2')).listWriters())
.some(w => w.userId === expectedUserId)
return a && b && c
}, 20000)
t.ok(goodOnesEnrolled, 'throwing org:ghost-org / unknown namespace did not abort good-org / good-1 / good-2 enrollment')
})