OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / repair-pointers.test.js

110 lines · javascript ref:
// repair-pointers — the operator repair for a converged pointer whose BYTES exist nowhere.
//
// The assertion this file exists for is the SAFETY one: a record that heals via the fetch must NEVER be
// tombstoned. A stale pointer is not evidence of unrecoverable content — if any peer holds the bytes,
// reconcileBytes gets them back intact, and deleting it instead would destroy data that was fine. That
// ordering (fetch first, judge second) cannot be proven against production without first causing the
// damage the tool repairs, which is exactly why it is proven here.
import test from 'brittle'
import { repairWithClient } from '../lib/repair-pointers-core.js'

const DID = 'did:plc:test0000000000000000000'
const entry = (key, state, over = {}) => ({ key, state, pointerOid: 'aaaa1111', localOid: state === 'stale' ? 'bbbb2222' : null, pointerSize: 390, ...over })

// A scripted client: pointer-diag answers are queued in order, and every call is recorded.
function fakeClient (diagSequence, { reconcile = { fetched: 0, failed: 0, more: false } } = {}) {
  const calls = []
  let i = 0
  return {
    calls,
    writes: () => calls.filter((c) => c.op === 'apply-write'),
    async call (op, params) {
      calls.push({ op, params })
      if (op === 'pointer-diag') return diagSequence[Math.min(i++, diagSequence.length - 1)]
      if (op === 'reconcile-bytes') return typeof reconcile === 'function' ? reconcile() : reconcile
      if (op === 'apply-write') return { ok: true }
      throw new Error('unexpected op ' + op)
    },
    async close () {}
  }
}

test('a clean store is reported clean and nothing is called beyond the diagnostic', async (t) => {
  const c = fakeClient([{ checked: 10, ok: 10, entries: [] }])
  const r = await repairWithClient(c, DID)
  t.is(r.clean, true, 'reported clean')
  t.is(c.writes().length, 0, 'no writes')
  t.absent(c.calls.some((x) => x.op === 'reconcile-bytes'), 'and no fetch attempted — nothing to repair')
})

test('⛔ a record that RECOVERS via the fetch is never tombstoned', async (t) => {
  // before: two unresolvable. after the fetch: both resolved. This is the case where tombstoning would
  // destroy content a peer still held — the failure mode that makes a naive "delete all stale" wrong.
  const c = fakeClient([
    { checked: 5, ok: 3, entries: [entry('app.bsky.feed.post/a', 'stale'), entry('app.bsky.feed.post/b', 'missing')] },
    { checked: 5, ok: 5, entries: [] }
  ], { reconcile: { fetched: 2, failed: 0, more: false } })

  const r = await repairWithClient(c, DID, { write: true }) // even with --write
  t.is(r.tombstoned, 0, 'NOTHING is tombstoned')
  t.is(c.writes().length, 0, 'no apply-write was issued at all')
  t.is(r.recovered, 2, 'both are reported as recovered by the fetch')
})

test('only what survives the fetch is tombstoned, and the fetch happens FIRST', async (t) => {
  const c = fakeClient([
    { checked: 5, ok: 3, entries: [entry('app.bsky.feed.post/gone', 'stale'), entry('app.bsky.feed.post/back', 'missing')] },
    { checked: 5, ok: 4, entries: [entry('app.bsky.feed.post/gone', 'stale')] }, // 'back' healed
    { checked: 4, ok: 4, entries: [] }
  ], { reconcile: { fetched: 1, failed: 1, more: false } })

  const r = await repairWithClient(c, DID, { write: true })
  t.is(r.tombstoned, 1, 'exactly one tombstone')
  const w = c.writes()
  t.is(w[0].params.rkey, 'gone', 'and it is the record that could NOT be recovered')
  t.is(w[0].params.action, 'delete', 'issued as a delete (a soft, converged tombstone)')

  const order = c.calls.map((x) => x.op)
  t.ok(order.indexOf('reconcile-bytes') < order.indexOf('apply-write'),
    'the fetch is attempted BEFORE any delete — that ordering is the whole safety argument')
  t.is(order[order.length - 1], 'pointer-diag', 'and it re-verifies afterwards rather than asserting success')
})

test('DRY RUN is the default: it reports what it would do and writes nothing', async (t) => {
  const c = fakeClient([
    { checked: 3, ok: 2, entries: [entry('app.bsky.feed.post/gone', 'stale')] },
    { checked: 3, ok: 2, entries: [entry('app.bsky.feed.post/gone', 'stale')] }
  ], { reconcile: { fetched: 0, failed: 1, more: false } })

  const r = await repairWithClient(c, DID) // no --write
  t.is(r.wouldTombstone, 1, 'reports what it would tombstone')
  t.is(c.writes().length, 0, 'and mutates nothing — mutation requires an explicit --write')
})

test('a legacy inline record (no-oid) is NEVER a candidate', async (t) => {
  // no-oid means the record sits INLINE in the log where a pointer belongs. Its content is present and
  // fine; the repair is migrate-inline-pointers. Tombstoning it would delete a healthy record.
  const c = fakeClient([{ checked: 4, ok: 3, entries: [entry('app.bsky.feed.post/inline', 'no-oid')] }])
  const r = await repairWithClient(c, DID, { write: true })
  t.is(r.clean, true, 'treated as clean by this tool')
  t.is(c.writes().length, 0, 'and never deleted — that is a different repair')
})

test('a bounded reconcile drain stops rather than spinning on `more`', async (t) => {
  let n = 0
  const c = fakeClient([
    { checked: 9, ok: 8, entries: [entry('app.bsky.feed.post/x', 'missing')] },
    { checked: 9, ok: 8, entries: [entry('app.bsky.feed.post/x', 'missing')] }
  ], { reconcile: () => { n++; return { fetched: 0, failed: 1, more: true } } }) // always claims more

  await repairWithClient(c, DID)
  t.ok(n <= 12, `drain is bounded (${n} passes) — a large backlog must not spin forever`)
})

test('a store with no record substrate is skipped, not repaired', async (t) => {
  const c = fakeClient([{ skipped: 'no-record-store' }])
  const r = await repairWithClient(c, DID, { write: true })
  t.is(r.skipped, 'no-record-store', 'reported as skipped')
  t.is(c.writes().length, 0, 'and nothing written')
})