OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / release-registry.test.js

265 lines · javascript ref:
/**
 * ReleaseRegistry tests — P.10 Phase 0.5g
 */

import test from 'brittle'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import crypto from 'hypercore-crypto'
import Corestore from 'corestore'

import {
  ReleaseRegistry,
  canonicalBytes,
  signEntry,
  verifyEntry
} from '../lib/release-registry.js'

function mktempStore () {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-reg-'))
  return { store: new Corestore(dir), dir }
}

function fakeEntry (overrides = {}) {
  return {
    name: 'pear-git-v3',
    version: '1.0.0',
    treeHash: 'a'.repeat(64),
    serverKey: 'b'.repeat(64),
    upgrade: 'pgh://fleeky/pear-git-v3',
    deps: [{ name: 'hyper-svc', version: '0.1.0', tree: 'c'.repeat(64) }],
    publishedAt: new Date().toISOString(),
    publisherDevicePubkey: 'd'.repeat(64),
    ...overrides
  }
}

// ── Lifecycle ───────────────────────────────────────────────

test('ready + close', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()
  t.ok(reg.key, 'has core key')
  t.is(reg.keyHex.length, 64, 'hex keyHex')
  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

test('ready twice is idempotent', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()
  await reg.ready()
  t.pass('second ready ok')
  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// ── add / get / latest ──────────────────────────────────────

test('add + get round-trip', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  const entry = fakeEntry()
  await reg.add(entry)

  const back = await reg.get('pear-git-v3', '1.0.0')
  t.ok(back)
  t.is(back.treeHash, entry.treeHash)
  t.is(back.upgrade, entry.upgrade)
  t.is(back.deps.length, 1)

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

test('add — rejects entries missing required fields', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  let err
  try { await reg.add({ name: 'x' }) } catch (e) { err = e }
  t.ok(err, 'requires version + treeHash')

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

test('latest — newest timestamp wins', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  await reg.add(fakeEntry({ version: '1.0.0', publishedAt: '2026-01-01T00:00:00.000Z' }))
  await reg.add(fakeEntry({ version: '2.0.0', publishedAt: '2026-04-01T00:00:00.000Z' }))
  await reg.add(fakeEntry({ version: '1.1.0', publishedAt: '2026-02-01T00:00:00.000Z' }))

  const latest = await reg.latest('pear-git-v3')
  t.is(latest.version, '2.0.0', 'picks entry with newest publishedAt')

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

test('latest — missing app returns null', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()
  const latest = await reg.latest('nonexistent')
  t.is(latest, null)
  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// ── list ────────────────────────────────────────────────────

test('list — all entries', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  await reg.add(fakeEntry({ name: 'a', version: '1.0.0' }))
  await reg.add(fakeEntry({ name: 'a', version: '2.0.0' }))
  await reg.add(fakeEntry({ name: 'b', version: '1.0.0' }))

  const all = await reg.list()
  t.is(all.length, 3)

  const onlyA = await reg.list('a')
  t.is(onlyA.length, 2)
  t.ok(onlyA.every(e => e.name === 'a'))

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// ── lookupByTree ────────────────────────────────────────────

test('lookupByTree — resolves treeHash → release entry', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  const entry = fakeEntry({ treeHash: '1'.repeat(64) })
  await reg.add(entry)

  const found = await reg.lookupByTree('1'.repeat(64))
  t.ok(found)
  t.is(found.name, 'pear-git-v3')
  t.is(found.version, '1.0.0')

  const missing = await reg.lookupByTree('0'.repeat(64))
  t.is(missing, null)

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// ── remove ──────────────────────────────────────────────────

test('remove — deletes entry and fixes up latest pointer', async (t) => {
  const { store, dir } = mktempStore()
  const reg = new ReleaseRegistry(store)
  await reg.ready()

  await reg.add(fakeEntry({ version: '1.0.0', publishedAt: '2026-01-01T00:00:00.000Z' }))
  await reg.add(fakeEntry({ version: '2.0.0', publishedAt: '2026-02-01T00:00:00.000Z' }))

  t.is((await reg.latest('pear-git-v3')).version, '2.0.0')

  await reg.remove('pear-git-v3', '2.0.0')
  t.is(await reg.get('pear-git-v3', '2.0.0'), null, 'entry gone')
  t.is((await reg.latest('pear-git-v3')).version, '1.0.0', 'latest rolled back')

  await reg.remove('pear-git-v3', '1.0.0')
  t.is(await reg.latest('pear-git-v3'), null, 'no latest when all versions removed')

  await reg.close()
  await store.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// ── signing / verification ──────────────────────────────────

test('canonicalBytes — stable across key insertion order', (t) => {
  const e1 = fakeEntry({ name: 'x' })
  const e2 = {
    // same fields, different insertion order
    upgrade: e1.upgrade,
    name: 'x',
    version: e1.version,
    treeHash: e1.treeHash,
    serverKey: e1.serverKey,
    deps: e1.deps,
    publishedAt: e1.publishedAt,
    publisherDevicePubkey: e1.publisherDevicePubkey
  }
  t.is(canonicalBytes(e1).toString('hex'), canonicalBytes(e2).toString('hex'))
})

test('canonicalBytes — ignores sig field', (t) => {
  const e = fakeEntry()
  const withSig = { ...e, sig: 'decafbad'.repeat(16) }
  t.is(canonicalBytes(e).toString('hex'), canonicalBytes(withSig).toString('hex'))
})

test('sign + verify — valid signature accepts', (t) => {
  const kp = crypto.keyPair()
  const entry = fakeEntry()
  const signed = signEntry(entry, kp.secretKey)
  t.ok(signed.sig)
  t.is(signed.sig.length, 128, 'hex 64 bytes')
  t.ok(verifyEntry(signed, kp.publicKey))
})

test('verify — tampered entry rejects', (t) => {
  const kp = crypto.keyPair()
  const entry = fakeEntry()
  const signed = signEntry(entry, kp.secretKey)
  const tampered = { ...signed, treeHash: 'x'.repeat(64) }
  t.absent(verifyEntry(tampered, kp.publicKey))
})

test('verify — wrong key rejects', (t) => {
  const kp1 = crypto.keyPair()
  const kp2 = crypto.keyPair()
  const signed = signEntry(fakeEntry(), kp1.secretKey)
  t.absent(verifyEntry(signed, kp2.publicKey))
})

// ── Replication surrogate ───────────────────────────────────

test('registry core key persists — re-opening same store surfaces same entries', async (t) => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reg-persist-'))
  const store1 = new Corestore(dir)
  const reg1 = new ReleaseRegistry(store1)
  await reg1.ready()
  const keyHex = reg1.keyHex
  await reg1.add(fakeEntry({ version: '9.9.9' }))
  await reg1.close()
  await store1.close()

  const store2 = new Corestore(dir)
  const reg2 = new ReleaseRegistry(store2)
  await reg2.ready()
  t.is(reg2.keyHex, keyHex, 'same core key across opens')
  const back = await reg2.get('pear-git-v3', '9.9.9')
  t.ok(back, 'entry persisted to disk')
  await reg2.close()
  await store2.close()
  fs.rmSync(dir, { recursive: true, force: true })
})