OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / mentions.test.js

96 lines · javascript ref:
// Mentions — being named in someone else's post.
//
// The codec already does its half: `[@alice](favela://<identity-root>)` produces a `#mention` facet
// carrying the target exactly as written, and a bare `@alice` produces plain text with NO facet
// (`markdown-strip.md` §3.4). What was missing is anything that CONSUMES them, so being mentioned
// was invisible.
//
// ⛔ The target is the identity ROOT, never a feed key and never a DID. `markdown-strip.md` §3.4 was
// revised on 2026-09-05 for exactly this: a DID in canonical bytes makes rotating one a rewrite of
// every record that ever named you, which `record-vocabulary.md` §8 forbids. The root is the one
// thing that does not rotate.
//
// ⚠ There is no mention EDGE. A mention lives in the body of a post, so finding mentions is a scan
// of what you hold — the same class of work as `search`, and it is stated rather than indexed away.

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-men-'))
const rec = (body, createdAt = '2026-09-07T00:00:00Z') => ({ frontmatter: { createdAt }, body })

// A feed founded by a KNOWN identity, so its root is something a mention can name.
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 p = await new Peer(f).ready()
  return { peer: p, root: id.rootHex }
}

test('★ a mention of MY root is found; a mention of someone else is not', async (t) => {
  const me = await mkPeer()
  const them = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  await them.peer.feed.put(POST, 'p1', rec('hello [@me](favela://' + me.root + ') how are you'))
  await them.peer.feed.put(POST, 'p2', rec('unrelated [@other](favela://' + them.root + ')'))
  await them.peer.update()
  await me.peer.follow(them.peer.feed.key, them.peer.feed)
  await me.peer.update()

  const mine = await me.peer.myMentions()
  t.alike(mine.map((m) => m.name), ['p1'], 'only the one that names me')
  t.is(mine[0].author, them.peer.feed.key.toString('hex'), 'and it says who said it')
})

test('⛔ a BARE @name is not a mention — never a guess', async (t) => {
  const me = await mkPeer()
  const them = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  await them.peer.feed.put(POST, 'p1', rec('hey @me, no link here'))
  await them.peer.update()
  await me.peer.follow(them.peer.feed.key, them.peer.feed)
  await me.peer.update()

  // `markdown-strip.md` §3.4: a mention resolves or is omitted. Guessing who `@me` meant is how a
  // notification list starts telling people they were named when they were not.
  t.alike(await me.peer.myMentions(), [])
})

test('a mention in MY OWN post about myself still counts as authored by me', async (t) => {
  const me = await mkPeer()
  t.teardown(() => me.peer.close())
  await me.peer.feed.put(POST, 'p1', rec('note to [@self](favela://' + me.root + ')'))
  await me.peer.update()
  const mine = await me.peer.myMentions()
  // Not filtered out here. Whether to SHOW yourself mentioning yourself is a rendering decision,
  // and the projection stays honest about what it found.
  t.is(mine.length, 1)
  t.is(mine[0].author, me.peer.feed.key.toString('hex'))
})

test('⚠ mentions come only from feeds I HOLD', async (t) => {
  const me = await mkPeer()
  const them = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  await them.peer.feed.put(POST, 'p1', rec('[@me](favela://' + me.root + ')'))
  await them.peer.update()
  // No follow, no hold. ⛔ This is the structural limit of the whole model: an interaction lives in
  // its author's feed, so someone you have no copy of can name you and you will never know. The
  // doorbell is the one thing a stranger CAN do.
  t.alike(await me.peer.myMentions(), [], 'a stranger naming me is invisible, and that is structural')
})