// gip-overlay P3e — RefReconcilerManager.
//
// The consumer-side lifecycle: ensureFor(repo) opens the per-repo tracker,
// inits the bare repo if missing, wires the overlay-backed ensureObjects, and
// starts a RefReconciler so the bare repo follows the autobase. Deterministic,
// in-process (real git + real tracker + real overlay, no swarm); the over-
// swarm version is the two-node e2e.
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync, writeFileSync, existsSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { HyperIssues } from 'hyper-issues'
// hyper-overlay exports the class as `HyperOverlay`; alias it the same way
// lib/pear-git.js does so the V2-era call sites below keep reading naturally.
import { HyperOverlay as HyperOverlayV2 } from 'hyper-overlay'
import { execFileSync } from '../lib/_compat-exec.js'
import { publishRefUpdate } from '../lib/gip-publish.js'
import { RefReconcilerManager } from '../lib/services/ref-reconciler-manager.js'
function git (args, opts = {}) {
return execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
}
test('ensureFor inits a missing bare repo and materializes it from the autobase', async (t) => {
const dir = mkdtempSync(join(tmpdir(), 'pgv3-recmgr-'))
// origin bare repo with one commit
const origin = join(dir, 'origin.git')
const work = join(dir, 'work')
git(['init', '--bare', '-q', origin])
git(['init', '-q', work])
git(['-C', work, 'config', 'user.email', 't@t'])
git(['-C', work, 'config', 'user.name', 'T'])
git(['-C', work, 'config', 'commit.gpgsign', 'false'])
writeFileSync(join(work, 'a.txt'), 'hello\n')
git(['-C', work, 'add', '.'])
git(['-C', work, 'commit', '-q', '-m', 'c1'])
const oid = git(['-C', work, 'rev-parse', 'HEAD'])
git(['-C', work, 'push', '-q', origin, 'HEAD:refs/heads/main'])
// real overlay + tracker; publish the commit
const store = new Corestore(join(dir, 'store'))
const overlay = new HyperOverlayV2(store, { namespace: 'overlay-v2', destDir: join(dir, 'fetch') })
await overlay.ready()
const tracker = new HyperIssues(store, { namespace: 'issues:repo:gip-test' })
await tracker.ready()
t.teardown(async () => { try { await tracker.close() } catch {}; try { await overlay.close() } catch {}; try { await store.close() } catch {} })
await publishRefUpdate({ repoPath: origin, repoName: 'gip-test', ref: 'refs/heads/main', oldOid: null, newOid: oid, tracker, overlay, spoolDir: join(dir, 'gip-bundles'), userId: 'u' })
// consumer node: a separate reposDir, no bare repo yet
const reposDir = join(dir, 'consumer-repos')
const mgr = new RefReconcilerManager({
issueRegistry: { async getForRepo () { return tracker } }, // same tracker stands in for the replicated one
overlay,
reposDir
})
t.teardown(async () => { try { await mgr.closeAll() } catch {} })
const target = join(reposDir, 'gip-test.git')
t.absent(existsSync(target), 'no bare repo before ensureFor')
await mgr.ensureFor('gip-test')
t.ok(existsSync(target), 'bare repo initialized by ensureFor')
t.is(git(['--git-dir', target, 'rev-parse', 'refs/heads/main']), oid, 'ref materialized from the autobase')
t.is(git(['--git-dir', target, 'cat-file', '-p', oid + ':a.txt']), 'hello', 'blob byte-identical')
})
test('ensureFor is idempotent (one reconciler per repo)', async (t) => {
const dir = mkdtempSync(join(tmpdir(), 'pgv3-recmgr2-'))
const store = new Corestore(join(dir, 'store'))
const overlay = new HyperOverlayV2(store, { namespace: 'overlay-v2', destDir: join(dir, 'fetch') })
await overlay.ready()
const tracker = new HyperIssues(store, { namespace: 'issues:repo:r' })
await tracker.ready()
t.teardown(async () => { try { await tracker.close() } catch {}; try { await overlay.close() } catch {}; try { await store.close() } catch {} })
const mgr = new RefReconcilerManager({ issueRegistry: { async getForRepo () { return tracker } }, overlay, reposDir: join(dir, 'repos') })
t.teardown(async () => { try { await mgr.closeAll() } catch {} })
const a = await mgr.ensureFor('r')
const b = await mgr.ensureFor('r')
t.ok(a === b, 'same reconciler instance returned')
})