// 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`)