// The "Published to pgh" list must not advertise installs that cannot work.
//
// The registry and the tree store have OPPOSITE lifecycle rules:
//
// - the tree store is content-addressed and garbage-collected; a tree
// survives only while something references it, and PublishedIndex keeps
// maxVersions = 1, so old versions are swept by `pgh store gc`.
// - the ReleaseRegistry is an append-only signed autobase with NO retention.
//
// They therefore diverge by construction. Measured on tabertron 2026-08-29 for
// hyper-tps: the registry advertised 382 distinct tree hashes, of which 10 were
// present in the local store and 372 were not. Every one of those 372 rows
// printed a confident `pgh app install pgh://<key>/hyper-tps@<version>` that
// resolves to nothing here.
//
// Marking is deliberately chosen over pruning: the registry is signed and
// replicated, so removing entries is a far heavier action, and "not held HERE"
// is not the same as "gone" -- another peer may still hold the tree. The label
// has to say the former, never the latter.
import test from 'brittle'
import { markHeldLocally } from '../lib/handlers/pgh-tree.js'
test('entries are marked by whether their tree is held locally', (t) => {
const entries = [
{ name: 'app', version: '1.0.0', treeHash: 'aa'.repeat(32) },
{ name: 'app', version: '0.9.0', treeHash: 'bb'.repeat(32) },
{ name: 'app', version: '0.8.0', treeHash: 'cc'.repeat(32) }
]
const held = new Set([ 'aa'.repeat(32) ])
const out = markHeldLocally(entries, (h) => held.has(h))
t.is(out.length, 3, 'nothing is dropped -- the registry is reported as it is')
t.ok(out[0].heldLocally, 'present tree marked held')
t.absent(out[1].heldLocally, 'absent tree marked NOT held')
t.absent(out[2].heldLocally)
})
test('an entry with no tree hash is not claimed to be held', (t) => {
const out = markHeldLocally([{ name: 'app', version: '1.0.0' }], () => true)
t.absent(out[0].heldLocally, 'no hash means we cannot assert it is resolvable')
})
test('a throwing probe degrades to "unknown", never to a false claim', (t) => {
// A stat can fail for reasons that say nothing about the tree (permissions,
// a store that has not been created yet). Reporting "held" there would put
// an install command on screen that does not work.
const out = markHeldLocally(
[{ name: 'app', version: '1.0.0', treeHash: 'dd'.repeat(32) }],
() => { throw new Error('EACCES') }
)
t.absent(out[0].heldLocally, 'probe failure is not treated as held')
})
test('marking is pure -- the input entries are not mutated', (t) => {
const entries = [{ name: 'app', version: '1.0.0', treeHash: 'ee'.repeat(32) }]
markHeldLocally(entries, () => true)
t.absent('heldLocally' in entries[0], 'caller keeps its own objects intact')
})
test('an empty or missing list is handled', (t) => {
t.alike(markHeldLocally([], () => true), [])
t.alike(markHeldLocally(null, () => true), [])
t.alike(markHeldLocally(undefined, () => true), [])
})
// ── Page rendering ───────────────────────────────────────────
import { renderRepoReleases } from '../lib/web/pages/repo-releases.js'
function svcWith (entries) {
return {
_releases: { async list () { return { versions: [], channels: {} } } },
async dispatch (name) {
if (name === 'tree-registry-list') {
return { registryKey: 'k'.repeat(64), count: entries.length, entries }
}
return {}
}
}
}
test('the page marks absent trees and refuses to print an install for them', async (t) => {
// (svc, ctx, repoName, opts) -- the order the router actually uses.
const html = await renderRepoReleases(svcWith([
{ name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true },
{ name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false }
]), {}, 'demo', { showAll: true })
t.ok(html.includes('not stored here'), 'the absent row is labelled')
t.ok(html.includes('tree-absent'), 'and dimmed via its own class')
t.ok(html.includes('<strong>1</strong> of 2'), 'summary counts it (copy is not the contract)')
// The held version keeps a usable command; the absent one must not have one.
t.ok(html.includes('pgh app install pgh://' + 'k'.repeat(64) + '/demo@2.0.0'),
'held version still offers an install')
t.absent(html.includes('/demo@1.0.0'),
'absent version does NOT advertise an install that cannot resolve')
t.ok(html.includes('unavailable from this forge'), 'it says so instead')
})
test('a fully-held list shows no summary and no markers', async (t) => {
const html = await renderRepoReleases(svcWith([
{ name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true }
]), {}, 'demo', {})
t.absent(html.includes('not stored here'), 'no marker')
t.absent(html.includes('not stored on this forge'), 'no summary noise when everything resolves')
t.ok(html.includes('pgh app install'), 'install still offered')
})
// ── Hidden by default ────────────────────────────────────────
//
// 377 of 384 hyper-tps rows were entries this forge cannot serve. Rendering
// them by default buried the 7 usable ones and shipped ~280KB of markup to say
// so. They are OMITTED now, not merely dimmed, with a link to see them.
test('not-stored entries are omitted by default', async (t) => {
const html = await renderRepoReleases(svcWith([
{ name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true },
{ name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false },
{ name: 'demo', version: '0.9.0', treeHash: 'cc'.repeat(32), publishedAt: 0, heldLocally: false }
]), {}, 'demo', {})
t.ok(html.includes('demo@2.0.0'), 'the servable version is shown')
t.absent(html.includes('0.9.0'), 'a not-stored version is not rendered at all')
t.absent(html.includes('1.0.0'), 'nor the other one')
t.ok(html.includes('?all=1'), 'but there is a way to see them')
t.ok(/2 (older )?entr/i.test(html) || html.includes('>2<'), 'and the count is stated')
})
test('showAll renders them, marked', async (t) => {
const html = await renderRepoReleases(svcWith([
{ name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true },
{ name: 'demo', version: '1.0.0', treeHash: 'bb'.repeat(32), publishedAt: 1, heldLocally: false }
]), {}, 'demo', { showAll: true })
t.ok(html.includes('1.0.0'), 'the not-stored version appears')
t.ok(html.includes('not stored here'), 'still marked')
t.absent(html.includes('/demo@1.0.0'), 'and still offers no install')
})
test('when everything is servable there is no hidden-count line', async (t) => {
const html = await renderRepoReleases(svcWith([
{ name: 'demo', version: '2.0.0', treeHash: 'aa'.repeat(32), publishedAt: 2, heldLocally: true }
]), {}, 'demo', {})
t.absent(html.includes('?all=1'), 'no toggle offered when nothing is hidden')
})