// Membrane feed-registry reactor (community-ux.md §111): front each registry feed's home DID + fire bsky
// requestCrawl exactly once per new DID. Pure, injected stubs — no swarm, no bsky. Run via brittle-node.
import test from 'brittle'
import { makeMembraneReactor } from '../lib/membrane-reactor.js'
const A = 'did:plc:aaaa2222222222222222222222'
const B = 'did:plc:bbbb2222222222222222222222'
const entry = (did, rkey) => ({ did, rkey, name: rkey, curator: 'roster', params: {}, roomKey: 'f'.repeat(64) })
const tick = () => new Promise((r) => setImmediate(r))
function stubs ({ crawl = true } = {}) {
const calls = { add: [], front: [], crawl: [] }
return {
calls,
addFront: (did) => calls.add.push(did),
front: (did) => { calls.front.push(did); return Promise.resolve() },
requestCrawl: crawl ? ((did) => { calls.crawl.push(did); return Promise.resolve() }) : null,
log: () => {}
}
}
test('add-feed → front the home DID + requestCrawl once per new DID', async (t) => {
const s = stubs()
const react = makeMembraneReactor(s)
await react([entry(A, 'loser-club'), entry(B, 'pets')])
await tick()
t.alike(s.calls.add.sort(), [A, B].sort(), 'both home DIDs added to the front allow-set')
t.alike(s.calls.front.sort(), [A, B].sort(), 'both home DIDs fronted')
t.alike(s.calls.crawl.sort(), [A, B].sort(), 'requestCrawl fired once per DID')
})
test('re-fire (registry re-emits full set) → no duplicate front or crawl', async (t) => {
const s = stubs()
const react = makeMembraneReactor(s)
await react([entry(A, 'loser-club')])
await react([entry(A, 'loser-club')]) // watch re-emits the same entry
await tick()
t.is(s.calls.add.length, 1, 'addFront called once (idempotent per DID)')
t.is(s.calls.front.length, 1, 'front called once')
t.is(s.calls.crawl.length, 1, 'requestCrawl fired exactly once despite the re-fire')
})
test('a new DID appearing in a later fire is fronted + crawled', async (t) => {
const s = stubs()
const react = makeMembraneReactor(s)
await react([entry(A, 'loser-club')])
await react([entry(A, 'loser-club'), entry(B, 'pets')]) // B published later
await tick()
t.alike(s.calls.crawl.sort(), [A, B].sort(), 'the later DID is crawled once; A not re-crawled')
t.is(react.crawled().length, 2, 'both DIDs recorded as crawled')
})
test('requestCrawl OFF (null) → still fronts, never crawls (inert until configured)', async (t) => {
const s = stubs({ crawl: false })
const react = makeMembraneReactor(s)
await react([entry(A, 'loser-club')])
await tick()
t.alike(s.calls.front, [A], 'fronts the DID')
t.is(react.crawled().length, 0, 'no requestCrawl when the crawl relay is not configured')
})
test('requestCrawl failure → un-marked so a later reconcile retries', async (t) => {
const calls = { crawl: [] }
let failNext = true
const react = makeMembraneReactor({
addFront: () => {},
front: () => Promise.resolve(),
requestCrawl: (did) => { calls.crawl.push(did); if (failNext) { failNext = false; return Promise.reject(new Error('relay 502')) } return Promise.resolve() },
log: () => {}
})
await react([entry(A, 'loser-club')])
await tick()
t.is(react.crawled().length, 0, 'a failed crawl is un-marked (not counted as done)')
await react([entry(A, 'loser-club')]) // retry
await tick()
t.is(calls.crawl.length, 2, 'requestCrawl retried on the next reconcile')
t.is(react.crawled().length, 1, 'the retry succeeded → now marked crawled')
})
test('entries without a did are skipped', async (t) => {
const s = stubs()
const react = makeMembraneReactor(s)
await react([{ rkey: 'no-did' }, entry(A, 'ok')])
await tick()
t.alike(s.calls.front, [A], 'only the entry with a did is fronted')
})