OverviewFilesCommitsIssuesDocsReleasesPeers

favela-community / test / community-room.test.mjs

136 lines · javascript ref:
// favela-community — the co-owned community room, both halves:
//   READ  : a KEYLESS reader (favela-web) derives the roster from the replicated plaintext view (no key).
//   WRITE : a MOD, admitted by the owner (addMod → writer), co-manages membership + moderation, and its
//           ops linearize through the owner (sole indexer) — the write layer.
// Bare-native (Autobase/Hyperbee need a disk path). Run: bare test/community-room.test.mjs
import Corestore from 'corestore'
import { CommunityRoom, CommunityRooms } from '../index.js'

const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m) }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const stamp = Date.now()
const A = 'did:plc:alice2222222222222222222', B = 'did:plc:bob222222222222222222222'
const C = 'did:plc:carol22222222222222222222', D = 'did:plc:dave2222222222222222222222'

const ownerStore = new Corestore('data/cc-owner-' + stamp)
const readerStore = new Corestore('data/cc-reader-' + stamp)
const modStore = new Corestore('data/cc-mod-' + stamp)

// pipe two bases' replication streams together (in-process stand-in for the swarm)
function wire (a, b) {
  const s1 = a.base.replicate(true); const s2 = b.base.replicate(false)
  s1.on('error', () => {}); s2.on('error', () => {})
  s1.pipe(s2).pipe(s1)
  return () => { try { s1.destroy() } catch {} try { s2.destroy() } catch {} }
}
async function until (fn, ms = 15000) { const end = Date.now() + ms; while (Date.now() < end) { if (await fn()) return true; await sleep(100) } return false }

// ── OWNER: create + manage ──
const owner = await CommunityRoom.create(ownerStore, { name: 'Test Community' })
const ROOM = owner.key
assert(/^[0-9a-f]{64}$/.test(ROOM), 'fresh room has a 64-hex public bootstrap key')
assert(owner.writable, 'creator is a writer (genesis indexer)')
for (const did of [A, B, C]) await owner.addMember(did)
await owner.ban(B)
await owner.update()
let om = await owner.members()
assert(om.length === 2 && om.includes(A) && om.includes(C) && !om.includes(B), 'owner roster = {A,C}, B banned')

// ── KEYLESS READER ──
const reader = await CommunityRoom.openReadonly(readerStore, ROOM)
assert(!reader.writable, 'keyless reader is NOT writable (holds no key)')
const unwireR = wire(owner, reader)
assert(await until(async () => (await reader.members()).length === 2), 'KEYLESS READ: reader derives roster {A,C} from replication')
const meta = await reader.meta()
assert(meta.name === 'Test Community', 'keyless reader reads meta.name')
let threw = false
try { await reader.addMember(D) } catch { threw = true }
assert(threw, 'keyless reader REFUSES to write (addMember throws — favela-web is keyless)')

// ── MOD: admitted by the owner, co-writes (the write layer) ──
const mod = await CommunityRoom.open(modStore, ROOM)
assert(!mod.writable, 'mod is not writable before admission')
const unwireM = wire(owner, mod)
// owner admits the mod as a plain WRITER (non-indexer — the safe topology; owner stays sole indexer N=1)
await owner.addMod(mod.localKey, { userId: 'mod-b' })
assert(await mod.waitWritable({ timeout: 15000 }), 'mod becomes writable after owner.addMod (admission)')

const mods = await owner.mods()
const modRec = mods.find((w) => w.userId === 'mod-b')
assert(modRec && modRec.role === 'writer', 'addMod default role = writer (non-indexer → no N=2 quorum trap)')

// mod writes; its ops linearize through the owner (sole indexer, ackInterval) and reach the owner + reader
await mod.addMember(D)
await mod.ban(A)
assert(await until(async () => { const m = await owner.members(); return m.includes(D) && !m.includes(A) }), 'WRITE LAYER: owner sees the mod\'s add(D) + ban(A) — co-writer ops linearize through the owner')
assert(await until(async () => { const m = await reader.members(); return m.includes(D) && !m.includes(A) }), 'the keyless reader also converges on the mod\'s writes')
const final = (await owner.members()).sort()
assert(final.length === 2 && final.includes(C) && final.includes(D), 'final roster = {C,D} (A banned by mod, B banned by owner)')

// ── COMMUNITY DID linkage: the room binds its co-owned atproto DID (meta:did), keyless-readable ──
const DID_COMM = 'did:plc:community222222222222222'
const didStore = new Corestore('data/cc-did-' + stamp)
const didOwner = await CommunityRoom.create(didStore, { name: 'DID Community', did: DID_COMM })
assert((await didOwner.communityDid()) === DID_COMM, 'community DID set at create is readable via communityDid()')
const didReaderStore = new Corestore('data/cc-didr-' + stamp)
const didReader = await CommunityRoom.openReadonly(didReaderStore, didOwner.key)
const unwireD = wire(didOwner, didReader)
assert(await until(async () => (await didReader.communityDid()) === DID_COMM), 'a KEYLESS reader replicates + reads the community DID (meta:did)')
let didThrew = false
try { await didReader.setDid('did:plc:evil22222222222222222222') } catch { didThrew = true }
assert(didThrew, 'keyless reader cannot setDid (write-guarded)')
unwireD(); await didOwner.close(); await didReader.close()
try { await didStore.close(); await didReaderStore.close() } catch {}

// ── ROSTER WATCHER: a member added AFTER watch starts reaches the consumer with no re-open ──
// This is favela-web's path: index members added to a room LATER without a restart (DESIGN §7). We drive
// replication by hand (no swarm) and assert the watcher fires a baseline AND a delta.
const watchStore = new Corestore('data/cc-watch-' + stamp)
const crooms = new CommunityRooms(watchStore, null) // no swarm — wire replication manually below
const seen = []
const unwatch = await crooms.watch(ROOM, (dids) => { seen.push(dids.slice().sort()) }, { debounceMs: 50 })
const watchReader = await crooms.room(ROOM) // same replica watch() opened
const unwireW = wire(owner, watchReader)
// current roster at this point = {C, D} (A + B banned above)
assert(await until(() => seen.length > 0 && seen[seen.length - 1].includes(D) && seen[seen.length - 1].includes(C)), 'watch BASELINE: consumer sees the current roster {C,D} after replication')
const E = 'did:plc:erin2222222222222222222222'
await owner.addMember(E)
assert(await until(() => seen[seen.length - 1].includes(E)), 'watch DELTA: a member (E) added AFTER watch started reaches the consumer with NO re-open')
await unwatch()
unwireW()

// ── CURATION CONTRACT: {curator,params} — HOW the feed selects posts (distinct from WHO=roster) ──
// bsky-neutral (only affects which post URIs a feed returns) + keyless-readable + backward-compatible.
const curStore = new Corestore('data/cc-cur-' + stamp)
const curOwner = await CommunityRoom.create(curStore, { name: 'Cur Community' })
const cur0 = await curOwner.curation()
assert(cur0.curator === 'roster' && typeof cur0.params === 'object', 'DEFAULT curation = roster (pre-curation rooms keep working, no migration)')
await curOwner.setCuration('subject', { tag: 'p2p' })
const cur1 = await curOwner.curation()
assert(cur1.curator === 'subject' && cur1.params.tag === 'p2p', 'setCuration binds {curator:subject, params:{tag}} — mutable contract')
let curThrew = false
try { await curOwner.setCuration('', {}) } catch { curThrew = true }
assert(curThrew, 'setCuration rejects an empty curator name')
let curParamThrew = false
try { await curOwner.setCuration('rules', [1, 2]) } catch { curParamThrew = true }
assert(curParamThrew, 'setCuration rejects non-object params')
// a KEYLESS reader replicates + reads the curation contract (no key)
const curReaderStore = new Corestore('data/cc-curr-' + stamp)
const curReader = await CommunityRoom.openReadonly(curReaderStore, curOwner.key)
const unwireC = wire(curOwner, curReader)
assert(await until(async () => (await curReader.curation()).curator === 'subject'), 'KEYLESS READ: reader replicates the curation contract {curator:subject}')
let curRThrew = false
try { await curReader.setCuration('roster', {}) } catch { curRThrew = true }
assert(curRThrew, 'keyless reader cannot setCuration (write-guarded)')
// custom curator name accepted with NO schema change (architecture.md escape-hatch)
await curOwner.setCuration('my-custom-algo', { weight: 3 })
assert((await curOwner.curation()).curator === 'my-custom-algo', 'custom curator id accepted (schema is curator-agnostic)')
unwireC(); await curOwner.close(); await curReader.close()
try { await curStore.close(); await curReaderStore.close() } catch {}

console.log('\nFAVELA-COMMUNITY OK (Bare-native): keyless read + mod admission + co-writer membership/moderation linearize through the owner + roster watcher (baseline + delta) + curation contract (default/set/keyless-read/custom)')
unwireR(); unwireM()
await owner.close(); await reader.close(); await mod.close()
try { await crooms.close() } catch {}
try { await ownerStore.close(); await readerStore.close(); await modStore.close(); await watchStore.close() } catch {}