/**
* pgh bootstrap tests — P.10 Phase 0.5i
*
* Covers: parseBootstrap spec resolution, BOOTSTRAP_TOPIC determinism,
* `pgh init` setup of config + identity + store dirs.
*/
import test from 'brittle'
import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import crypto from 'hypercore-crypto'
import { parseBootstrap, BOOTSTRAP_TOPIC, BOOTSTRAP_VERSION } from '../lib/bootstrap.js'
// ── parseBootstrap ──────────────────────────────────────────
test('BOOTSTRAP_TOPIC is deterministic', (t) => {
const t1 = crypto.hash(Buffer.from('pgh-bootstrap-v1'))
t.is(Buffer.compare(BOOTSTRAP_TOPIC, t1), 0)
t.is(BOOTSTRAP_TOPIC.length, 32)
t.is(BOOTSTRAP_VERSION, 'pgh-bootstrap-v1')
})
test('parseBootstrap — undefined returns default topic', (t) => {
const r = parseBootstrap()
t.is(r.kind, 'topic')
t.is(Buffer.compare(r.value, BOOTSTRAP_TOPIC), 0)
t.is(r.label, BOOTSTRAP_VERSION)
})
test('parseBootstrap — punch:// URL parses', (t) => {
const key = 'a'.repeat(64)
const r = parseBootstrap('punch://' + key + '/anything')
t.is(r.kind, 'punch')
t.is(r.value, key)
t.ok(r.label.startsWith('punch://aaaaaaaaaaaa'))
})
test('parseBootstrap — pgh://publisher parses', (t) => {
const r = parseBootstrap('pgh://fleeky/pear-git-v3')
t.is(r.kind, 'pgh')
t.is(r.value, 'fleeky')
})
test('parseBootstrap — topic:<hex> parses', (t) => {
const hex = 'c'.repeat(64)
const r = parseBootstrap('topic:' + hex)
t.is(r.kind, 'topic')
t.is(r.value.toString('hex'), hex)
})
test('parseBootstrap — invalid topic length rejects', (t) => {
t.exception(() => parseBootstrap('topic:abc'))
})
test('parseBootstrap — unknown scheme rejects', (t) => {
t.exception(() => parseBootstrap('http://example.com'))
t.exception(() => parseBootstrap('random-string'))
})
// ── pgh init ────────────────────────────────────────────────
//
// We can't easily override HOME for the existing init command (uses
// ~/.config/pgh paths hardcoded via os.homedir()). Instead, snapshot
// the user's existing config + identity (if any), run init in a
// controlled way, verify, then restore.
function snapshot (p) {
if (!fs.existsSync(p)) return { existed: false }
const isDir = fs.statSync(p).isDirectory()
if (isDir) {
return { existed: true, kind: 'dir', entries: fs.readdirSync(p) }
}
return { existed: true, kind: 'file', content: fs.readFileSync(p) }
}
test('pgh init creates expected dirs + config (idempotent-by-design)', async (t) => {
const { run } = await import('../lib/commands/init.js')
const configPath = path.join(os.homedir(), '.config', 'pgh', 'config.json')
const identityFile = path.join(os.homedir(), '.pgh', 'identity', 'identity.json')
const configSnap = snapshot(configPath)
const identitySnap = snapshot(identityFile)
try {
// Fresh init — use --force to handle "already initialized"
const result = await run(null, ['--force', '--offline', '--quiet'])
t.ok(result.initialized)
t.ok(fs.existsSync(configPath))
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
t.is(config.bootstrapVersion, BOOTSTRAP_VERSION)
t.is(config.bootstrapTopicHex, BOOTSTRAP_TOPIC.toString('hex'))
t.ok(config.initializedAt)
t.ok(fs.existsSync(identityFile))
const id = JSON.parse(fs.readFileSync(identityFile, 'utf8'))
t.is(id.publicKey.length, 64, 'hex 32 bytes')
t.is(id.secretKey.length, 128, 'hex 64 bytes')
// Without --force, re-running reports already initialized
const secondRun = await run(null, ['--offline', '--quiet'])
t.ok(secondRun.alreadyInitialized)
} finally {
// Restore snapshots
if (!configSnap.existed) {
try { fs.unlinkSync(configPath) } catch {}
} else if (configSnap.kind === 'file') {
fs.writeFileSync(configPath, configSnap.content)
}
if (!identitySnap.existed) {
try { fs.unlinkSync(identityFile) } catch {}
} else if (identitySnap.kind === 'file') {
fs.writeFileSync(identityFile, identitySnap.content)
}
}
})
test('pgh init --bootstrap punch://KEY resolves custom spec', async (t) => {
const { run } = await import('../lib/commands/init.js')
const configPath = path.join(os.homedir(), '.config', 'pgh', 'config.json')
const configSnap = snapshot(configPath)
try {
const key = 'd'.repeat(64)
const result = await run(null, [
'--force', '--offline', '--quiet',
'--bootstrap', 'punch://' + key + '/anything'
])
t.ok(result.initialized)
t.is(result.config.bootstrapSpec, 'punch://' + key + '/anything')
} finally {
if (!configSnap.existed) {
try { fs.unlinkSync(configPath) } catch {}
} else if (configSnap.kind === 'file') {
fs.writeFileSync(configPath, configSnap.content)
}
}
})
test('pgh init --bootstrap bad-spec errors out via process.exit stub', async (t) => {
// We can't actually call process.exit in a test. Invoke parseBootstrap
// directly to verify the path that exit() runs on.
t.exception(() => parseBootstrap('gibberish'))
})