// #52 Phase 1.5 — THE cutover proof. Boot the experimental daemon on a COPY
// of prod's REAL store (allowBackup) and assert full parity: identity address
// preserved, orgs migrated v1→v2 (#37), org issues migrated into rooms (#42),
// IdentityRegistry users read back, schema version stamped. This is the
// strongest pre-cutover evidence — real data, real boot path, real migration.
//
// SAFETY: copies ~/pgv3-storage read-only to a tmp dir and only ever touches
// the COPY. The live prod daemon on the original is undisturbed (separate
// inode + allowBackup). Skips if prod store / orgs fixture is absent.
//
// Heavy (copies ~11G of prod data). Run manually, under BARE:
// bare test/cutover-prod-copy.test.js
//
// NOT node, despite what this said until 2026-07-31: importing ../index.js pulls hyper-overlay's
// bare-path, which dereferences the `Bare` global at module scope and throws under node before a single
// assertion runs. The stale instruction reads as "this test is broken" rather than "wrong runtime" —
// which is roughly how it came to be dismissed as broken while being the only test positioned to catch
// the hypercore-storage downgrade (see org #24).
import test from 'brittle'
import process from '#node:process'
import { execSync } from '../lib/_compat-exec.js'
import { existsSync, mkdirSync, readFileSync, rmSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { Client } from 'hyper-svc'
// Read prod identity BEFORE importing the daemon (daemon reads env at boot).
process.env.PGV3_USE_RAW_IDENTITY = '1'
const { startPearGit } = await import('../index.js')
const PROD_STORAGE = '/home/fleeky/pgv3-storage'
const PROD_ORGS_FIXTURE = '/tmp/prod-orgs.json' // pgh --json org list > /tmp/prod-orgs.json
test('cutover: experimental daemon boots on a COPY of prod store, full parity', async (t) => {
// Booting on a COPY OF REAL PROD DATA is not a unit test: this store carries
// ~140 repos and a dozen orgs, and boot mounts every one of them. brittle's
// default 30s expires mid-mount, which reads as a hang rather than as "the
// daemon is working". Give it room proportional to the real workload.
t.timeout(180000)
if (!existsSync(PROD_STORAGE)) { t.pass('skipped — prod storage absent'); return }
const prodIdentity = JSON.parse(readFileSync(join(PROD_STORAGE, 'identity.json'), 'utf8'))
const prodOrgs = existsSync(PROD_ORGS_FIXTURE) ? JSON.parse(readFileSync(PROD_ORGS_FIXTURE, 'utf8')) : null
// prod may already be cut over (v2) — capture its version so the invariant
// is "the test only touches the COPY, never mutates prod", not "prod is v0".
const readVer = (dir) => { try { return JSON.parse(readFileSync(join(dir, 'schema-version.json'), 'utf8')).version } catch { return 0 } }
const prodVerBefore = readVer(PROD_STORAGE)
// ONE FIXED ROOT, SWEPT AT START — not mkdtemp. This copy is ~11G, and mkdtemp meant every
// interrupted run left a fresh 11G directory behind forever: on 2026-07-30 eight of them had
// accumulated in /tmp (88G) from runs killed between the `cp -r` below and teardown.
//
// t.teardown alone cannot fix that — it only runs when the test COMPLETES, and the failure mode is
// precisely a run that does not. A fixed path is what actually bounds the damage: the sweep below
// reclaims the previous crashed run's copy, so at most ONE stale copy can ever exist regardless of
// how the process dies. The exit/signal hooks are a best-effort bonus on top, not the mechanism.
//
// TRADE-OFF, stated: two concurrent runs of THIS test on one machine would now collide. That was
// never a supported thing to do (they would copy 22G of prod), and bounded disk is worth more.
const ROOT = join(tmpdir(), 'cutover-prod-copy')
try { rmSync(ROOT, { recursive: true, force: true }) } catch {}
mkdirSync(ROOT, { recursive: true })
const cleanup = () => { try { rmSync(ROOT, { recursive: true, force: true }) } catch {} }
// Registered BEFORE the expensive copy, so an early abort or a boot failure is covered too — the
// old code only registered teardown after a successful boot.
t.teardown(cleanup)
try { process.on('exit', cleanup) } catch {}
for (const sig of ['SIGINT', 'SIGTERM']) {
try { process.on(sig, () => { cleanup(); process.exit(130) }) } catch {}
}
// 1. read-only copy of the live prod store → tmp (original untouched).
// `copy` must NOT exist beforehand: `cp -r src dst` creates dst as the copy. Pre-creating it would
// nest the store one level deeper and every path below would miss.
const copy = join(ROOT, 'storage')
execSync('cp -r ' + JSON.stringify(PROD_STORAGE) + ' ' + JSON.stringify(copy))
// drop the copied live socket so the daemon makes its own
try { rmSync(join(copy, 'svc.sock')) } catch {}
t.ok(existsSync(join(copy, 'store')), 'store copied')
t.is(readVer(copy), prodVerBefore, 'copy starts at prod\'s current schema version (v' + prodVerBefore + ')')
// 1b. a FIXTURE bare repo (never the real ~/git-repos — the daemon may write
// hooks into reposDir). Proves the MIGRATED daemon still serves repos — the
// core forge function, which the original cutover proof omitted.
// Both fixtures live UNDER the swept root too, so the one cleanup covers everything. They were
// separate mkdtemps before, which is why 22 stray cutover-repo*/cutover-repos* dirs outlived their
// runs alongside the big ones.
const work = join(ROOT, 'work')
execSync('git init -q "' + work + '"')
execSync('git -C "' + work + '" -c user.email=t@t -c user.name=t commit --allow-empty -q -m init')
const reposDir = join(ROOT, 'repos')
mkdirSync(reposDir, { recursive: true })
execSync('git clone -q --bare "' + work + '" "' + join(reposDir, 'fixture.git') + '"')
// 2. boot the REAL experimental daemon on the copy (allowBackup, hermetic)
let node
try {
node = await startPearGit({
storage: copy,
uds: join(copy, 'svc.sock'),
reposDir,
http: { port: 0 },
swarm: false,
deploy: false,
autoImportSSH: false,
allowBackup: true
})
} catch (err) {
t.fail('daemon failed to boot on prod-store copy: ' + err.message)
cleanup() // was `rmSync(copy)` — which left the parent dir AND both fixtures behind
return
}
// Close the daemon THEN remove its storage, in one teardown, so the order does not depend on
// brittle's teardown sequencing. cleanup() is idempotent (force:true), so the earlier registration
// running as well is a harmless no-op.
t.teardown(async () => { try { await node.close() } catch {}; cleanup() })
// 3. IDENTITY — same address (publicKey) as prod
t.is(node.identity.publicKeyHex, prodIdentity.publicKey,
'daemon identity == prod identity (punch:// address preserved)')
// 4. ORGS — migrated v1 → v2, all present
const orgs = await node.orgs.list()
t.ok(orgs.length > 0, 'orgs present after migration (' + orgs.length + ')')
if (prodOrgs) {
// subset: every org in the (possibly-stale) fixture must be migrated; the
// live copy may have MORE orgs created since the fixture was captured.
const migratedNames = new Set(orgs.map(o => o.name))
const missing = prodOrgs.map(o => o.name).filter(n => !migratedNames.has(n))
t.alike(missing, [], 'every prod-fixture org migrated (missing: ' + JSON.stringify(missing) + ')')
// members preserved on the org that has them
for (const po of prodOrgs.filter(o => (o.members || []).length > 0)) {
const mig = await node.orgs.get(po.name)
const ids = new Set((mig.members || []).map(m => m.userId))
for (const pm of po.members) {
t.ok(ids.has(pm.userId), po.name + ' member ' + String(pm.userId).slice(0, 8) + ' preserved')
}
}
}
// 5. ORG ISSUES — readable through the linked-core room (migrated #42)
let issuesTotal = 0
for (const o of orgs) {
const tracker = await node.issueRegistry.getForOrg(o.name)
const list = await tracker.list()
issuesTotal += list.length
}
t.ok(issuesTotal >= 0, 'org issue trackers all open + readable post-migration (' + issuesTotal + ' org issues)')
// 6. IDENTITY REGISTRY — users read back under the new code (schema pass-through)
const users = await node.identityRegistry.listUsers()
t.ok(Array.isArray(users), 'IdentityRegistry.listUsers() reads prod identities (' + users.length + ')')
// 6b. REPO SERVING — the migrated daemon still serves bare repos from reposDir
// (the core forge function). list-repos reflects reposDir even without swarm.
const client = await Client.discover({ uds: node.svc.uds.path })
t.teardown(async () => { try { await client.close() } catch {} })
const repoList = await client.call('list-repos', {})
t.is(repoList.active, true, 'sync/repo service active on the migrated daemon')
t.ok(repoList.repoCount >= 1, 'migrated daemon serves bare repos from reposDir (' + repoList.repoCount + ')')
// 7. VERSION — the COPY ends at target v2 (whether it started v0 and migrated,
// or started v2 and the boot was an idempotent no-op).
t.ok(existsSync(join(copy, 'schema-version.json')), 'schema-version.json present on the copy')
t.is(readVer(copy), 2, 'copy at schema v2 after boot')
// INVARIANT: the test only touches the COPY — prod's version is unchanged.
t.is(readVer(PROD_STORAGE), prodVerBefore, 'prod ORIGINAL version unchanged by the test (v' + prodVerBefore + ')')
})