/**
* 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')
})