// favela roster resolver (#2) — Bare-native, offline. A FAKE owner peer serves an app.bsky.graph.list
// + listitem records; assert parseListUri and resolveRoster (this-list-only, deduped subject DIDs).
// Run: `bare test/roster.test.mjs`
import '../lib/bare-globals.js'
import { parseListUri, resolveRoster, LIST_COLLECTION, LISTITEM_COLLECTION } from '../lib/roster.js'
const assert = (cond, msg) => { if (!cond) throw new Error('FAIL: ' + msg); console.log(' ok -', msg) }
const OWNER = 'did:plc:owner'
const URI = `at://${OWNER}/app.bsky.graph.list/3list`
const OTHER = `at://${OWNER}/app.bsky.graph.list/3other`
// --- parseListUri ---
const p = parseListUri(URI)
assert(p.owner === OWNER && p.rkey === '3list' && p.uri === URI, 'parseListUri extracts owner + rkey')
let threw = false
try { parseListUri(`at://${OWNER}/app.bsky.feed.post/1`) } catch { threw = true }
assert(threw, 'parseListUri rejects a non-list URI')
// --- resolveRoster against a fake owner peer (getRecord + listRecords -> { uri, cid, value }) ---
const li = (rkey, subject, list) => ({ uri: `at://${OWNER}/app.bsky.graph.listitem/${rkey}`, cid: 'x', value: { $type: LISTITEM_COLLECTION, subject, list, createdAt: 'x' } })
const ownerPeer = {
did: OWNER,
getRecord (coll, rkey) {
if (coll === LIST_COLLECTION && rkey === '3list') return { uri: URI, cid: 'x', value: { $type: LIST_COLLECTION, name: 'My Community', purpose: 'app.bsky.graph.defs#curatelist', createdAt: 'x' } }
return null
},
listRecords (coll) {
if (coll !== LISTITEM_COLLECTION) return []
return [
li('a', 'did:plc:alice', URI),
li('b', 'did:plc:bob', URI),
li('c', 'did:plc:carol', OTHER), // points at a DIFFERENT list -> excluded
li('d', 'did:plc:alice', URI) // duplicate subject -> deduped
]
}
}
const r = await resolveRoster(ownerPeer, URI)
assert(r.name === 'My Community', 'reads the list record metadata (name)')
assert(r.purpose === 'app.bsky.graph.defs#curatelist', 'reads the list purpose')
assert(r.members.length === 2, 'roster = 2 unique members from THIS list (other-list + duplicate excluded)')
assert(r.members.includes('did:plc:alice') && r.members.includes('did:plc:bob'), 'members are the listitem subjects')
assert(!r.members.includes('did:plc:carol'), 'a listitem pointing at a different list is excluded')
console.log('\nROSTER OK: parse list URI + resolve members (this-list only, deduped) from an owner peer')
// ---- the REAL DidPeer contract: { records, cursor }, not a bare array -------------------------------
//
// ⛔ WHY THIS EXISTS. The fake above returns a bare array, and so did DidPeer.listRecords — until
// atproto-did-peer 0.2.10 changed it to the lexicon's { records, cursor }. resolveRoster kept doing
// `for (const it of items)` over the wrapper and threw `items is not iterable`: community rosters
// stopped resolving entirely, in production, and every test here still passed because the fake had
// frozen the OLD contract. A fake that does not track the real thing is worse than no fake — it
// converts a live breakage into a green suite.
const wrapped = {
did: OWNER,
getRecord: ownerPeer.getRecord,
listRecords (coll) {
const arr = ownerPeer.listRecords(coll)
return { records: arr, cursor: undefined } // exactly what DidPeer 0.2.10+ returns
}
}
const rw = await resolveRoster(wrapped, URI)
assert(rw.members.length === 2, 'the lexicon shape { records, cursor } resolves the SAME roster (2 members)')
assert(rw.members.includes('did:plc:alice') && rw.members.includes('did:plc:bob'), 'same members from the wrapped shape')
assert(rw.name === 'My Community', 'and the list metadata still reads')
// A peer that returns nothing at all must degrade, not throw — this is the path a not-yet-replicated
// front takes, and it used to be a bare array so `|| []` covered it.
const emptyish = { did: OWNER, getRecord: ownerPeer.getRecord, listRecords: () => ({ records: [], cursor: undefined }) }
const re = await resolveRoster(emptyish, URI)
assert(re.members.length === 0, 'an empty { records } resolves to an empty roster rather than throwing')
console.log('\nROSTER SHAPE OK: both the legacy array and the lexicon { records, cursor } resolve identically')