// Typing `@someone` in a post.
//
// ⛔ `markdown-strip.md` §3.4 is right that a bare `@alice` must never BECOME a mention by guess —
// that rule constrains the CODEC, which has no registry and cannot know who was meant. It does not
// forbid RESOLVING one where the answer is known: the namespace says which feed `alice0` is, and
// `handleVerdict` says whether that feed agrees. Resolution is not a guess.
//
// So the daemon rewrites `@alice0` into `[@alice0](favela://<root>)` before the record is written,
// and only when the handle resolves CONFIRMED. It happens here rather than in the surface because
// the CLI and any other client write posts too, and a feature that only exists in one of them is
// not the feature.
import test from 'brittle'
import { linkMentions } from '../lib/mentions.js'
// A namespace stub: alice0 is confirmed, bob0 is claimed at a feed that disagrees, nobody0 is not
// claimed at all.
const ROOT_A = 'ab'.repeat(32)
const resolve = async (handle) => {
if (handle === 'alice0') return { handle, root: ROOT_A, verdict: 'confirmed' }
if (handle === 'bob0') return { handle, root: 'cd'.repeat(32), verdict: 'MISMATCH' }
if (handle === 'far0') return { handle, root: 'ef'.repeat(32), verdict: 'unheld' }
return null
}
test('★ a bare @handle becomes a real mention', async (t) => {
t.is(await linkMentions('hey @alice0 look', resolve),
'hey [@alice0](favela://' + ROOT_A + ') look')
})
test('⛔ a handle the FEED disagrees with is left as text', async (t) => {
// This is the rename attack arriving through the composer: a name claimed at somebody else's feed
// key. It must never become a link that resolves to them.
t.is(await linkMentions('hey @bob0', resolve), 'hey @bob0')
})
test('⚠ a handle we cannot CHECK is left as text too', async (t) => {
// `unheld` — we do not hold that feed, so we cannot compare the claim against its founder.
// Unchecked is not confirmed, and linking it would make the check decorative.
t.is(await linkMentions('hey @far0', resolve), 'hey @far0')
})
test('an unclaimed name stays plain text', async (t) => {
t.is(await linkMentions('hey @nobody0 and @alice', resolve), 'hey @nobody0 and @alice')
})
test('⛔ an ALREADY-linked mention is not rewritten again', async (t) => {
const already = '[@alice0](favela://' + ROOT_A + ')'
t.is(await linkMentions(already, resolve), already, 'or every save would nest the link')
})
test('⛔ code is not prose — @ inside a span or a fence is left alone', async (t) => {
t.is(await linkMentions('use `@alice0` here', resolve), 'use `@alice0` here')
const fenced = '```\n@alice0\n```'
t.is(await linkMentions(fenced, resolve), fenced)
// A regex over the whole body would have rewritten both, and a code sample is exactly where an
// @ is most likely to appear without meaning a person.
})
test('⚠ an email address is not a mention', async (t) => {
t.is(await linkMentions('mail me at alice@alice0.example', resolve), 'mail me at alice@alice0.example')
})
test('several mentions in one post all resolve', async (t) => {
const out = await linkMentions('@alice0 and @alice0 again', resolve)
t.is(out.match(/favela:\/\//g).length, 2)
})
test('⚠ resolution is asked ONCE per distinct handle', async (t) => {
let calls = 0
const counting = async (h) => { calls++; return resolve(h) }
await linkMentions('@alice0 @alice0 @alice0', counting)
t.is(calls, 1, 'a post naming one person ten times is one lookup, not ten')
})
// ── the thing people actually type ────────────────────────────────────────────────────────────
//
// ⛔ The first version of this matched `@alice0` and never `@alice`, so every mention anybody wrote
// stayed plain text. The number exists so a NAME is never scarce; nobody types it.
const byName = async (token) => {
if (token === 'alice0' || token === 'alice') return { handle: 'alice0', root: ROOT_A, verdict: 'confirmed' }
if (token === 'crowd') return null // two people hold it — the caller returns null for ambiguity
return null
}
test('★★ a bare NAME resolves — this is what a person types', async (t) => {
t.is(await linkMentions('hey @alice how are you', byName),
'hey [@alice](favela://' + ROOT_A + ') how are you')
})
test('the handle form still works', async (t) => {
t.is(await linkMentions('hey @alice0', byName), 'hey [@alice0](favela://' + ROOT_A + ')')
})
test('⛔ an AMBIGUOUS name stays text — two alices means @alice names nobody', async (t) => {
// NAMING.md §3.2: fail visibly rather than guess. "The first alice" is that guess wearing a
// helpful face, and it would silently link a stranger.
t.is(await linkMentions('hey @crowd', byName), 'hey @crowd')
})