// 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()
})