OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / blob-refs.test.mjs

162 lines · javascript ref:
// Referenced-blob convergence — spec/convergence.md §6.
//
// A node can hold a byte-perfect, fully verified record set and still render no images: blobs live
// OUTSIDE the MST. Nothing checked that until 0.3.44.
//
// The walk is GENERIC on purpose, and that is the property most worth pinning. A hardcoded list of known
// paths (profile.avatar, embed.images[].image, …) is wrong the day a lexicon adds a field, and wrong
// SILENTLY — a blob nobody enumerates is a blob nobody notices is missing.
//
// Run: node test/blob-refs.test.mjs
import { collectBlobRefs, collectRepoBlobRefs, diffBlobs } from '../lib/blob-refs.js'

const assert = (cond, msg) => { if (!cond) throw new Error('FAIL: ' + msg); console.log('  ok -', msg) }
const CID1 = 'bafkreiabcdefghijklmnopqrstuvwxyz234567abcdefghijklmnop'
const CID2 = 'bafkreizyxwvutsrqponmlkjihgfedcba765432zyxwvutsrqponml'
const blob = (cid, mime = 'image/jpeg') => ({ $type: 'blob', ref: { $link: cid }, mimeType: mime, size: 1234 })

console.log('# the four ref shapes all normalise')
{
  const shapes = [
    { $type: 'blob', ref: { $link: CID1 }, mimeType: 'image/png' },        // decoded {$link}
    { $type: 'blob', ref: { '/': CID1 }, mimeType: 'image/png' },          // dag-json
    { $type: 'blob', ref: CID1, mimeType: 'image/png' },                   // bare string
    { $type: 'blob', ref: { asCID: null, toString: () => CID1 }, mimeType: 'image/png' } // CID-ish
  ]
  shapes[3].ref.asCID = shapes[3].ref // a multiformats CID is its own .asCID
  for (const s of shapes) {
    const r = collectBlobRefs(s)
    assert(r.length === 1 && r[0].cid === CID1, 'ref shape normalised to a cid')
  }
}

console.log('\n# blobs are found wherever they sit — no path list')
{
  const profile = { $type: 'app.bsky.actor.profile', displayName: 'x', avatar: blob(CID1), banner: blob(CID2) }
  assert(collectBlobRefs(profile).length === 2, 'profile avatar + banner')

  const post = {
    $type: 'app.bsky.feed.post',
    text: 'hi',
    embed: { $type: 'app.bsky.embed.images', images: [{ alt: 'a', image: blob(CID1) }, { alt: 'b', image: blob(CID2) }] }
  }
  assert(collectBlobRefs(post).length === 2, 'both embedded images')

  const rwm = {
    $type: 'app.bsky.feed.post',
    embed: { $type: 'app.bsky.embed.recordWithMedia', media: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } }
  }
  assert(collectBlobRefs(rwm).length === 1, 'the media side of a quote-with-media')

  // ⛔ THE POINT: a field no one has written an extractor for is still found.
  const future = { $type: 'app.bsky.feed.post', embed: { $type: 'com.example.notyetinvented', deeply: { nested: { thing: blob(CID1) } } } }
  assert(collectBlobRefs(future).length === 1, 'a blob in a lexicon that does not exist yet is STILL found')
}

console.log('\n# the legacy (pre-lexicon) blob form is taken too')
{
  const legacy = { $type: 'app.bsky.actor.profile', avatar: { cid: CID1, mimeType: 'image/jpeg' } }
  assert(collectBlobRefs(legacy).length === 1, 'a legacy {cid,mimeType} blob counts — wj62 predates the cutover')
}

console.log('\n# not-a-blob is not taken')
{
  const post = {
    $type: 'app.bsky.feed.post',
    text: 'a reply',
    reply: { parent: { uri: 'at://x/y/z', cid: CID1 }, root: { uri: 'at://x/y/z', cid: CID2 } }
  }
  // A strongRef also has a `cid`, but no mimeType — treating it as a blob would invent gaps for every
  // reply in the repo.
  assert(collectBlobRefs(post).length === 0, 'a strongRef (cid without mimeType) is NOT a blob')
  assert(collectBlobRefs({ text: 'plain' }).length === 0, 'a record with no blobs yields none')
  assert(collectBlobRefs(null).length === 0, 'null is safe')
}

console.log('\n# the same blob in two records is ONE blob')
{
  const records = [
    { collection: 'app.bsky.feed.post', rkey: 'a', record: { embed: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } } },
    { collection: 'app.bsky.feed.post', rkey: 'b', record: { embed: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } } }
  ]
  const m = collectRepoBlobRefs(records)
  assert(m.size === 1, 'content-addressed: one CID, one entry')
  assert(m.get(CID1).refs.length === 2, 'but both referring records are named')
}

console.log('\n# ⛔ ACCEPTANCE (#32): an absent avatar is reported, naming the CID')
{
  const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { $type: 'app.bsky.actor.profile', avatar: blob(CID1), banner: blob(CID2) } }]
  const d = await diffBlobs({ records, hasBlob: async (cid) => cid !== CID1 }) // avatar bytes absent
  assert(d.referenced === 2, 'two blobs referenced')
  assert(d.present === 1 && d.missing.length === 1, 'one present, one missing')
  assert(d.missing[0].cid === CID1, 'the MISSING one is named by CID')
  assert(d.missing[0].refs[0].collection === 'app.bsky.actor.profile', 'and the record that wants it is named')
  assert(/avatar/.test(d.missing[0].refs[0].where), 'down to the FIELD — "' + d.missing[0].refs[0].where + '"')
}

console.log('\n# a fully-present repo reports no gap')
{
  const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
  const d = await diffBlobs({ records, hasBlob: async () => true })
  assert(d.referenced === 1 && d.missing.length === 0, 'nothing missing')
}

console.log('\n# ⛔ with no inventory, orphans are NOT claimed to be checked')
{
  const d = await diffBlobs({ records: [], hasBlob: async () => true })
  assert(d.orphansChecked === false, '"we did not look" is reported as such')
  assert(d.orphans.length === 0 && d.inventory === null, 'and NOT as "there are none" — different answers')
  assert(/listBlobs/.test(d.orphanNote), 'names what it would take — the class that lost the 2026-07-03 image')
}

console.log('\n# ⛔ ACCEPTANCE (#32 slice 2): an ORPHAN is reported as orphaned, not as missing')
{
  const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
  // The authority holds TWO blobs; only CID1 is referenced by any record. CID2 is an orphan — invisible
  // to any walk of records, which is the whole point of asking the authority.
  const d = await diffBlobs({
    records,
    hasBlob: async () => true,
    listAuthorityBlobs: async () => ({ cids: [CID1, CID2] })
  })
  assert(d.orphansChecked === true, 'the inventory was consulted')
  assert(d.inventory === 2 && d.referenced === 1, 'authority holds 2, records reference 1')
  assert(d.orphans.length === 1 && d.orphans[0].cid === CID2, 'the ORPHAN is named — and it is CID2')
  assert(d.missing.length === 0, 'and it is NOT reported as missing — we have every blob we need')
}

console.log('\n# an inventory that fails degrades to unchecked, never to zero')
{
  const d = await diffBlobs({ records: [], hasBlob: async () => true, listAuthorityBlobs: async () => { throw new Error('501') } })
  assert(d.orphansChecked === false, 'a throwing inventory reads as unchecked')
  const d2 = await diffBlobs({ records: [], hasBlob: async () => true, listAuthorityBlobs: async () => null })
  assert(d2.orphansChecked === false, 'so does a null one — an old peer must not look converged')
}

console.log('\n# a throwing hasBlob counts as absent, never as a crash')
{
  const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
  const d = await diffBlobs({ records, hasBlob: async () => { throw new Error('disk gone') } })
  assert(d.missing.length === 1, 'an unreadable store reads as missing, not as healthy')
}

console.log('\nall blob-refs assertions passed')

// ---- content-address verification -------------------------------------------------------------------
import { verifyBlobCid } from '../lib/blob-refs.js'
import { cidForBlobSync } from 'atproto-repo/lib/cid.js'

console.log('\n# ⛔ fetched blob bytes are verified against their CID before being written')
{
  const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
  const realCid = String(cidForBlobSync(bytes))
  assert(await verifyBlobCid(bytes, realCid) === true, 'the right bytes under the right CID verify')
  assert(await verifyBlobCid(new Uint8Array([9, 9, 9]), realCid) === false, 'WRONG bytes under that CID are refused — a bad server cannot poison the store')
  assert(await verifyBlobCid(bytes, 'bafkreinotacid') === false, 'a mismatched CID is refused')
  assert(await verifyBlobCid(new Uint8Array(0), realCid) === false, 'empty bytes are refused')
  assert(await verifyBlobCid(bytes, null) === false, 'a missing CID is refused')
}

console.log('\nall blob verification assertions passed')