OverviewFilesCommitsIssuesDocsReleasesPeers

favela-bridge / test / plan-edge.test.js

95 lines · javascript ref:
// Projecting an edge — a like, a repost, a reply.
//
// ⛔ THE SIGNED SET IS FROZEN (interaction.md §5.4). These are the three whose subject is a RECORD;
// follow and block name an ACTOR, and on atproto an actor is a DID, which a favela feed key is not.
//
// ⛔ EVERY REFUSAL IS NAMED. §5.5 says "every verb states its loss" and calls out `projectLike`
// returning null as a silence the design forbids — so a withheld edge carries a reason a person can
// read, not an absence they have to infer.

import test from 'brittle'
import { planEdge, LIKE_COLLECTION, REPOST_COLLECTION, POST_COLLECTION } from '../lib/plan-edge.js'
import { tidFor } from '../lib/tid.js'

const OURS = 'a'.repeat(64)
const THEIRS = 'b'.repeat(64)
const SUBJECT = { uri: 'at://did:plc:me/app.bsky.feed.post/3mv', cid: 'bafy123' }
const ctx = (extra = {}) => ({ ownFeed: OURS, subject: () => SUBJECT, ...extra })
const edge = (collection, extra = {}) => ({
  name: '1788822724600',
  collection,
  target: { feed: OURS, collection: 'post', name: '1788800000000' },
  createdAt: '2026-09-07T23:12:04.600Z',
  ...extra
})

test('★★ a like becomes app.bsky.feed.like, under a TID rkey', (t) => {
  const p = planEdge(edge('reaction', { value: 'like' }), ctx())
  t.ok(p.project)
  t.is(p.collection, LIKE_COLLECTION)
  t.alike(p.record.subject, SUBJECT, 'subject carries uri AND cid — the lexicon requires both')
  t.is(p.record.createdAt, '2026-09-07T23:12:04.600Z', 'the edge\'s own time, not now')
  t.is(p.rkey, tidFor('1788822724600'), 'derived from the edge name, so a re-run reproduces it')
})

test('★★★ a non-like reaction is withheld, never sent as a like', (t) => {
  // ⛔ bsky has exactly one reaction. Sending a 🎉 as a like would publish a different act under
  // somebody's name.
  const p = planEdge(edge('reaction', { value: '🎉' }), ctx())
  t.absent(p.project)
  t.ok(p.losses.some((l) => /no reaction other than like/.test(l)), p.losses.join('; '))
})

test('★★ a repost becomes app.bsky.feed.repost', (t) => {
  const p = planEdge(edge('repost'), ctx())
  t.ok(p.project)
  t.is(p.collection, REPOST_COLLECTION)
  t.alike(p.record.subject, SUBJECT)
})

test('★★★ a reply is a post carrying root and parent', (t) => {
  const p = planEdge(edge('reply', { body: 'well said' }), ctx())
  t.ok(p.project)
  t.is(p.collection, POST_COLLECTION)
  t.is(p.record.text, 'well said')
  t.alike(p.record.reply.parent, SUBJECT)
  // ⚠ With no deeper chain the root IS the parent — a top-level reply, which is the common case and
  // not an error.
  t.alike(p.record.reply.root, SUBJECT, 'root falls back to the parent')
})

test('★★★ a reply to a reply carries the thread ROOT, not the parent twice', (t) => {
  const ROOT = { uri: 'at://did:plc:me/app.bsky.feed.post/3root', cid: 'bafyroot' }
  const p = planEdge(edge('reply', { body: 'and another thing' }), ctx({ root: () => ROOT }))
  t.alike(p.record.reply.root, ROOT, 'the walked-up root')
  t.alike(p.record.reply.parent, SUBJECT, 'and the immediate parent')
})

test('★★★ a subject on someone else\'s feed is withheld, and says why', (t) => {
  // ⛔ Naming a foreign subject means choosing how it is stored, permanently (interaction.md §3).
  // Nothing here forces that decision, and a person should see that their like did not travel.
  const p = planEdge(edge('reaction', { value: 'like', target: { feed: THEIRS, collection: 'post', name: 'x' } }), ctx())
  t.absent(p.project)
  t.ok(p.losses.some((l) => /another feed/.test(l)), p.losses.join('; '))
})

test('★★★ an unresolvable subject is an ordinary wait, not a failure', (t) => {
  // An edge projected before the post it points at — the next pass finds it.
  const p = planEdge(edge('reaction', { value: 'like' }), ctx({ subject: () => null }))
  t.absent(p.project)
  t.ok(p.losses.some((l) => /not in the atproto repo yet/.test(l)), p.losses.join('; '))
})

test('★★★ a reply in a room that reaches nothing is withheld by the lattice', (t) => {
  // ⛔ The reach lattice answers for the VERB. A members room reaches nothing but favela, and an
  // interaction written there must not escape because the identity has bsky switched on.
  const p = planEdge(edge('reply', { body: 'secret' }), ctx({ roomType: 'members' }))
  t.absent(p.project)
  t.ok(p.losses.length > 0, 'and says so: ' + p.losses.join('; '))
})

test('★★ a follow is not projected here — an actor is not a record', (t) => {
  const p = planEdge(edge('follow'), ctx())
  t.absent(p.project)
})