OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / notifications.test.js

260 lines · javascript ref:
// Notifications — a PROJECTION, not a delivery mechanism.
//
// `SOCIAL-MODEL.md` §3 lists a notification list among the projections: "a pure function from
// records to a view — derived on read, never stored, never replicated". That settles the thing
// hyper-social #62 was stuck on. #62 framed it as delivery and found the constraint set
// {stranger can notify, survives offline, bounded cost, Sybil-resistant, no relay} unsatisfiable —
// which it is, but only for the half this does not do.
//
// Every interaction is written in its AUTHOR's feed. So if you hold their feed you already have the
// notification; nothing needs to be sent, and there is nothing to deliver. What remains genuinely
// unsolved is only (a) a stranger you hold nothing of — which the DOORBELL answers, and it is
// included here — and (b) waking a sleeping device, which is a separate job.

import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { Peer, POST } from '../lib/peer.js'
import { identity } from './helpers.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-notif-'))
const rec = (body, createdAt = '2026-09-07T00:00:00Z') => ({ frontmatter: { createdAt }, body })

async function mkPeer () {
  const id = await identity()
  const d = await id.device()
  const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()) })
  await f.ready()
  await f.addDevice(d.bind(f.deviceKey))
  await f.update()
  const peer = await new Peer(f).ready()
  peer.bind = (writerKeyHex) => d.bind(writerKeyHex) // how this peer proves who is knocking (node.js sets it in prod)
  return { peer, root: id.rootHex }
}

// me, holding them — the ordinary case, and the only one that can produce a notification.
async function two () {
  const me = await mkPeer()
  const them = await mkPeer()
  await me.peer.feed.put(POST, 'p1', rec('my post'))
  await me.peer.update()
  me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
  them.peer.feeds.set(Peer.nameFor(me.peer.feed.key), me.peer.feed)
  await me.peer.update(); await them.peer.update()
  const target = { feed: me.peer.feed.key.toString('hex'), collection: POST, name: 'p1' }
  return { me, them, target }
}

test('★ every kind of interaction with MY records becomes a notification', async (t) => {
  const { me, them, target } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  await them.peer.like(target)
  await them.peer.repost(target)
  await them.peer.reply(target, 'a reply', 'r1')
  await them.peer.follow(me.peer.feed.key)
  await them.peer.feed.put(POST, 'm1', rec('hi [@you](favela://' + me.root + ')'))
  await them.peer.update()
  await me.peer.update()

  const kinds = (await me.peer.notifications()).map((n) => n.kind).sort()
  t.alike(kinds, ['follow', 'mention', 'reaction', 'reply', 'repost'].sort(),
    'like, repost, reply, follow and mention all arrive — nothing was delivered to get them here')
})

test('⛔ my OWN actions are not notifications', async (t) => {
  const { me, target } = await two()
  t.teardown(() => me.peer.close())
  await me.peer.like(target)
  await me.peer.repost(target)
  await me.peer.update()
  // Liking your own post is a perfectly ordinary thing to do and telling you about it is not.
  t.alike(await me.peer.notifications(), [])
})

test('⛔ interactions with SOMEONE ELSE\'S records are not mine', async (t) => {
  const { me, them } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
  await them.peer.feed.put(POST, 'theirs', rec('their post'))
  await them.peer.update()
  const other = { feed: them.peer.feed.key.toString('hex'), collection: POST, name: 'theirs' }
  await them.peer.like(other)
  await them.peer.update()
  await me.peer.update()
  t.alike(await me.peer.notifications(), [], 'a like on their own post is not news for me')
})

test('a notification says WHO, WHAT and WHICH record', async (t) => {
  const { me, them, target } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
  await them.peer.like(target)
  await them.peer.update(); await me.peer.update()

  const [n] = await me.peer.notifications()
  t.is(n.kind, 'reaction')
  t.is(n.author, them.peer.feed.key.toString('hex'), 'who')
  t.is(n.value, 'like', 'what — the reaction value, so 👍 and a like are distinguishable')
  t.alike(n.target, target, 'and which record of mine it was about')
})

test('★ the DOORBELL is in the list — the one thing a stranger can do', async (t) => {
  const { me, them } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  // ⛔ A STRANGER, which is what this test always claimed to be about. It used to knock from `them`,
  // whose feed `me` already holds — so it was really testing a knock from a friend, and it passed
  // because nothing distinguished the two. Since an interaction now rings the doorbell, that
  // distinction is load-bearing: a knock from a held feed is a SECOND notification for an act that
  // already produced one, so it is skipped, and this test would have been the thing that noticed.
  const stranger = 'ab'.repeat(32)
  // A knock proves which identity is knocking: here a stranger's device, binding the writer core it
  // knocks through (written via my writer, as there is no transport in this test).
  const strangerDevice = await (await identity()).device()
  await me.peer.feed.knock(Buffer.from(stranger, 'hex'), (writerKeyHex) => strangerDevice.bind(writerKeyHex))
  await me.peer.update()

  const knocks = (await me.peer.notifications()).filter((n) => n.kind === 'knock')
  // ⛔ This is the answer to the half #62 could not solve. Everything else here requires holding the
  // author's feed; a knock is what somebody you hold NOTHING of can still do.
  t.is(knocks.length, 1)
  t.is(knocks[0].author, stranger, 'and it names the feed that rang')
})

test('⛔ a knock from a feed we ALREADY HOLD is not a second notification', async (t) => {
  const { me, them, target } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  // They react, which now also rings. We can already READ the reaction, so the knock says nothing
  // the list does not already carry.
  await them.peer.react(target, '🔥', '2026-09-05T00:00:00Z')
  await them.peer.update(); await me.peer.update()

  const list = await me.peer.notifications()
  t.is(list.filter((n) => n.kind === 'reaction').length, 1, 'the reaction is there, by replication')
  t.is(list.filter((n) => n.kind === 'knock').length, 0, 'and not announced a second time as a knock')

  // ⚠ Skipped for RENDERING only — the row is still in the feed, so consent is untouched.
  t.is((await me.peer.feed.knocks()).length, 1, 'the knock itself was not dismissed')
})

test('newest first, and bounded', async (t) => {
  const { me, them, target } = await two()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
  await them.peer.react(target, '🔥', '2026-09-01T00:00:00Z')
  await them.peer.react(target, '😂', '2026-09-05T00:00:00Z')
  await them.peer.update(); await me.peer.update()

  const rows = await me.peer.notifications()
  t.is(rows[0].value, '😂', 'newest first')
  t.is((await me.peer.notifications({ limit: 1 })).length, 1)
})

test('⚠ only feeds I HOLD can notify me', async (t) => {
  const me = await mkPeer()
  const stranger = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), stranger.peer.close()]))
  await me.peer.feed.put(POST, 'p1', rec('my post'))
  await me.peer.update()
  await stranger.peer.like({ feed: me.peer.feed.key.toString('hex'), collection: POST, name: 'p1' })
  await stranger.peer.update()
  await me.peer.update()
  // The structural limit, stated. An interaction lives in its author's feed, so somebody you have no
  // copy of can like your post and you will never know — which is exactly why the doorbell exists.
  t.alike(await me.peer.notifications(), [])
})

// ── the fediverse, through a bridge's OBSERVATION FEED (hyper-social #82, I1 S4) ──────────────────────────────
//
// ⛔ A FEED THIS NODE HOLDS BUT DOES NOT TRUST BLINDLY. A bridge writes what the fediverse did to me into a feed IT
// founded; I hold it like any feed, and it becomes notifications only while its founder is the observer root the
// bridge vouched for at registration (pinned in `peer.observers`). The author is the outside actor — an AP URI,
// never a favela feed — and the row says where it came from.
const OBSERVATION = 'observation'

async function gatewayFeed () {
  const gw = await identity()
  const d = await gw.device()
  const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()), optimistic: true })
  await f.ready()
  await f.addDevice(d.bind(f.deviceKey))
  await f.update()
  return { feed: f, root: gw.rootHex }
}

const obs = (kind, extra = {}) => ({
  frontmatter: { kind, actor: 'https://prsm.space/users/alice', subject: null, target: null, at: 1792005677219, activity: 'https://prsm.space/a/' + kind, ...extra },
  body: extra.body || ''
})

test('★★ observations from a pinned bridge feed become fediverse notifications', async (t) => {
  const me = await mkPeer()
  const gw = await gatewayFeed()
  t.teardown(() => Promise.all([me.peer.close(), gw.feed.close()]))
  await me.peer.feed.put(POST, 'p1', rec('my post'))
  await me.peer.update()
  const target = { feed: me.peer.feed.key.toString('hex'), collection: POST, name: 'p1' }

  await gw.feed.put(OBSERVATION, 'o1', obs('like', { target }))
  await gw.feed.put(OBSERVATION, 'o2', { ...obs('reply', { target, activity: 'https://prsm.space/notes/9' }), body: 'nice post!' })
  await gw.feed.put(OBSERVATION, 'o3', obs('follow'))
  await gw.feed.update()

  const gwHex = gw.feed.key.toString('hex')
  me.peer.feeds.set(Peer.nameFor(gw.feed.key), gw.feed)
  me.peer.observers.set(gwHex, gw.root)

  const n = (await me.peer.notifications()).filter((x) => x.source === 'fediverse')
  t.alike(n.map((x) => x.kind).sort(), ['follow', 'reaction', 'reply'])
  t.ok(n.every((x) => x.author === 'https://prsm.space/users/alice'), 'the author is the outside actor, not the bridge feed')
  const reply = n.find((x) => x.kind === 'reply')
  t.is(reply.body, 'nice post!', 'with the reply text')
  t.alike(reply.target, target, 'aimed at my post')
  t.ok(/^2026-/.test(reply.at), 'an ISO time, so it sorts with every other notification')
})

test('⛔ a held feed whose founder is NOT the pinned observer root yields nothing', async (t) => {
  const me = await mkPeer()
  const gw = await gatewayFeed()
  const impostor = await identity()
  t.teardown(() => Promise.all([me.peer.close(), gw.feed.close()]))
  await gw.feed.put(OBSERVATION, 'o1', obs('follow'))
  await gw.feed.update()
  me.peer.feeds.set(Peer.nameFor(gw.feed.key), gw.feed)
  me.peer.observers.set(gw.feed.key.toString('hex'), impostor.rootHex) // pinned to somebody else
  const n = (await me.peer.notifications()).filter((x) => x.source === 'fediverse')
  t.is(n.length, 0, 'not the founder the bridge vouched for → not rendered')
})

test('an observation feed is never scanned as an ordinary feed', async (t) => {
  const me = await mkPeer()
  const gw = await gatewayFeed()
  t.teardown(() => Promise.all([me.peer.close(), gw.feed.close()]))
  await gw.feed.put(OBSERVATION, 'o1', obs('follow'))
  await gw.feed.update()
  me.peer.feeds.set(Peer.nameFor(gw.feed.key), gw.feed)
  me.peer.observers.set(gw.feed.key.toString('hex'), gw.root)
  const n = await me.peer.notifications()
  t.ok(n.every((x) => x.author !== gw.feed.key.toString('hex')), 'no row is attributed to the bridge feed itself')
})

// ⛔ A bsky actor is a DID, not a URL (hyper-social #82, I2): the same observation feed carries both networks, and a
// row says which one it came from so the surface can link the right place.
test('★ an observation whose actor is a DID is marked source:bsky', async (t) => {
  const me = await mkPeer()
  const gw = await gatewayFeed()
  t.teardown(() => Promise.all([me.peer.close(), gw.feed.close()]))
  await gw.feed.put(OBSERVATION, 'b1', obs('follow', { actor: 'did:plc:alicealicealice', activity: 'at://did:plc:alicealicealice/app.bsky.graph.follow/3x' }))
  await gw.feed.put(OBSERVATION, 'f1', obs('follow'))
  await gw.feed.update()
  me.peer.feeds.set(Peer.nameFor(gw.feed.key), gw.feed)
  me.peer.observers.set(gw.feed.key.toString('hex'), gw.root)
  const rows = (await me.peer.notifications()).filter((x) => x.kind === 'follow')
  const by = Object.fromEntries(rows.map((x) => [x.author, x.source]))
  t.is(by['did:plc:alicealicealice'], 'bsky')
  t.is(by['https://prsm.space/users/alice'], 'fediverse')
})