// pear-git #3 Increment 2b: SEPARATE-corestore self-heal for a broken
// standalone tracker.
//
// Background (Increment 2, self-heal-resync.test.js): a per-repo tracker whose
// autobase manifest is migrated out from under a standalone GENESIS peer
// crash-loops on boot — the peer's own WRITABLE genesis writer core advances its
// stale authored state across the migration, hits a view key it can't resolve
// locally, and `apply-state.js` derefs a null view `ref` in `_startTrace`
// (gated by `node.writer.core.writable`). Increment 2 reopened the tracker under
// a fresh corestore NAMESPACE — but a namespace is NOT a clean slate: the same
// store still resolves the writable genesis core BY KEY, so it re-crashes.
//
// Increment 2b reopens the tracker against a genuinely SEPARATE Corestore (its
// own storage path) that replicates from peers. This test proves the mechanism
// that makes that work, without needing to synthesize the exact prod SIGABRT
// (which depends on a precise migration/partial-replication interleaving):
//
// 1. HEAL — a fresh separate store opened with the tracker bootstrapKey,
// replicating from a healthy MIGRATED peer, converges like a mirror: the
// migrated view materializes and every issue is readable.
// 2. CRASH GATE — the genesis writer core is WRITABLE in the origin store (the
// `node.writer.core.writable` gate that crashes), and STILL writable through
// a fresh namespace on that store (so Increment 2 can't escape it), but is a
// READ-ONLY replica in a separate store (so Increment 2b heals like a mirror).
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 { replicate, replicateAndSync } from 'autobase-test-helpers'
function tmp () { return mkdtempSync(join(tmpdir(), 'pgv3-heal-')) }
const viewKey = (t) => t.base.system.views.map(v => v.key.toString('hex')).join(',')
async function settle (bases, rounds = 30) {
for (let i = 0; i < rounds; i++) {
for (const b of bases) { try { await b.ack() } catch {} }
await replicateAndSync(bases)
}
}
// Build a healthy tracker on peers B + C whose manifest has migrated (indexer
// set changed -> new entropy-derived view key) well past genesis, with content.
async function buildMigratedTracker (dir) {
const storeA = new Corestore(join(dir, 'A')); await storeA.ready()
const A = new HyperIssues(storeA, { namespace: 'ns' }); await A.ready()
const key = A.keyHex
await A.create({ title: 'issue-1', body: 'genesis', author: 'A' })
const storeB = new Corestore(join(dir, 'B')); await storeB.ready()
const B = new HyperIssues(storeB, { namespace: 'ns', bootstrapKey: key }); await B.ready()
const storeC = new Corestore(join(dir, 'C')); await storeC.ready()
const C = new HyperIssues(storeC, { namespace: 'ns', bootstrapKey: key }); await C.ready()
await replicateAndSync([A.base, B.base, C.base])
// migration: add B + C as indexers (entropy view key changes)
await A.addWriter('B', { writerKey: B.localKeyHex, role: 'owner' })
await A.addWriter('C', { writerKey: C.localKeyHex, role: 'owner' })
await settle([A.base, B.base, C.base])
// grow the view so it is materially past genesis
for (let i = 0; i < 40; i++) await B.create({ title: 'issue-B' + i, body: 'x'.repeat(40), author: 'B' })
await settle([A.base, B.base, C.base])
const genesisLocalKey = A.base.local.key
return {
key,
genesisLocalKey,
migratedView: viewKey(B),
all: await B.list({ status: 'all' }),
peers: [B, C],
stores: { A: storeA, B: storeB, C: storeC },
trackers: { A, B, C }
}
}
test('Increment 2b HEAL — a separate corestore replicates a migrated tracker like a mirror', async (t) => {
const dir = tmp()
const g = await buildMigratedTracker(dir)
t.ok(g.all.length >= 41, `healthy migrated tracker has content (${g.all.length} issues)`)
// Open the tracker in a FRESH, SEPARATE corestore (its own path) with the same
// bootstrapKey — exactly what Increment 2b does for a broken tracker.
const healStore = new Corestore(join(dir, 'HEAL')); await healStore.ready()
const H = new HyperIssues(healStore, { namespace: 'ns', bootstrapKey: g.key })
await H.ready()
// Replicate from the healthy peers and poll until the issue DATA is readable
// (reaching the view KEY is not enough — the view blocks must download too).
const done = replicate([H.base, g.peers[0].base, g.peers[1].base])
let healed = []
let converged = false
for (let i = 0; i < 100; i++) {
try { await H.base.update() } catch {}
await new Promise((r) => setTimeout(r, 100))
if (viewKey(H) === g.migratedView) {
try { healed = await H.list({ status: 'all' }) } catch { healed = [] }
if (healed.length === g.all.length) { converged = true; break }
}
}
await done()
t.ok(converged, 'separate store converged on the migrated view + readable issue data')
t.is(viewKey(H), g.migratedView, 'separate store reached the migrated view key')
t.is(healed.length, g.all.length, 'separate store materialized every issue (heals like a mirror)')
t.absent(H.base.writable, 'separate store is a READ-ONLY replica — no stale writable local writer to advance')
t.teardown(async () => {
try { await H.close(); await healStore.close() } catch {}
for (const name of ['A', 'B', 'C']) {
try { await g.trackers[name].close() } catch {}
try { await g.stores[name].close() } catch {}
}
})
})
test('Increment 2b CRASH GATE — genesis core writable in origin store (incl. fresh namespace), read-only in a separate store', async (t) => {
const dir = tmp()
const g = await buildMigratedTracker(dir)
// The crash is gated by `node.writer.core.writable` (apply-state.js:1256): only
// a WRITABLE local writer reaches _startTrace and derefs the null view ref.
const originStore = g.stores.A
// (a) In the origin store the genesis writer core is WRITABLE — the crash gate.
const c1 = originStore.get({ key: g.genesisLocalKey })
await c1.ready()
t.is(c1.writable, true, 'genesis writer core is WRITABLE in the origin store (the crash gate)')
await c1.close()
// (b) A fresh NAMESPACE on the SAME store STILL resolves that core by key and it
// is STILL writable — Increment 2 (fresh namespace) cannot escape the gate.
const nsStore = originStore.namespace('ns#r1')
const c2 = nsStore.get({ key: g.genesisLocalKey })
await c2.ready()
t.is(c2.writable, true, 'fresh-namespace view of the SAME store keeps the genesis core WRITABLE (Increment 2 cannot escape the crash gate)')
await c2.close()
// (c) In a genuinely SEPARATE store the genesis core, once replicated, is a
// READ-ONLY replica — no writable local writer to advance, so Increment 2b
// takes the fast-forward/mirror path and heals.
const sepStore = new Corestore(join(dir, 'SEP')); await sepStore.ready()
const S = new HyperIssues(sepStore, { namespace: 'ns', bootstrapKey: g.key }); await S.ready()
const done = replicate([S.base, g.peers[0].base, g.peers[1].base])
for (let i = 0; i < 60; i++) { try { await S.base.update() } catch {} await new Promise((r) => setTimeout(r, 100)); if (viewKey(S) === g.migratedView) break }
await done()
const c3 = sepStore.get({ key: g.genesisLocalKey })
await c3.ready()
t.is(c3.writable, false, 'separate store holds the genesis core READ-ONLY (writable=false) — heals like a mirror')
await c3.close()
t.teardown(async () => {
try { await S.close(); await sepStore.close() } catch {}
for (const name of ['A', 'B', 'C']) {
try { await g.trackers[name].close() } catch {}
try { await g.stores[name].close() } catch {}
}
})
})