/**
* DeletedReleases — the fold that stops a puller chasing retired versions.
* See lib/services/deleted-releases.js for the 2026-08-29 incident this
* exists to prevent.
*/
import test from 'brittle'
import {
DeletedReleases, foldDeleted,
OP_PUBLISHED, OP_PROMOTED, OP_DELETED
} from '../lib/services/deleted-releases.js'
test('a deleted version is tombstoned; an untouched one is not', (t) => {
const s = new DeletedReleases()
s.applyEntry({ op: OP_PUBLISHED, repo: 'r', version: 'v1' }, 0)
s.applyEntry({ op: OP_PUBLISHED, repo: 'r', version: 'v2' }, 1)
s.applyEntry({ op: OP_DELETED, repo: 'r', version: 'v1' }, 2)
t.ok(s.has('r', 'v1'), 'deleted version is tombstoned')
t.absent(s.has('r', 'v2'), 'live version is not')
t.is(s.size, 1)
t.alike(s.list(), [{ repo: 'r', version: 'v1' }])
})
test('versions are scoped per repo — same version string, different repo', (t) => {
const s = new DeletedReleases()
s.applyEntry({ op: OP_DELETED, repo: 'a', version: 'v1' }, 0)
t.ok(s.has('a', 'v1'))
t.absent(s.has('b', 'v1'), 'a delete in repo a says nothing about repo b')
})
test('later seq wins, and an out-of-order replay cannot clobber newer state', (t) => {
const s = new DeletedReleases()
s.applyEntry({ op: OP_DELETED, repo: 'r', version: 'v1' }, 5)
t.ok(s.has('r', 'v1'))
// An older entry arriving late must not un-delete it.
s.applyEntry({ op: OP_PUBLISHED, repo: 'r', version: 'v1' }, 2)
t.ok(s.has('r', 'v1'), 'stale replay ignored')
// A genuine republish at a NEWER seq revives it — mistakes are reversible.
s.applyEntry({ op: OP_PUBLISHED, repo: 'r', version: 'v1' }, 9)
t.absent(s.has('r', 'v1'), 'republish revives the version')
})
test('channel-only ops carry no version and are ignored', (t) => {
const s = new DeletedReleases()
s.applyEntry({ op: OP_DELETED, repo: 'r', version: 'v1' }, 0)
s.applyEntry({ op: OP_PROMOTED, repo: 'r', version: '', channel: 'stable' }, 1)
t.ok(s.has('r', 'v1'), 'unaffected by a channel op')
t.is(s.size, 1)
})
test('foldDeleted replays a whole log; isDeleted is a bare predicate', async (t) => {
const log = {
async * entries () {
yield { op: OP_PUBLISHED, repo: 'r', version: 'v1' }
yield { op: OP_PUBLISHED, repo: 'r', version: 'v2' }
yield { op: OP_DELETED, repo: 'r', version: 'v1' }
}
}
const s = await foldDeleted(log)
t.is(s.size, 1)
const pred = s.isDeleted
t.ok(pred('r', 'v1'), 'predicate works detached from the instance')
t.absent(pred('r', 'v2'))
})
test('foldDeleted tolerates a missing or malformed log', async (t) => {
t.is((await foldDeleted(null)).size, 0)
t.is((await foldDeleted({})).size, 0, 'no entries() — returns an empty set, does not throw')
})