OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / bidirectional-e2e-convergence.test.js

161 lines · javascript ref:
// #16 END-TO-END CONVERGENCE: a repo created on the MIRROR materializes on the
// SOURCE via the reverse RepoPuller.
//
// This exercises the real #16 machinery end to end:
//   - a REAL RepoEventLog on the mirror, appended when the mirror "creates" a
//     repo, REPLICATED to the source over a REAL Hyperswarm (testnet) via
//     corestore.replicate — exactly as two daemons do;
//   - the REAL reverse RepoPuller (lib/services/repo-mirror.js) the source's
//     watcher boots on a peer's `__server__`/`__repoeventlog__` announcement,
//     streaming that log and pulling each repo;
//   - REAL git materialization: the pull does init-if-missing + `git fetch`
//     (the same shape as hyper-sync.pull) so the source ends up with a
//     byte-identical bare repo.
//
// The ONLY substitution is the fetch TRANSPORT: hyper-sync fetches over
// `punch://` (needs a git-remote-punch helper); here the two "daemons" share a
// host so we fetch from the mirror's on-disk bare repo via a local path. The
// transport is orthogonal to #16 — the fix is about DISCOVERY (source learns
// of + pulls the mirror's repo), which is fully exercised.
//
// Companion: bidirectional-repo-discovery.test.js proves a real startPearGit
// daemon BOOTS the reverse puller from the announcement. Together they cover
// discovery announce → boot → stream → pull → materialize.

import test from 'brittle'
import { mkdtempSync, existsSync, writeFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import b4a from 'b4a'
import Corestore from 'corestore'
import createTestnet from '@hyperswarm/testnet'
import Hyperswarm from 'hyperswarm'

import { execFileSync } from '../lib/_compat-exec.js'
import { RepoEventLog } from '../lib/services/repo-event-log.js'
import { RepoPuller } from '../lib/services/repo-mirror.js'

let n = 0
const tmp = (tag) => mkdtempSync(join(tmpdir(), 'pgv3-e2e-' + tag + '-' + (n++) + '-'))
const git = (args, opts = {}) => execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
function bareRef (bare, ref) {
  try { return git(['--git-dir', bare, 'rev-parse', '--verify', '--quiet', ref]) || null } catch { return null }
}
async function waitFor (pred, ms = 20000) {
  const t0 = Date.now()
  while (Date.now() - t0 < ms) {
    try { if (await pred()) return true } catch {}
    await new Promise((r) => setTimeout(r, 100))
  }
  return false
}

// Real sync adapter: same contract the RepoPuller uses on hyper-sync
// (`remotes` Map, addRemote, pull) and the same init-if-missing + git fetch
// behavior — but fetches from the remote's on-disk bare repo via a local path
// instead of punch:// (transport is orthogonal to #16; see header).
class LocalFetchSync {
  constructor (sourceReposDir) {
    this._reposDir = sourceReposDir
    this.remotes = new Map()      // name -> { bareDirFor(name) }
    this.served = new Map()       // name -> bare path (source's own served repos)
    this.pulls = []
  }

  addRemote (name, remoteReposDir) { this.remotes.set(name, remoteReposDir) }

  async pull (remoteName, repoName) {
    this.pulls.push({ remoteName, repoName })
    const remoteReposDir = this.remotes.get(remoteName)
    if (!remoteReposDir) throw new Error('unknown remote ' + remoteName)
    const remoteBare = join(remoteReposDir, repoName + '.git')
    const localBare = join(this._reposDir, repoName + '.git')
    if (!existsSync(localBare)) git(['init', '--bare', '-q', localBare]) // init-if-missing
    git(['-C', localBare, 'fetch', remoteBare, '+refs/*:refs/*', '--no-tags']) // fetch-if-present
    this.served.set(repoName, localBare)
  }
}

test('#16 e2e: a repo created on the mirror converges onto the source via the reverse RepoPuller', async (t) => {
  t.timeout(90000)
  const testnet = await createTestnet(3, t.teardown)
  const topic = b4a.alloc(32, 'pgv3-e2e-16')

  // ── MIRROR: real bare repo + a commit, and a real RepoEventLog ──
  const mirrorReposDir = tmp('mirror-repos')
  const mirrorBare = join(mirrorReposDir, 'mirror-made.git')
  git(['init', '--bare', '-q', mirrorBare])
  const work = tmp('work')
  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, 'README.md'), 'made-on-the-mirror\n')
  git(['-C', work, 'add', '.'])
  git(['-C', work, 'commit', '-q', '-m', 'created on mirror'])
  const oid = git(['-C', work, 'rev-parse', 'HEAD'])
  git(['-C', work, 'push', '-q', mirrorBare, 'HEAD:refs/heads/main'])
  t.is(bareRef(mirrorBare, 'refs/heads/main'), oid, 'precondition: mirror serves mirror-made@main')

  const mirrorStore = new Corestore(tmp('mirror-store'))
  await mirrorStore.ready()
  const mirrorLog = new RepoEventLog({ corestore: mirrorStore })
  await mirrorLog.ready()
  const mirrorSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  mirrorSwarm.on('connection', (conn) => mirrorStore.replicate(conn))
  mirrorSwarm.join(topic, { server: true, client: false })
  await mirrorSwarm.flush()

  // The mirror's own serve-hook would append this 'created' entry.
  await mirrorLog.append({ op: 0, name: 'mirror-made', head: oid })

  // ── SOURCE: opens the mirror's RepoEventLog BY KEY (real replication) ──
  const sourceReposDir = tmp('source-repos')
  const sourceStore = new Corestore(tmp('source-store'))
  await sourceStore.ready()
  const sourceView = new RepoEventLog({ corestore: sourceStore, key: mirrorLog.keyHex })
  await sourceView.ready()
  const sourceSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  sourceSwarm.on('connection', (conn) => sourceStore.replicate(conn))
  sourceSwarm.join(topic, { server: false, client: true })
  await sourceSwarm.flush()

  t.teardown(async () => {
    try { await mirrorSwarm.destroy() } catch {}
    try { await sourceSwarm.destroy() } catch {}
    try { await mirrorLog.close() } catch {}
    try { await sourceView.close() } catch {}
    try { await mirrorStore.close() } catch {}
    try { await sourceStore.close() } catch {}
  })

  // Precondition: the source does NOT have the mirror-made repo.
  t.absent(existsSync(join(sourceReposDir, 'mirror-made.git')), 'precondition: source lacks mirror-made before sync')

  // ── The #16 reverse RepoPuller the source's watcher boots for this peer ──
  const sync = new LocalFetchSync(sourceReposDir)
  sync.addRemote('peer-mirror', mirrorReposDir) // stands in for the mirror's punch:// serverKey

  const puller = new RepoPuller({
    sync,
    log: sourceView,
    primaryServerKey: 'f'.repeat(64),
    remoteName: 'peer-mirror',
    cursorPath: join(tmp('cursor'), 'c.json')
  })
  t.teardown(async () => { try { await puller.close() } catch {} })
  await puller.start()

  // ── ASSERT CONVERGENCE: the mirror-created repo materializes on the source ──
  const converged = await waitFor(() => bareRef(join(sourceReposDir, 'mirror-made.git'), 'refs/heads/main') === oid, 25000)
  t.ok(converged, 'source materialized mirror-made@main via the reverse puller (110→112 convergence)')
  t.is(bareRef(join(sourceReposDir, 'mirror-made.git'), 'refs/heads/main'), oid, 'source ref == mirror ref (same oid)')
  t.is(
    git(['--git-dir', join(sourceReposDir, 'mirror-made.git'), 'cat-file', '-p', oid + ':README.md']),
    'made-on-the-mirror',
    'blob byte-identical on the source (real content converged, not just a ref)'
  )
  t.ok(sync.pulls.some(p => p.repoName === 'mirror-made' && p.remoteName === 'peer-mirror'),
    'reverse puller pulled mirror-made from the mirror peer')
})