OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / pgh-bootstrap.test.js

150 lines · javascript ref:
/**
 * 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'))
})