OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / mirror-writer-enrollment.test.js

187 lines · javascript ref:
// 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')
})