OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / constellation.test.mjs

161 lines · javascript ref:
// constellation — the historical backfill that closes spacedust's reconnect gap.
// Run: node test/constellation.test.mjs
//
// doFetch is injected, so pagination, the cursor walk, the truncation bound and fail-soft behaviour are
// tested with no network. The assertion that matters most is that a Constellation pointer is SHAPE-
// IDENTICAL to a spacedust one — if they diverge, the drain silently grows two code paths.
import { createConstellationClient, rowToPointer, DEFAULT_ENDPOINT } from '../lib/constellation.js'
import { SOURCES } from '../lib/spacedust.js'
import { pointerKey, shouldSkipRev } from '../lib/link-drain.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }

const SUBJECT = 'at://did:plc:wj62/app.bsky.feed.post/3kxyz'
const jsonRes = (body) => ({ ok: true, json: async () => body })
const row = (n) => ({ did: `did:plc:r${n}`, collection: 'app.bsky.feed.post', rkey: `rk${n}` })

// ── row -> pointer ───────────────────────────────────────────────────────────────────────────────
{
  const p = rowToPointer(row(1), { subject: SUBJECT, source: SOURCES.REPLY_PARENT })
  ok(p.did === 'did:plc:r1' && p.rkey === 'rk1', 'a row decomposes into a locator')
  ok(p.subject === SUBJECT && p.source === SOURCES.REPLY_PARENT, 'query context is filled in — the row itself carries neither')
  ok(p.contentBearing === true, 'tier is derived from the source, identically to spacedust')
  ok(p.origin === 'backfill', 'origin distinguishes a backfilled pointer from a live one')
  ok(p.rev === null, 'Constellation does not report the rev that produced a link')
  ok(shouldSkipRev(p.rev, '3lzzz') === false,
    'and a null rev must NOT skip — we re-fetch and let verification decide, rather than skipping on absent data')
}
{
  ok(rowToPointer(null, { subject: SUBJECT, source: SOURCES.LIKE }) === null, 'a null row is null')
  ok(rowToPointer({ did: 'x', collection: 'y' }, { subject: SUBJECT, source: SOURCES.LIKE }) === null, 'an incomplete row is null')
  ok(rowToPointer({ did: '', collection: 'y', rkey: 'z' }, { subject: SUBJECT, source: SOURCES.LIKE }) === null, 'empty fields are refused')
  ok(rowToPointer(row(1), { subject: SUBJECT, source: SOURCES.LIKE }).contentBearing === false,
    'a LIKE backfills as Tier C — the drain will skip it without a fetch, exactly as for a live like')
}

// ── SHAPE PARITY with spacedust — the unification claim ──────────────────────────────────────────
{
  const fromConstellation = rowToPointer(row(1), { subject: SUBJECT, source: SOURCES.REPLY_PARENT })
  const fromSpacedust = {
    operation: 'create', source: SOURCES.REPLY_PARENT, subject: SUBJECT,
    did: 'did:plc:r1', collection: 'app.bsky.feed.post', rkey: 'rk1',
    rev: '3lbbb', contentBearing: true, origin: 'live'
  }
  const keys = (o) => Object.keys(o).sort().join(',')
  ok(keys(fromConstellation) === keys(fromSpacedust),
    'both sources produce the SAME pointer keys — the drain must never need to know where a pointer came from')
  ok(pointerKey(fromConstellation) === pointerKey(fromSpacedust),
    'and they dedup against each other, so a backfill after reconnect does not re-retain what the live stream already handled')
}

// ── getBacklinks ─────────────────────────────────────────────────────────────────────────────────
{
  let url = null
  const c = createConstellationClient({ doFetch: async (u) => { url = u; return jsonRes({ total: 2, records: [row(1), row(2)] }) } })
  const res = await c.getBacklinks(SUBJECT, SOURCES.REPLY_PARENT)
  ok(res.pointers.length === 2 && res.total === 2, 'a page of records maps to pointers')
  ok(res.cursor === null, 'no cursor means the last page')
  ok(url.includes('/xrpc/blue.microcosm.links.getBacklinks'), 'we call the XRPC lexicon path, not the deprecated /links/* legacy API')
  ok(decodeURIComponent(url).includes(SUBJECT) && decodeURIComponent(url).includes(SOURCES.REPLY_PARENT),
    'subject and source are the required params')
}
{
  let url = null
  const c = createConstellationClient({ doFetch: async (u) => { url = u; return jsonRes({ records: [] }) } })
  await c.getBacklinks(SUBJECT, SOURCES.LIKE, { limit: 5000 })
  ok(new URL(url).searchParams.get('limit') === '100', 'limit is clamped to the lexicon maximum of 100')
  await c.getBacklinks(SUBJECT, SOURCES.LIKE, { limit: 0 })
  ok(new URL(url).searchParams.get('limit') === '1', 'and to a minimum of 1')
}
{
  const c = createConstellationClient({ doFetch: async () => jsonRes({ records: [row(1), null, { did: 'x' }] }) })
  const res = await c.getBacklinks(SUBJECT, SOURCES.REPLY_PARENT)
  ok(res.pointers.length === 1, 'malformed rows inside a good page are dropped, not fatal')
}
{
  const c = createConstellationClient({ doFetch: async () => ({ ok: false }) })
  ok((await c.getBacklinks(SUBJECT, SOURCES.LIKE)) === null, 'a non-ok response is null')
  const c2 = createConstellationClient({ doFetch: async () => { throw new Error('offline') } })
  ok((await c2.getBacklinks(SUBJECT, SOURCES.LIKE)) === null, 'a thrown fetch is null — a gap we cannot close stays open, it does not crash the drain')
  const c3 = createConstellationClient({ doFetch: async () => jsonRes({ nonsense: true }) })
  ok((await c3.getBacklinks(SUBJECT, SOURCES.LIKE)) === null, 'a response with no records array is null')
  const c4 = createConstellationClient({ doFetch: async () => jsonRes({ records: [] }) })
  ok((await c4.getBacklinks('', SOURCES.LIKE)) === null, 'a missing subject queries nothing')
}

// ── counts: the Tier-C pressure valve ────────────────────────────────────────────────────────────
{
  let url = null
  const c = createConstellationClient({ doFetch: async (u) => { url = u; return jsonRes({ total: 50123 }) } })
  ok((await c.countBacklinks(SUBJECT, SOURCES.LIKE)) === 50123,
    'a 50k-like post is ONE count query — this is precisely why §7.6 never retains likes')
  ok(url.includes('getBacklinksCount'), 'via the count lexicon')
  ok((await createConstellationClient({ doFetch: async () => jsonRes(42) }).countBacklinks(SUBJECT, SOURCES.LIKE)) === 42,
    'a bare numeric body is accepted too — the legacy count endpoint returns plain text')
  ok((await createConstellationClient({ doFetch: async () => jsonRes({}) }).countBacklinks(SUBJECT, SOURCES.LIKE)) === null,
    'an unrecognised count body is null rather than a guessed zero')
}

// ── distinct DIDs ────────────────────────────────────────────────────────────────────────────────
{
  const c = createConstellationClient({ doFetch: async () => jsonRes({ dids: ['did:plc:a', 'did:plc:b'] }) })
  ok((await c.getDistinctDids(SUBJECT, SOURCES.LIKE)).dids.length === 2, 'distinct DIDs come back as a list')
  const c2 = createConstellationClient({ doFetch: async () => jsonRes({ records: [{ did: 'did:plc:a' }] }) })
  ok((await c2.getDistinctDids(SUBJECT, SOURCES.LIKE)).dids[0] === 'did:plc:a', 'a records-shaped body is tolerated')
}

// ── pagination + the truncation bound ────────────────────────────────────────────────────────────
{
  const pages = [
    { records: [row(1)], cursor: 'c1' },
    { records: [row(2)], cursor: 'c2' },
    { records: [row(3)] }
  ]
  let i = 0
  const seen = []
  const c = createConstellationClient({ doFetch: async (u) => { seen.push(new URL(u).searchParams.get('cursor')); return jsonRes(pages[i++]) } })
  const out = []
  for await (const p of c.walkBacklinks(SUBJECT, SOURCES.REPLY_PARENT)) out.push(p)
  ok(out.length === 3, 'the walk follows the cursor to the last page')
  ok(seen[0] === null && seen[1] === 'c1' && seen[2] === 'c2', 'and threads each cursor through')
}
{
  // A subject with endless backlinks must not turn a gap-fill into an unbounded crawl.
  let truncated = null
  const c = createConstellationClient({ doFetch: async () => jsonRes({ records: [row(1)], cursor: 'always-more' }) })
  const out = []
  for await (const p of c.walkBacklinks(SUBJECT, SOURCES.REPLY_PARENT, { maxPages: 3, onTruncate: (t) => { truncated = t } })) out.push(p)
  ok(out.length === 3, 'the walk is bounded by maxPages')
  ok(truncated !== null && truncated.maxPages === 3,
    'and truncation is REPORTED — a silent cap reads as completeness you do not have')
}
{
  const c = createConstellationClient({ doFetch: async () => ({ ok: false }) })
  const out = []
  for await (const p of c.walkBacklinks(SUBJECT, SOURCES.REPLY_PARENT)) out.push(p)
  ok(out.length === 0, 'a failed page ends the walk rather than looping')
}

// ── backfillInto: the reconnect gap-fill ─────────────────────────────────────────────────────────
{
  const c = createConstellationClient({ doFetch: async () => jsonRes({ records: [row(1), row(2)] }) })
  const queued = []
  const { offered, duplicates } = await c.backfillInto((p) => queued.push(p), {
    subjects: [SUBJECT, 'at://did:plc:wj62/app.bsky.feed.post/other'],
    sources: [SOURCES.REPLY_PARENT, SOURCES.QUOTE]
  })
  ok(offered === 2 && queued.length === 2,
    'the same record offered under SEVERAL sources is enqueued ONCE — a reply links via both reply.parent and reply.root, and backfilled pointers carry rev:null so nothing downstream would dedup them')
  ok(duplicates === 6, 'and the collapsed duplicates are reported, so a deduped sweep is distinguishable from an empty one')
  ok(queued.every((p) => p.origin === 'backfill'), 'and everything it offers is marked as backfill, so a reconnect is visible in the stats')
}
{
  const c = createConstellationClient({ doFetch: async () => jsonRes({ records: [] }) })
  ok((await c.backfillInto(() => {}, { subjects: [], sources: [] })).offered === 0, 'nothing to backfill offers nothing')
}

ok(DEFAULT_ENDPOINT.startsWith('https://'), 'the default instance is https')

console.log(`\nCONSTELLATION OK: ${pass} assertions`)