OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / ref-publish-reconcile.test.js

97 lines · javascript ref:
// ref-publish-reconciler — origin-side reconcile (Keet "right state" pattern).
//
// Scenario: a ref advances on disk but the producer event was MISSED (daemon
// down / dead hook), so the autobase ref-view never moved and the commit never
// propagated. The reconcile must notice disk-is-ahead and re-announce it.

import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync, writeFileSync } 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 { reconcileRepoRefs } from '../lib/services/ref-publish-reconciler.js'

function git (args, opts = {}) {
  return execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
}
function headOf (refs, ref) {
  const r = refs.find((x) => x.ref === ref)
  return r && r.hash
}

test('reconcile re-announces a disk-ahead ref the producer missed', async (t) => {
  const dir = mkdtempSync(join(tmpdir(), 'pgv3-reconcile-'))
  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'), 'one\n')
  git(['-C', work, 'add', '.'])
  git(['-C', work, 'commit', '-q', '-m', 'c1'])
  const o1 = git(['-C', work, 'rev-parse', 'HEAD'])
  git(['-C', work, 'push', '-q', origin, 'HEAD:refs/heads/main'])

  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:reconcile-test' })
  await tracker.ready()
  t.teardown(async () => {
    try { await tracker.close() } catch {}
    try { await overlay.close() } catch {}
    try { await store.close() } catch {}
  })

  const common = {
    repoPath: origin,
    repoName: 'reconcile-test',
    tracker,
    overlay,
    spoolDir: join(dir, 'gip-bundles'),
    userId: 'u-test'
  }

  // 1. Normal publish of o1 — the event fired, view == o1.
  await publishRefUpdate({ ...common, ref: 'refs/heads/main', oldOid: null, newOid: o1 })
  t.is(headOf(await tracker.listRefs(), 'refs/heads/main'), o1, 'view at o1 after normal publish')

  // 2. Advance the bare repo to o2 but DO NOT announce — the missed event.
  writeFileSync(join(work, 'a.txt'), 'two\n')
  git(['-C', work, 'commit', '-q', '-am', 'c2'])
  const o2 = git(['-C', work, 'rev-parse', 'HEAD'])
  git(['-C', work, 'push', '-q', origin, 'HEAD:refs/heads/main'])
  t.is(headOf(await tracker.listRefs(), 'refs/heads/main'), o1, 'view STILL at o1 (event missed); disk is ahead at o2')

  // 3. Reconcile: disk (o2) ahead of view (o1) → re-announce.
  const r1 = await reconcileRepoRefs(common)
  t.is(r1.published, 1, 'reconcile published the missed ref')
  t.is(headOf(await tracker.listRefs(), 'refs/heads/main'), o2, 'view caught up to o2 after reconcile')

  // 4. Idempotent — second pass is a no-op.
  const r2 = await reconcileRepoRefs(common)
  t.is(r2.published, 0, 'second reconcile publishes nothing')
  t.is(r2.inSync, 1, 'ref reported in-sync')
})

test('reconcile is a no-op on a read-only (mirror) tracker', async (t) => {
  const r = await reconcileRepoRefs({
    repoPath: '/nonexistent.git',
    repoName: 'x',
    tracker: { writable: false },
    overlay: {},
    spoolDir: '/tmp/x',
    userId: 'u'
  })
  t.alike(r, { published: 0, skipped: 0, inSync: 0 }, 'mirror tracker → no publish, no disk access')
})