OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / identity-resync.test.js

144 lines · javascript ref:
// Identity-autobase force-resync + re-admission.
//
// Asserts the recovery contract:
//   1. the re-joined peer uses a DIFFERENT local writer key (the keypair is the
//      lever for this base — it pins keyPair, so a fresh corestore alone is not
//      enough, unlike per-repo trackers)
//   2. it can READ existing history while still read-only
//   3. it is READ-ONLY until an indexer admits it (autobase golden rule:
//      never write content before admission)
//   4. after admission it is writable, has a non-empty indexer set, and its
//      writes actually linearize — visible on the ORIGIN, not just locally
//   5. the resync record round-trips, so a restart reopens the healed store
//      rather than reverting to the stuck one

import test from 'brittle'
import Corestore from 'corestore'
import b4a from 'b4a'
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import crypto from 'hypercore-crypto'
import { IdentityRegistry } from 'hyper-identity/auth/identity-registry'
import {
  forceResyncIdentity,
  loadIdentityResync,
  openResyncedIdentity
} from '../lib/services/identity-resync.js'

const sleep = (ms) => new Promise(r => setTimeout(r, ms))

function wire (a, b) {
  const s1 = a.base.replicate(true)
  const s2 = b.base.replicate(false)
  s1.pipe(s2).pipe(s1)
  return () => { try { s1.destroy(); s2.destroy() } catch {} }
}

async function tmpdir (name) {
  const dir = path.join(os.tmpdir(), 'pgv3-identity-resync-' + name + '-' + crypto.randomBytes(6).toString('hex'))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

test('forceResyncIdentity: clean re-join + re-admission restores a working writer', async (t) => {
  const originDir = await tmpdir('origin')
  const peerDir = await tmpdir('peer')

  // ── origin with real history ──
  const originStore = new Corestore(path.join(originDir, 'store'))
  await originStore.ready()
  const origin = new IdentityRegistry(originStore.namespace('identity'), {})
  await origin.ready()
  const bootstrapKey = b4a.toString(origin.base.key, 'hex')
  const seeded = await origin.createUser({ name: 'pre-existing-user' })

  // ── the peer force-resyncs: fresh corestore + freshly minted writer keypair ──
  const { registry: peer, store: peerStore, rec, localKeyHex } =
    await forceResyncIdentity({ storage: peerDir, bootstrapKey })

  t.ok(localKeyHex, 'resynced peer has a local writer key')
  t.is(rec.gen, 1, 'first resync is generation 1')

  const un = wire(origin, peer)
  await sleep(1500)
  try { await peer.base.update() } catch {}

  // 2 + 3 — reads history, but must NOT be writable yet
  t.absent(peer.base.writable, 'resynced peer is READ-ONLY before admission')
  const readBeforeAdmit = await peer.getUser(seeded.userId)
  t.ok(readBeforeAdmit, 'read-only peer can READ existing history')

  // 4 — re-admission by the indexer
  await origin.addWriter(localKeyHex, { role: 'writer', addedBy: 'test-origin' })
  await origin.base.update()
  await sleep(3000)
  await peer.base.update()

  t.ok(peer.base.writable, 'peer is WRITABLE after the indexer admits it')
  const lz = peer.base.linearizer || (peer.base._applyState && peer.base._applyState.linearizer)
  t.ok(lz && lz.indexers.length > 0, 'peer has a NON-EMPTY indexer set after admission')

  // the payload: does a write actually persist and linearize?
  const written = await peer.createUser({ name: 'written-through-healed-peer' })
  await sleep(2500)
  await peer.base.update()
  await origin.base.update()

  t.ok(await peer.getUser(written.userId), 'healed peer reads back its own write')
  t.ok(await origin.getUser(written.userId), 'the ORIGIN sees the write — it really linearized')

  un()
  await peer.close(); await peerStore.close()
  await origin.close(); await originStore.close()
})

test('the resync record survives a restart (the heal is not reverted on boot)', async (t) => {
  const originDir = await tmpdir('origin2')
  const peerDir = await tmpdir('peer2')

  const originStore = new Corestore(path.join(originDir, 'store'))
  await originStore.ready()
  const origin = new IdentityRegistry(originStore.namespace('identity'), {})
  await origin.ready()
  const bootstrapKey = b4a.toString(origin.base.key, 'hex')

  const first = await forceResyncIdentity({ storage: peerDir, bootstrapKey })
  const firstLocal = first.localKeyHex
  await first.registry.close(); await first.store.close()

  // simulate a daemon restart: read the persisted record and reopen from it
  const rec = loadIdentityResync(peerDir)
  t.ok(rec, 'resync record persisted')
  t.is(rec.gen, 1, 'record carries the generation')

  const reopened = await openResyncedIdentity({ storage: peerDir, bootstrapKey, rec })
  const reopenedLocal = b4a.toString(reopened.registry.base.local.key, 'hex')
  t.is(reopenedLocal, firstLocal,
    'reopening from the record yields the SAME writer key — the heal survives restart')

  await reopened.registry.close(); await reopened.store.close()
  await origin.close(); await originStore.close()
})

test('a second resync mints a NEW generation and a NEW writer key', async (t) => {
  const originDir = await tmpdir('origin3')
  const peerDir = await tmpdir('peer3')

  const originStore = new Corestore(path.join(originDir, 'store'))
  await originStore.ready()
  const origin = new IdentityRegistry(originStore.namespace('identity'), {})
  await origin.ready()
  const bootstrapKey = b4a.toString(origin.base.key, 'hex')

  const one = await forceResyncIdentity({ storage: peerDir, bootstrapKey })
  await one.registry.close(); await one.store.close()

  const two = await forceResyncIdentity({ storage: peerDir, bootstrapKey })
  t.is(two.rec.gen, 2, 'generation increments')
  t.not(two.localKeyHex, one.localKeyHex, 'second resync uses a DIFFERENT writer key')
  await two.registry.close(); await two.store.close()

  await origin.close(); await originStore.close()
})