OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / test / membrane-reactor.test.js

87 lines · javascript ref:
// 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')
})