// 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')
})