OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / test / feed.test.js

234 lines · javascript ref:
// feed.js — the bridge's public face as a FEED, not a log of arrivals.
//
// WHY IT IS A PRODUCT SURFACE AND A DIAGNOSTIC AT ONCE. The existing /timeline shows only what the
// fediverse SENT us, as a flat list of activity types. That answers "did anything arrive" and cannot
// answer the question we actually keep getting wrong: are interactions being handled in BOTH
// directions? A like we received and a like we sent are the same shape on this page, so a missing
// one is visible rather than inferred from a log.
//
// ⛔ PUBLIC ONLY, AND THE FILTER IS THE SAFETY PROPERTY. There is no login in front of this. Mastodon
// delivers DMs to an inbox exactly as it delivers replies, through the same handler into the same
// store, so anything that cannot prove it was public is withheld — including everything stored
// before `public` was recorded.

import test from 'brittle'
import { buildFeed, handleOf } from '../lib/feed.js'

const REMOTE = 'https://mastodon.social/users/alice'
const OURS = 'https://p2p.7rnx.net/ap/objects/3kmine'

const note = (rkey, content, published) => ({
  id: `https://p2p.7rnx.net/ap/objects/${rkey}`,
  type: 'Note',
  content,
  published,
  attributedTo: 'https://p2p.7rnx.net/ap/actor'
})

const inbound = (over) => ({
  id: 'act-' + Math.random(), type: 'Create', actor: REMOTE, objectId: `${REMOTE}/notes/x`,
  content: '<p>nice post</p>', published: '2026-09-15T10:00:00Z', public: true,
  recipient: 'p2p', receivedAt: Date.parse('2026-09-15T10:00:00Z'), ...over
})

test('a remote actor URI reads as the handle a fediverse user recognises', (t) => {
  t.is(handleOf(REMOTE), '@alice@mastodon.social')
  t.is(handleOf('https://prsm.space/users/fleeky'), '@fleeky@prsm.space')
  t.is(handleOf('https://example.org/@bob'), '@bob@example.org', 'the /@name form too')
  t.is(handleOf('not a url'), 'not a url', 'and anything unparseable is shown as-is, never invented')
})

test('own posts and inbound activity land in ONE feed, newest first', (t) => {
  const rows = buildFeed({
    self: { handle: '@p2p@p2p.7rnx.net', name: 'Fleeky' },
    notes: [note('a', '<p>my post</p>', '2026-09-15T09:00:00Z')],
    inbound: [inbound({ published: '2026-09-15T11:00:00Z' })]
  })
  t.is(rows.length, 2)
  t.is(rows[0].at, '2026-09-15T11:00:00Z', 'newest first, like every feed anyone has used')
  t.is(rows[1].kind, 'post')
  t.is(rows[1].author.handle, '@p2p@p2p.7rnx.net', 'our own post is attributed to us, not to a URL')
  t.is(rows[1].text, 'my post', "and the remote server's markup is not ours to render")
})

test('★ every interaction verb is visible and labelled, which is the point of the page', (t) => {
  const rows = buildFeed({
    self: { handle: '@p2p@p2p.7rnx.net' },
    notes: [],
    inbound: [
      inbound({ type: 'Like', objectId: OURS, content: null, published: '2026-09-15T10:00:00Z' }),
      inbound({ type: 'Announce', objectId: OURS, content: null, published: '2026-09-15T10:01:00Z' }),
      inbound({ type: 'Follow', objectId: null, content: null, published: '2026-09-15T10:02:00Z' }),
      inbound({ type: 'Create', inReplyTo: OURS, published: '2026-09-15T10:03:00Z' })
    ]
  })
  t.alike(rows.map((r) => r.kind), ['reply', 'follow', 'boost', 'like'], 'all four, newest first')
  t.is(rows[3].verb, 'liked your post', 'a like says so in words')
  t.is(rows[2].verb, 'boosted your post')
  t.is(rows[1].verb, 'followed you')
  t.is(rows[0].kind, 'reply', 'a reply is content, not a bare verb')
  t.is(rows[0].text, 'nice post')
})

test('⛔ anything that cannot prove it was public is withheld', (t) => {
  const rows = buildFeed({
    self: {},
    notes: [],
    inbound: [
      inbound({ id: 'dm', public: false, content: '<p>secret</p>' }),
      inbound({ id: 'legacy', content: '<p>from before the field existed</p>' })
    ].map((i) => (i.id === 'legacy' ? (delete i.public, i) : i))
  })
  t.is(rows.length, 0, 'a DM and an unattributable item are both absent')
})

test('⛔ a row for somebody else never appears on this peer\'s feed', (t) => {
  const rows = buildFeed({
    self: {},
    recipient: 'ashatron',
    notes: [],
    inbound: [inbound({ recipient: 'p2p' }), inbound({ id: 'mine', recipient: 'ashatron' })]
  })
  t.is(rows.length, 1)
  t.is(rows[0].id, 'mine')
})

test('an unknown activity type is dropped, not shown as the nearest familiar one', (t) => {
  const rows = buildFeed({ self: {}, notes: [], inbound: [inbound({ type: 'Flag' })] })
  t.is(rows.length, 0, 'inventing a verb would tell the reader something that did not happen')
})

// ── the outbound half ─────────────────────────────────────────────────────────────────────────────
//
// ⛔ THE REASON THE PAGE IS WORTH LOOKING AT. Without these the feed shows our posts and other
// people's reactions to them, which proves INBOUND handling and says nothing about outbound. A like
// we sent and a like we received now sit in the same stream, so a broken direction is visible instead
// of being inferred from a delivery log.

const ownLike = (rkey, at, object = 'https://mastodon.social/notes/1') => ({
  kind: 'like', rkey, key: 'like:' + rkey, at,
  activity: { id: 'https://p2p.7rnx.net/ap/activities/like/' + rkey, type: 'Like', object }
})

test('★★ our own likes and boosts appear, attributed to us', (t) => {
  const rows = buildFeed({
    self: { handle: '@p2p@p2p.7rnx.net', name: 'Fleeky' },
    own: [
      ownLike('l1', '2026-09-15T12:00:00Z'),
      { kind: 'announce', rkey: 'b1', key: 'announce:b1', at: '2026-09-15T12:01:00Z', activity: { type: 'Announce', object: 'https://mastodon.social/notes/2' } }
    ]
  })
  t.alike(rows.map((r) => r.kind), ['boost', 'like'], 'newest first')
  t.is(rows[1].verb, 'you liked a post', 'in words, and in the first person — it was us')
  t.is(rows[0].verb, 'you boosted a post')
  t.is(rows[1].author.handle, '@p2p@p2p.7rnx.net', 'attributed to us, not to whoever we acted on')
  t.is(rows[1].source, 'own')
  t.is(rows[1].subject, 'https://mastodon.social/notes/1', 'and it links to what we acted on')
})

test('★ both directions interleave by time in ONE stream', (t) => {
  const rows = buildFeed({
    self: { handle: '@p2p@p2p.7rnx.net' },
    notes: [note('a', '<p>my post</p>', '2026-09-15T09:00:00Z')],
    own: [ownLike('l1', '2026-09-15T11:00:00Z')],
    inbound: [inbound({ type: 'Like', objectId: OURS, content: null, published: '2026-09-15T10:00:00Z' })]
  })
  t.alike(rows.map((r) => r.verb), ['you liked a post', 'liked your post', null],
    'ours, theirs, and a post — one timeline, which is the whole diagnostic')
})

test('⛔ an outbound interaction with no time still shows, sorted last', (t) => {
  const rows = buildFeed({ self: {}, own: [ownLike('l1', null)] })
  t.is(rows.length, 1, '"we do not know when" is not "it did not happen"')
})

// ── images ────────────────────────────────────────────────────────────────────────────────────────

test('a post carries its pictures through to the row', (t) => {
  const withImg = {
    ...note('img', '<p>look</p>', '2026-09-15T09:00:00Z'),
    attachment: [{ type: 'Document', mediaType: 'image/png', url: 'https://p2p.7rnx.net/ap/blobs/bafk1', name: 'a screenshot' }]
  }
  const rows = buildFeed({ self: {}, notes: [withImg] })
  t.is(rows[0].attachments.length, 1)
  t.is(rows[0].attachments[0].url, 'https://p2p.7rnx.net/ap/blobs/bafk1')
  t.is(rows[0].attachments[0].name, 'a screenshot', 'alt text travels — a picture with no description is unreadable to some readers')
})

test('★ an inbound reply keeps its pictures too', (t) => {
  const rows = buildFeed({
    self: {},
    inbound: [inbound({
      inReplyTo: OURS,
      attachment: [{ type: 'Document', mediaType: 'image/jpeg', url: 'https://mastodon.social/media/1.jpg', name: 'their photo' }]
    })]
  })
  t.is(rows[0].attachments.length, 1, 'a reply from the fediverse can have a picture in it')
  t.is(rows[0].attachments[0].mediaType, 'image/jpeg')
})

test('⛔ an attachment with no url is dropped, and the count is bounded', (t) => {
  const many = Array.from({ length: 20 }, (_, i) => ({ type: 'Document', mediaType: 'image/png', url: 'u' + i }))
  const rows = buildFeed({ self: {}, notes: [{ ...note('m', '<p>x</p>', 'z'), attachment: [{ type: 'Document' }, ...many] }] })
  t.absent(rows[0].attachments.some((a) => !a.url), 'nothing without a url')
  t.ok(rows[0].attachments.length <= 8, 'a remote server does not get to decide how tall our page is')
})

// ── recovering an unattributed item ───────────────────────────────────────────────────────────────
//
// ⛔ THE SCOPE HID THE ONE THING WE WANTED TO SEE. Items stored before `recipient` existed belong to
// nobody, which is right when nothing can say whose they were. But the OBJECT URI can: `/ap/objects/x`
// is the primary's flat path and `/ap/users/<name>/objects/x` is that user's. Reading it is recovering
// addressing that is still present, not guessing at addressing that was lost.

test('★★ an unattributed item is recovered from the object path when it names our host', (t) => {
  const rows = buildFeed({
    self: {}, recipient: 'p2p', host: 'ap.7rnx.net',
    inbound: [{ id: 'l', type: 'Like', actor: 'https://prsm.space/users/fleeky', objectId: 'https://ap.7rnx.net/ap/objects/3mvks7khp3u6w', public: false, published: '2026-09-14T00:00:00Z' }]
  })
  t.is(rows.length, 1, 'the flat object path is the primary, so this is the primary\'s')
  t.is(rows[0].verb, 'liked your post')
})

test('★★ and it goes to the RIGHT user, not just the primary', (t) => {
  const item = { id: 'l', type: 'Like', actor: 'https://prsm.space/users/x', objectId: 'https://ap.7rnx.net/ap/users/ashatron/objects/9', public: false, published: '2026-09-14T00:00:00Z' }
  t.is(buildFeed({ self: {}, recipient: 'ashatron', host: 'ap.7rnx.net', inbound: [item] }).length, 1, "ashatron's")
  t.is(buildFeed({ self: {}, recipient: 'p2p', host: 'ap.7rnx.net', inbound: [item] }).length, 0, 'and not the primary\'s')
})

test('⛔ an object on somebody else\'s host recovers nothing and stays unattributed', (t) => {
  const rows = buildFeed({
    self: {}, recipient: 'p2p', host: 'ap.7rnx.net',
    inbound: [{ id: 'l', type: 'Like', actor: 'https://m.example/users/x', objectId: 'https://elsewhere.net/ap/objects/1', public: true, published: 'z' }]
  })
  t.is(rows.length, 0, 'we cannot read another server\'s paths, and will not pretend to')
})

// ── interactions we cannot federate, but can still show ────────────────────────────────────────────
//
// ⛔ MEASURED, NOT ASSUMED. All 6 of the live bridge's unaddressable interactions are likes of BLUESKY
// posts: did:plc:4732wedq…, did:plc:e2jevhuu…, did:plc:y5caotnr…. None is on our host, none is fronted
// by us, and Bluesky content has no ActivityPub identity at all — so `projectLike` is right to refuse
// and minting a URI on a host we do not control is what hyper-social #71 §9 explicitly declines.
//
// ⚠ THE AP PROJECTION IS UNCHANGED. Nothing new goes on the wire. This is OUR page showing OUR act,
// linked to where the post actually lives, which is the only truthful way to render it.

test('★★ an unprojectable like still appears, linked to where the post really is', (t) => {
  const rows = buildFeed({
    self: { handle: '@p2p@ap.7rnx.net' },
    unprojectable: [{ kind: 'like', rkey: 'k1', at: '2026-09-15T10:00:00Z', subject: 'at://did:plc:y5caotnrle4kxlfez4azaf33/app.bsky.feed.post/3mrx7jvraws23' }]
  })
  t.is(rows.length, 1)
  t.is(rows[0].verb, 'you liked a post')
  t.is(rows[0].subject, 'https://bsky.app/profile/did:plc:y5caotnrle4kxlfez4azaf33/post/3mrx7jvraws23',
    'named where it lives, not at a URI we would have had to invent')
  t.is(rows[0].network, 'bsky', 'and the row says which network, so the link is not a surprise')
})

test('⛔ a subject we cannot even locate is still not invented', (t) => {
  const rows = buildFeed({ self: {}, unprojectable: [{ kind: 'like', rkey: 'k2', at: 'z', subject: 'garbage' }] })
  t.is(rows.length, 1, 'the act happened and is shown')
  t.is(rows[0].subject, null, 'but nothing is fabricated to point at')
})