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