OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / self-heal-resync.test.js

72 lines · javascript ref:
// pear-git #3 Increment 2: a repo tracker autobase 'error' triggers a
// fresh-namespace resync — the standalone-peer self-heal. IssueRegistry bumps a
// per-repo generation, drops the broken tracker, and hands off to onResync so
// the daemon re-attaches the reconciler to a tracker reopened under a clean
// namespace (fresh local writer → re-materialize from peers, no crash-loop).
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync, readFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { IssueRegistry } from '../lib/issue-registry.js'

function tmp () { return mkdtempSync(join(tmpdir(), 'pgv3-resync-')) }

test('repo tracker error triggers fresh-namespace resync (#3 Increment 2)', async (t) => {
  const dir = tmp()
  const store = new Corestore(join(dir, 'store'))
  await store.ready()

  const indexPath = join(dir, 'issue-registry-index.json')
  let resynced = null
  const reg = new IssueRegistry(store, {
    indexPath,
    onResync: (repo) => { resynced = repo }
  })

  const tracker = await reg.getForRepo('myrepo')
  t.is(reg._repoNamespace('myrepo'), 'issues:repo:myrepo', 'gen 0 = canonical namespace')
  t.ok(reg._repos.has('myrepo'), 'tracker cached')

  // Simulate the tracker autobase erroring (the null-view failure). The Room's
  // Increment-1 handler isolates it + re-emits 'base-error' → IssueRegistry resyncs.
  tracker.base.emit('error', new Error('simulated null view'))

  // resync is async (awaits close); give it a beat
  await new Promise((r) => setTimeout(r, 250))

  t.is(reg._index.resyncGen.myrepo, 1, 'generation bumped to 1')
  t.is(reg._repoNamespace('myrepo'), 'issues:repo:myrepo#r1', 'namespace now suffixed #r1')
  t.is(resynced, 'myrepo', 'onResync invoked for the repo')
  t.absent(reg._repos.has('myrepo'), 'broken tracker dropped from cache')

  // generation persisted to the index file (survives restart)
  const persisted = JSON.parse(readFileSync(indexPath, 'utf8'))
  t.is(persisted.resyncGen && persisted.resyncGen.myrepo, 1, 'generation persisted to index')

  // reopening now lands on the fresh namespace — a NEW tracker instance
  const fresh = await reg.getForRepo('myrepo')
  t.ok(fresh, 'reopened under fresh namespace')
  t.not(fresh, tracker, 'a distinct tracker instance')

  try { await fresh.close() } catch {}
  await store.close()
})

test('resync caps at MAX_RESYNC to avoid an infinite loop (#3)', async (t) => {
  const dir = tmp()
  const store = new Corestore(join(dir, 'store'))
  await store.ready()
  let resyncCount = 0
  const reg = new IssueRegistry(store, {
    indexPath: join(dir, 'issue-registry-index.json'),
    onResync: () => { resyncCount++ }
  })
  await reg.getForRepo('r')
  // Simulate the fresh namespace ALSO erroring every time — must stop at the cap.
  for (let i = 0; i < 6; i++) await reg._resyncRepo('r')
  t.is(reg._index.resyncGen.r, 3, 'generation capped at MAX_RESYNC=3')
  t.is(resyncCount, 3, 'onResync invoked at most MAX_RESYNC times (then isolated)')
  await store.close()
})