// The default branch (HEAD) is REPLICATED state, not per-backend config.
//
// HEAD is a symbolic ref and the ref-view schema stores ref→hash pairs, so it
// was never modelable there. hyper-sync sets it locally on scan and
// _repointHeadIfDangling repairs it with a master→main→first heuristic — and
// two nodes running that heuristic against different materialization orders
// pick DIFFERENT defaults. The default branch is what the repo page renders
// and what `git clone` checks out.
//
// Measured across the live pair before this: 4 of 142 repos disagreed, e.g.
// hyper-svc tabertron=main stortron=bare-migration (20 vs 7 commits)
// and the public saw whichever backend the load balancer happened to hit.
//
// This rides the EXISTING generic set-meta op (meta:head), so there is no new
// op type, no wire-format change, no schema version bump, and no schema-guard
// interaction — an older node just stores the key and ignores it.
import test from 'brittle'
import { mkdtempSync, writeFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { RefReconciler } from '../lib/services/ref-reconciler.js'
import { execFileSync } from '../lib/_compat-exec.js'
function git (args, opts = {}) {
return execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
}
function tmp () { return mkdtempSync(join(tmpdir(), 'pgv3-head-')) }
function headOf (bare) {
try { return git(['--git-dir', bare, 'symbolic-ref', '--quiet', 'HEAD']) } catch { return null }
}
// Tracker stand-in: the per-repo ref view plus the generic meta store.
function fakeTracker ({ refs = [], meta = {}, writable = true } = {}) {
return {
writable,
base: { on () {}, off () {} },
async listRefs () { return refs.slice() },
async getMeta (k) { return meta[k] },
async setMeta (k, v) { meta[k] = v },
_meta: meta
}
}
// A bare repo with two real branches, so "adopt the other one" is meaningful.
async function makeRepo () {
const dir = tmp()
const bare = join(dir, 'repo.git')
const work = join(dir, 'work')
git(['init', '--bare', '-q', bare])
git(['init', '-q', '-b', 'main', 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'), 'one\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', bare, 'HEAD:refs/heads/main'])
git(['-C', work, 'push', '-q', bare, 'HEAD:refs/heads/legacy'])
return { bare, oid }
}
test('a node ADOPTS the replicated default branch', async (t) => {
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/legacy'])
t.is(headOf(bare), 'refs/heads/legacy', 'starts on the wrong branch (the divergence)')
const tracker = fakeTracker({
refs: [{ ref: 'refs/heads/main', hash: oid }, { ref: 'refs/heads/legacy', hash: oid }],
meta: { head: 'refs/heads/main' }
})
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.is(headOf(bare), 'refs/heads/main', 'HEAD converged on the replicated value')
})
test('adoption DEFERS when the target ref has not materialized yet', async (t) => {
// The hazard: pointing HEAD at a ref we do not have leaves a DANGLING HEAD,
// and `git clone` then finds no default branch at all — strictly worse than
// divergence.
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/main'])
const tracker = fakeTracker({
refs: [{ ref: 'refs/heads/main', hash: oid }],
meta: { head: 'refs/heads/not-here-yet' }
})
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.is(headOf(bare), 'refs/heads/main', 'HEAD left alone rather than pointed at a missing ref')
t.ok(git(['--git-dir', bare, 'rev-parse', '--verify', 'HEAD']), 'HEAD still resolves — repo is cloneable')
})
test('adoption happens on a LATER pass once the ref arrives', async (t) => {
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/legacy'])
// main must be genuinely ABSENT from the bare repo for this to test deferral
// — the guard is "does the ref exist on disk", not "is it in the view".
git(['--git-dir', bare, 'update-ref', '-d', 'refs/heads/main'])
// Pass 1: the view names a branch this node does not have yet.
const tracker = fakeTracker({
refs: [{ ref: 'refs/heads/legacy', hash: oid }],
meta: { head: 'refs/heads/main' }
})
const rec = new RefReconciler({ tracker, repoPath: bare })
await rec.reconcile()
t.is(headOf(bare), 'refs/heads/legacy', 'deferred on pass 1')
// Pass 2: main has now materialized (the view advertises it, so the
// reconciler creates it on disk).
tracker.listRefs = async () => [
{ ref: 'refs/heads/legacy', hash: oid }, { ref: 'refs/heads/main', hash: oid }
]
await rec.reconcile()
t.is(headOf(bare), 'refs/heads/main', 'adopted once the target existed')
})
test('a writable node SEEDS the value when the view has none', async (t) => {
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/main'])
const tracker = fakeTracker({ refs: [{ ref: 'refs/heads/main', hash: oid }], meta: {} })
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.is(tracker._meta.head, 'refs/heads/main', 'published its local default so the fleet can converge')
})
test('an existing replicated value is never overwritten — no flap', async (t) => {
// Two writers each re-publishing their own answer would make the fleet's
// default branch oscillate forever. First writer wins; others adopt.
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/legacy'])
const tracker = fakeTracker({
refs: [{ ref: 'refs/heads/main', hash: oid }, { ref: 'refs/heads/legacy', hash: oid }],
meta: { head: 'refs/heads/main' }
})
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.is(tracker._meta.head, 'refs/heads/main', 'the view still holds the first writer\'s value')
t.is(headOf(bare), 'refs/heads/main', 'and this node adopted it instead of asserting its own')
})
test('a read-only node adopts but never seeds', async (t) => {
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/main'])
const tracker = fakeTracker({ refs: [{ ref: 'refs/heads/main', hash: oid }], meta: {}, writable: false })
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.absent(tracker._meta.head, 'a mirror does not publish into the view')
})
test('a tracker without meta support is a no-op (older peers)', async (t) => {
const { bare, oid } = await makeRepo()
git(['--git-dir', bare, 'symbolic-ref', 'HEAD', 'refs/heads/main'])
const tracker = { writable: true, base: { on () {}, off () {} }, async listRefs () { return [{ ref: 'refs/heads/main', hash: oid }] } }
await new RefReconciler({ tracker, repoPath: bare }).reconcile()
t.is(headOf(bare), 'refs/heads/main', 'reconciliation proceeds normally without getMeta/setMeta')
})