OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / dedup-reclaim.test.js

129 lines · javascript ref:
/**
 * EvictionManager.reclaimSuperseded — single-relay dedup of stale app versions.
 * Durability-critical: dry-run by default, supersession decided by VERSION +
 * SAME PUBLISHER (never the byAppId index, never across publishers), never
 * reclaims a sacred (archive/custody/lease) entry, bounds a wedged unseed, and
 * uses the proven unseed → tombstone → purge teardown.
 */

import test from 'brittle'
import { EvictionManager } from 'p2p-hiverelay/core/relay-node/eviction.js'

const A = 'a'.repeat(64) // older
const B = 'b'.repeat(64) // newer / current
const C = 'c'.repeat(64)
const D = 'd'.repeat(64)
const E = 'e'.repeat(64)
const F = 'f'.repeat(64)
const PUB = '1'.repeat(64)
const PUB2 = '2'.repeat(64)

function harness (entries, bytes = {}, opts = {}) {
  const reg = {
    apps: new Map(entries),
    get (k) { return this.apps.get(k) },
    evictedCalls: [],
    markEvicted (k) { this.evictedCalls.push(k) }
  }
  const unseeds = []
  const purges = []
  const deps = {
    appRegistry: reg,
    seedingRegistry: {},
    storageAccounting: { getBytes: (k) => (k in bytes ? bytes[k] : null) },
    diskMonitor: {},
    getReplicationHealth: () => new Map(),
    myPubkeyHex: 'relay',
    unseed: opts.unseed || (async (k) => { unseeds.push(k) }),
    purgeDrive: async (k) => { purges.push(k) }
  }
  const cfg = opts.unseedTimeoutMs ? { unseedTimeoutMs: opts.unseedTimeoutMs } : {}
  return { ev: new EvictionManager(deps, cfg), reg, unseeds, purges }
}

const app = (appId, version, extra = {}) => ({ type: 'app', appId, version, publisherPubkey: PUB, ...extra })

test('dry-run (default) reports superseded but deletes nothing', async (t) => {
  const { ev, unseeds, purges, reg } = harness(
    [[A, app('cool', '1.0.0')], [B, app('cool', '2.0.0')]], { [A]: 500, [B]: 600 })
  const r = await ev.reclaimSuperseded()
  t.is(r.dryRun, true)
  t.alike(r.reclaimed.map(x => x.appKey), [A])
  t.is(r.freedBytes, 500)
  t.alike(unseeds, [])
  t.alike(purges, [])
  t.alike(reg.evictedCalls, [])
})

test('execute reclaims the superseded version via unseed → tombstone → purge', async (t) => {
  const { ev, unseeds, purges, reg } = harness(
    [[A, app('cool', '1.0.0')], [B, app('cool', '2.0.0')]], { [A]: 500, [B]: 600 })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.alike(r.reclaimed.map(x => x.appKey), [A])
  t.is(r.freedBytes, 500)
  t.alike(unseeds, [A])
  t.alike(purges, [A])
  t.alike(reg.evictedCalls, [A])
  t.absent(unseeds.includes(B), 'current version never touched')
})

test('DATA-LOSS REGRESSION: older version inserted last is reclaimed, current v2 is NOT', async (t) => {
  const { ev, unseeds } = harness(
    [[B, app('cool', '2.0.0')], [A, app('cool', '1.0.0')]], { [A]: 500, [B]: 600 })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.alike(r.reclaimed.map(x => x.appKey), [A], 'only the older v1 reclaimed')
  t.absent(unseeds.includes(B), 'live v2 never deleted despite being inserted first')
})

test('COLLISION: same appId, different publisher → nothing reclaimed', async (t) => {
  const { ev, unseeds } = harness(
    [[A, { type: 'app', appId: 'cool', version: '1.0.0', publisherPubkey: PUB }],
      [B, { type: 'app', appId: 'cool', version: '2.0.0', publisherPubkey: PUB2 }]],
    { [A]: 500, [B]: 600 })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.is(r.reclaimed.length, 0, 'unrelated drives colliding on appId are never cross-reclaimed')
  t.alike(unseeds, [])
})

test('NEVER reclaims a sacred superseded entry (archive / custody / lease)', async (t) => {
  const now = Date.now()
  const { ev, unseeds, purges } = harness([
    [C, app('arch', '1.0.0', { durability: 1 })], [D, app('arch', '2.0.0')],
    [E, app('cust', '1.0.0', { custodyIntentId: 'i1' })], [F, app('cust', '2.0.0')],
    [A, app('lease', '1.0.0', { leaseManaged: true, retainUntil: now + 1e6 })], [B, app('lease', '2.0.0')]
  ], { [C]: 1, [E]: 1, [A]: 1 })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.is(r.reclaimed.length, 0)
  t.alike(unseeds, [])
  t.alike(purges, [])
  t.alike(r.skipped.map(s => s.reason).sort(), ['ARCHIVE_TIER', 'CUSTODY_BOUND', 'LEASE_BOUND'])
})

test('a wedged unseed is bounded — entry skipped, NOT purged or tombstoned', async (t) => {
  const { ev, purges, reg } = harness(
    [[A, app('cool', '1.0.0')], [B, app('cool', '2.0.0')]], { [A]: 500 },
    { unseedTimeoutMs: 50, unseed: () => new Promise(() => {}) /* never resolves */ })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.is(r.reclaimed.length, 0)
  t.alike(r.skipped, [{ appKey: A, reason: 'unseed-timeout' }])
  t.alike(purges, [], 'purge never runs after unseed timeout')
  t.alike(reg.evictedCalls, [], 'tombstone never written after unseed timeout')
})

test('retainVersions keeps the N newest superseded versions', async (t) => {
  const { ev, unseeds } = harness([
    [A, app('cool', '1.0.0')], [C, app('cool', '2.0.0')], [B, app('cool', '3.0.0')]
  ], { [A]: 100, [C]: 200, [B]: 300 })
  const r = await ev.reclaimSuperseded({ dryRun: false, retainVersions: 1 })
  t.alike(r.reclaimed.map(x => x.appKey), [A], 'keep newest superseded (v2/C), reclaim older (v1/A)')
  t.alike(unseeds, [A])
})

test('no superseded versions → nothing reclaimed', async (t) => {
  const { ev } = harness(
    [[A, app('one', '1.0.0')], [B, app('two', '1.0.0')]], { [A]: 100, [B]: 200 })
  const r = await ev.reclaimSuperseded({ dryRun: false })
  t.is(r.reclaimed.length, 0)
  t.is(r.freedBytes, 0)
})