OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / repair-strongrefs.test.mjs

105 lines · javascript ref:
// repair-strongrefs — rewriting records whose strongRef lost its cid.
// Run: node test/repair-strongrefs.test.mjs
//
// The defect is silent by construction: a like with `subject:{uri}` and no cid indexes perfectly here
// and fails lexicon validation at the relay, so it federates as NOTHING while the UI shows it working.
// The assertions that matter are the refusals — never invent a cid, never partially repair, never
// mint a new rkey (which would double-count the subject).
import { brokenRefs, repairRecord, repairStrongRefs, STRONGREF_FIELDS } from '../lib/repair-strongrefs.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }

const URI = 'at://did:plc:someone00000000000000000/app.bsky.feed.post/3kabc'
const URI2 = 'at://did:plc:other0000000000000000000/app.bsky.feed.post/3kdef'
const like = (subject) => ({ $type: 'app.bsky.feed.like', subject, createdAt: '2026-07-29T23:39:11Z' })
const reply = (parent, root) => ({ $type: 'app.bsky.feed.post', text: 'hi', createdAt: 'x', reply: { parent, root } })

// A store shim: resolves any uri whose did is in `known`.
const storeFor = (known) => async (did) => (known.includes(did)
  ? { getRaw: async () => ({ bytes: new Uint8Array([1, 2, 3]), record: {} }) }
  : null)

// ── detection ────────────────────────────────────────────────────────────────────────────────────
ok(brokenRefs('app.bsky.feed.like', like({ uri: URI })).length === 1, 'a subject with no cid is broken')
ok(brokenRefs('app.bsky.feed.like', like({ uri: URI, cid: 'bafy' })).length === 0, 'a subject WITH a cid is fine')
ok(brokenRefs('app.bsky.feed.post', reply({ uri: URI }, { uri: URI2 })).length === 2,
  'a reply carries TWO strongRefs (parent AND root) and both count')
ok(brokenRefs('app.bsky.feed.post', { $type: 'app.bsky.feed.post', text: 'top-level' }).length === 0,
  'a non-reply post has no strongRef to break')
ok(brokenRefs('app.bsky.actor.profile', { displayName: 'x' }).length === 0, 'collections without strongRefs are ignored')
ok(brokenRefs('app.bsky.feed.like', like({ uri: 'not-an-at-uri' })).length === 0,
  'a malformed uri is not treated as a repairable ref — we could never resolve it')
ok(Object.keys(STRONGREF_FIELDS).length === 3, 'like, repost and post are the strongRef-carrying collections')

// ── repair: content-preserving ───────────────────────────────────────────────────────────────────
{
  const rec = like({ uri: URI })
  const fixed = await repairRecord('app.bsky.feed.like', rec, storeFor(['did:plc:someone00000000000000000']))
  ok(fixed.subject.uri === URI, 'the subject URI is UNCHANGED — only the missing cid is added')
  ok(!!fixed.subject.cid, 'and the cid is now present')
  ok(rec.subject.cid === undefined, 'the original record is not mutated in place')
  ok(fixed.createdAt === rec.createdAt, 'every other field is preserved')
}

// ── ⛔ the refusals ──────────────────────────────────────────────────────────────────────────────
{
  const fixed = await repairRecord('app.bsky.feed.like', like({ uri: URI }), storeFor([]))
  ok(fixed === null, 'an unresolvable subject is REFUSED — never rewrite with a guessed or placeholder cid')
}
{
  // parent resolvable, root not. A half-repaired reply is STILL invalid, so it must be left alone.
  const fixed = await repairRecord('app.bsky.feed.post', reply({ uri: URI }, { uri: URI2 }), storeFor(['did:plc:someone00000000000000000']))
  ok(fixed === null, 'a PARTIAL repair is refused — one fixed ref and one broken is still an invalid record')
}

// ── the sweep ────────────────────────────────────────────────────────────────────────────────────
const sweepFixture = () => ({
  'app.bsky.feed.like': [
    { rkey: 'good', record: like({ uri: URI, cid: 'bafyalready' }) },
    { rkey: 'bad1', record: like({ uri: URI }) },
    { rkey: 'bad2', record: like({ uri: URI2 }) } // URI2's did is NOT known -> unresolvable
  ],
  'app.bsky.feed.repost': [],
  'app.bsky.feed.post': []
})

{
  const fx = sweepFixture()
  const writes = []
  const s = await repairStrongRefs({
    listRecords: async (c) => fx[c],
    applyWrite: async (op) => { writes.push(op) },
    getRecordStore: storeFor(['did:plc:someone00000000000000000']),
    write: true
  })
  ok(s.checked === 3 && s.broken === 2, 'counts what it checked and what was broken')
  ok(s.repaired === 1 && s.unresolvable === 1, 'repairs what it can, reports what it cannot')
  ok(writes.length === 1 && writes[0].rkey === 'bad1', 'only the resolvable one is written')
  ok(writes[0].action === 'update' && writes[0].rkey === 'bad1',
    'written as an UPDATE with the SAME rkey — a new rkey would be a second like and double-count the subject')
  ok(!!writes[0].record.subject.cid, 'and the written record carries the cid')
}
{
  const fx = sweepFixture()
  const writes = []
  const s = await repairStrongRefs({
    listRecords: async (c) => fx[c],
    applyWrite: async (op) => { writes.push(op) },
    getRecordStore: storeFor(['did:plc:someone00000000000000000'])
  }) // no write
  ok(writes.length === 0, 'DRY RUN by default — nothing is written')
  ok(s.entries.some((e) => e.state === 'would-repair'), 'but it reports what it would do')
}
{
  // A collection that fails to list must not abort the sweep.
  const s = await repairStrongRefs({
    listRecords: async (c) => { if (c === 'app.bsky.feed.like') throw new Error('boom'); return [] },
    applyWrite: async () => {},
    getRecordStore: storeFor([])
  })
  ok(s.checked === 0, 'a failed collection is skipped, not fatal')
}

console.log(`\nREPAIR-STRONGREFS OK: ${pass} assertions`)