/**
* store-gc RPC — release retention runs inside the sidecar (it holds the
* registry; the full release list overflows the UDS frame for a CLI).
* 2026-10-11: the 14-day age gate kept 75 hyper-tps .pck copies (~44 GiB).
*/
import test from 'brittle'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import { Client } from 'hyper-svc'
import { Sidecar } from '../lib/index.js'
function mktemp (prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix + '-'))
}
const h = (c) => c.repeat(64)
test('store-gc keeps the newest 2 releases per app and sweeps older ones, fresh or not', async (t) => {
const root = mktemp('sidecar-root')
const storeRoot = mktemp('sidecar-store')
const sidecar = new Sidecar({
root,
socketPath: path.join(root, 'svc.sock'),
storeRoot,
registryStore: mktemp('sidecar-registry'),
nameRegistryStore: mktemp('sidecar-names'),
releaseRegistryReplicasDir: mktemp('sidecar-replicas'),
enableSeed: false,
passphrase: 'test-passphrase'
})
await sidecar.start()
const client = await Client.connectUds(path.join(root, 'svc.sock'))
// Three releases of one app; every tree pulled just now (the age gate would keep all).
// v1 = root a + dep b; v2 = root c + dep b (shared); v3 = root d + prebuild e.
for (const c of ['a', 'b', 'c', 'd', 'e']) {
fs.mkdirSync(sidecar.treeStore.treePath(h(c)), { recursive: true })
sidecar.treeStore._writeMeta(h(c), { pulledAt: Date.now(), schema: 2 })
}
const rel = (version, publishedAt, treeHash, deps) =>
client.call('publish-release', { entry: { name: 'app', version, publishedAt, treeHash, deps } })
await rel('1', '2026-10-01T00:00:00.000Z', h('a'), [{ name: 'x', version: '1', tree: h('b') }])
await rel('2', '2026-10-02T00:00:00.000Z', h('c'), [{ name: 'x', version: '1', tree: h('b') }])
await rel('3', '2026-10-03T00:00:00.000Z', h('d'), [{ name: 'y', version: '1', tree: null, prebuilds: { 'linux-x64': h('e') } }])
const dry = await client.call('store-gc', { dryRun: true, keepReleases: 2 })
t.alike(dry.candidates, [h('a')], 'dry run: only v1\'s own root is a candidate (b is shared with v2)')
t.ok(sidecar.treeStore.hasTree(h('a')), 'dry run deletes nothing')
const real = await client.call('store-gc', { keepReleases: 2 })
t.alike(real.removed, [h('a')], 'v1 root removed')
for (const c of ['b', 'c', 'd', 'e']) t.ok(sidecar.treeStore.hasTree(h(c)), 'kept ' + c)
await client.close()
await sidecar.stop()
})