OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / retired-mirror-boot-order.test.js

80 lines · javascript ref:
/**
 * A mirror learns its tombstones AFTER the boot scan has already run.
 *
 * Boot order on a MIRROR:
 *
 *   1. pear-git.js  foldRetired(repoEventLog)   -- the daemon's OWN log, which
 *                                                  on a mirror holds NO
 *                                                  tombstones. Set is empty.
 *   2. startSyncService  sync.scan()            -- prunes against that EMPTY
 *                                                  set, so every retired bare
 *                                                  dir stays in sync.repos.
 *   3. RepoPuller._runLoop  foldRetired(origin) -- asynchronously, later, the
 *                                                  set becomes populated.
 *   4. ...nothing re-prunes the map.
 *
 * So the mirror knows the tombstones and still lists the repos. Measured on
 * stortron 2026-09-02 right after deploying 0.1.247: `pgh repo retired`
 * returned all 15 names while `pgh repo` still listed 148; a manual
 * `pgh repo scan` dropped it to 133, confirming the set was right and only the
 * already-scanned map was stale.
 *
 * On an ORIGIN step 1 yields the real set before step 2, which is why this
 * never showed up there.
 */

import test from 'brittle'
import { mkdtempSync, mkdirSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { execFile } from '../lib/_compat-exec.js'

import { startSyncService } from '../lib/services/sync.js'
import { RetiredRepos, OP_RETIRED } from '../lib/services/retired-repos.js'

let n = 0
const tmpDir = () => mkdtempSync(join(tmpdir(), 'pgv3-bootorder-' + (n++) + '-'))
const listed = (sync) => sync.status().repos.map((r) => r.name).sort()

test('a tombstone learned AFTER the boot scan still drops out of the listing', async (t) => {
  t.timeout(60000)

  const reposDir = join(tmpDir(), 'git-repos')
  mkdirSync(reposDir, { recursive: true })
  for (const name of ['alpha', 'beta']) {
    await execFile('git', ['init', '--bare', '-q', join(reposDir, name + '.git')])
  }

  // Mirror boot: the set is EMPTY when the sync service scans.
  const retired = new RetiredRepos()

  const sync = await startSyncService({
    reposDir,
    offline: true,
    isRetired: retired.isRetired
  })
  t.teardown(async () => {
    try { sync._reposWatcher.cancelRescan() } catch {}
    try { sync._reposWatcher.close() } catch {}
    try { await sync.close() } catch {}
  })

  t.alike(listed(sync), ['alpha', 'beta'], 'boot scan lists both — nothing retired yet')

  // The RepoPuller's fold of the ORIGIN's log lands, populating the shared set.
  retired.applyEntry({ op: OP_RETIRED, name: 'alpha' }, 0)
  t.ok(retired.has('alpha'), 'the set now knows the tombstone')

  // Stop the watcher so only the explicit hook can rescue the assertion.
  sync._reposWatcher.cancelRescan()
  sync._reposWatcher.close()

  t.is(typeof sync.pruneRetired, 'function',
    'the sync service exposes pruneRetired so a late fold can re-prune')

  sync.pruneRetired()

  t.alike(listed(sync), ['beta'],
    'the late-learned tombstone drops out without re-walking the repos dir')
})