// favela-web blob path — avatar + post images. Pure: stubs the hpp `_call`.
//
// The load-bearing assertion is the blob REF SHAPE. It is not a guess: it was read out of the fleet's
// live records by decoding wj62's app.bsky.actor.profile/self and its working image post. Both store
//
// { $type:'blob', ref:{ $link:'<cid string>' }, mimeType, size }
//
// with `ref` a PLAIN object — NOT a multiformats CID instance, and NOT a CBOR tag-42 link (verified
// from the raw bytes: no d82a, a literal "$link" text key). That is off-spec for DAG-CBOR, where a blob
// ref should be a CID link — but it is what the whole repo already contains and what bsky demonstrably
// hydrates into cdn.bsky.app thumb/fullsize URLs. Emitting canonical refs for NEW records only would
// make the repo internally inconsistent, and fixing it properly is a content migration of every
// existing record. So new writes MATCH the existing shape, deliberately, and this test pins it so the
// decision is explicit rather than accidental.
//
// Run: node test/blobs.test.mjs
import { Writer } from '../lib/writer.js'
const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) }
const mk = () => {
const w = new Writer({})
const calls = []
w._call = async (method, params) => {
calls.push({ method, params })
if (method === 'upload-blob') return { cid: 'bafkreitestcid000000000000000000000000000000000000000', mimeType: params.mimeType, size: 1234 }
return { ok: true }
}
return { w, calls }
}
// ── blobRef shape (the byte-level contract with the existing repo) ────────────────────────────────
{
const ref = Writer.blobRef({ cid: 'bafkreiabc', mimeType: 'image/jpeg', size: 42 })
assert(ref.$type === 'blob', 'blobRef is $type blob')
assert(ref.mimeType === 'image/jpeg' && ref.size === 42, 'carries mimeType + size')
assert(typeof ref.ref === 'object' && ref.ref !== null, 'ref is an object')
assert(ref.ref.$link === 'bafkreiabc', 'ref.$link is the cid STRING — matches every existing record on disk')
assert(ref.ref.constructor === Object, 'ref is a PLAIN object, not a CID instance (same as the live records)')
assert(JSON.stringify(Object.keys(ref).sort()) === JSON.stringify(['$type', 'mimeType', 'ref', 'size']), 'exactly the four blob-ref fields, no extras')
}
// ── uploadBlob relays bytes as hex over the UDS (matching serve-device-uds) ───────────────────────
{
const { w, calls } = mk()
const out = await w.uploadBlob(Buffer.from([0xff, 0xd8, 0xff, 0xe0]), 'image/jpeg')
assert(calls[0].method === 'upload-blob', 'calls upload-blob on the hpp')
assert(calls[0].params.bytes === 'ffd8ffe0', 'bytes travel as HEX (the c.json RPC value cannot carry raw bytes)')
assert(calls[0].params.mimeType === 'image/jpeg', 'mime is passed through')
assert(out.cid && out.size === 1234, 'returns cid + size for the ref')
}
// ── post() with images builds an app.bsky.embed.images embed ──────────────────────────────────────
{
const { w, calls } = mk()
const blob = Writer.blobRef({ cid: 'bafkreiimg', mimeType: 'image/png', size: 99 })
await w.post('look', { images: [{ blob, alt: 'a description' }] })
const rec = calls[0].params.record
assert(rec.embed && rec.embed.$type === 'app.bsky.embed.images', 'embed is app.bsky.embed.images')
assert(rec.embed.images.length === 1, 'one image')
assert(rec.embed.images[0].image.ref.$link === 'bafkreiimg', 'the blob ref is embedded verbatim')
assert(rec.embed.images[0].alt === 'a description', 'alt text carried')
assert(rec.text === 'look' && Array.isArray(rec.langs), 'text + langs still correct alongside the embed')
}
// bsky REJECTS more than 4 images outright — a record the relay refuses federates as nothing while
// looking fine locally, so the cap is enforced at write time, not left to the UI.
{
const { w, calls } = mk()
const blob = Writer.blobRef({ cid: 'bafkreix', mimeType: 'image/png', size: 1 })
await w.post('many', { images: [1, 2, 3, 4, 5, 6].map(() => ({ blob, alt: '' })) })
assert(calls[0].params.record.embed.images.length === 4, 'capped at 4 (app.bsky.embed.images maxLength)')
}
// no images -> NO embed key at all (an empty embed would be an invalid record)
{
const { w, calls } = mk()
await w.post('plain')
assert(!('embed' in calls[0].params.record), 'a text-only post carries no embed key')
const { w: w2, calls: c2 } = mk()
await w2.post('plain', { images: [] })
assert(!('embed' in c2[0].params.record), 'an EMPTY images array also yields no embed')
}
// ── alt text defaults to '' rather than undefined (undefined would drop the required field) ───────
{
const { w, calls } = mk()
const blob = Writer.blobRef({ cid: 'bafkreiy', mimeType: 'image/png', size: 1 })
await w.post('noalt', { images: [{ blob }] })
assert(calls[0].params.record.embed.images[0].alt === '', 'alt defaults to empty string, never undefined')
}
// ── putRecord relays an update through the hpp (the avatar path) ──────────────────────────────────
{
const { w, calls } = mk()
await w.putRecord('app.bsky.actor.profile', 'self', { $type: 'app.bsky.actor.profile', displayName: 'x' }, { action: 'update' })
assert(calls[0].method === 'apply-write', 'putRecord goes through apply-write')
assert(calls[0].params.action === 'update' && calls[0].params.rkey === 'self', 'action + rkey correct')
assert(calls[0].params.collection === 'app.bsky.actor.profile', 'collection correct')
}
console.log('\nBLOBS OK: blob-ref shape matches the live records; embeds capped at 4; avatar writes via apply-write')