OverviewFilesCommitsIssuesDocsReleasesPeers

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

96 lines · javascript ref:
// pear-git #3 A-passive: mirror-writer coordination must (1) enroll new mirrors
// as PLAIN WRITERS (indexer:false), not indexers, and (2) NEVER re-touch an
// existing writer — re-adding with a changed role DEMOTES it (an indexer-set
// migration that crash-loops intermittent standalone peers). The coordination's
// skip-if-already-a-writer guard makes re-running it a true no-op on existing
// indexers, so the set is frozen (zero migration) while growth stops.
//
// This test exercises the exact primitives the guard uses: tracker.listWriters()
// membership check + addWriter role.
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { HyperIssues } from 'hyper-issues'
import { replicateAndSync } from 'autobase-test-helpers'

function tmp () { return mkdtempSync(join(tmpdir(), 'pgv3-passive-')) }
const idxKeys = (t) => {
  try {
    const s = t.base._applyState && t.base._applyState.system
    return s && s.indexers ? s.indexers.map(i => i.key.toString('hex')) : []
  } catch { return [] }
}
async function settle (bases, rounds = 25) {
  for (let i = 0; i < rounds; i++) {
    for (const b of bases) { try { await b.ack() } catch {} }
    await replicateAndSync(bases)
  }
}

// The exact A-passive enrollment step: skip if already a writer, else add as
// plain writer. Mirrors lib/pear-git.js mirror-writer coordination.
async function passiveEnroll (tracker, dev, writerKey) {
  let already = false
  try { const ws = await tracker.listWriters(); already = ws.some(w => w.writerKey === writerKey) } catch {}
  if (already) return { skipped: true }
  await tracker.addWriter('mirror-' + dev, { writerKey, role: 'writer', addedBy: 'test' })
  return { skipped: false }
}

test('A-passive skip-guard: re-enrolling an existing indexer is a no-op (no demotion, no migration)', async (t) => {
  const dir = tmp()
  const sG = new Corestore(join(dir, 'G')); await sG.ready()
  const G = new HyperIssues(sG, { namespace: 'ns' }); await G.ready()
  const key = G.keyHex
  await G.create({ title: 'i1', body: 'x', author: 'G' })
  const sB = new Corestore(join(dir, 'B')); await sB.ready()
  const B = new HyperIssues(sB, { namespace: 'ns', bootstrapKey: key }); await B.ready()
  await replicateAndSync([G.base, B.base])

  // Existing state: B was added as an INDEXER by the old (owner) policy.
  await G.addWriter('B', { writerKey: B.localKeyHex, role: 'owner' })
  await settle([G.base, B.base])
  const before = idxKeys(G).sort()
  t.ok(before.includes(B.base.local.key.toString('hex')), 'B is an indexer before re-enroll')

  // A-passive coordination re-runs and tries to enroll B again.
  const res = await passiveEnroll(G, 'B', B.localKeyHex)
  await settle([G.base, B.base])
  const after = idxKeys(G).sort()

  t.ok(res.skipped, 'existing writer B is SKIPPED (not re-added)')
  t.alike(after, before, 'indexer set UNCHANGED — B not demoted, no migration')
  t.ok(after.includes(B.base.local.key.toString('hex')), 'B is STILL an indexer')

  t.teardown(async () => { try { await G.close(); await B.close(); await sG.close(); await sB.close() } catch {} })
})

test('A-passive: a NEW mirror is enrolled as a plain writer, not an indexer', async (t) => {
  const dir = tmp()
  const sG = new Corestore(join(dir, 'G')); await sG.ready()
  const G = new HyperIssues(sG, { namespace: 'ns' }); await G.ready()
  const key = G.keyHex
  await G.create({ title: 'i1', body: 'x', author: 'G' })
  const sM = new Corestore(join(dir, 'M')); await sM.ready()
  const M = new HyperIssues(sM, { namespace: 'ns', bootstrapKey: key }); await M.ready()
  await replicateAndSync([G.base, M.base])

  const before = idxKeys(G).sort()
  const res = await passiveEnroll(G, 'M', M.localKeyHex)
  await settle([G.base, M.base])
  const after = idxKeys(G).sort()

  t.absent(res.skipped, 'new mirror M is enrolled (not skipped)')
  const mKey = M.base.local.key.toString('hex')
  t.absent(after.includes(mKey), 'M is NOT an indexer (indexer set unchanged)')
  t.alike(after, before, 'indexer set unchanged by adding a plain writer')
  const writers = await G.listWriters()
  const mWriter = writers.find(w => w.writerKey === M.localKeyHex)
  t.ok(mWriter, 'M IS a writer')
  t.is(mWriter.role, 'writer', 'M was enrolled with role=writer (plain writer, can push, no co-sign)')

  t.teardown(async () => { try { await G.close(); await M.close(); await sG.close(); await sM.close() } catch {} })
})