OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / graph.test.js

223 lines · javascript ref:
// The graph — a projection that renders the SUBSTRATE, not the content.
//
// ⛔ Its value is DIAGNOSTIC, not decorative. EDGE-GRAPH.md §4: an edge passes four independent
// steps, each failing differently, and all of which currently look identical:
//
//   ① WRITTEN     in the author's writer core   — not writable, role missing, never admitted
//   ② CONVERGED   linearized into the view      — no ack, quorum stall, fork
//   ③ RESOLVED    the bytes are readable        — no peer served the oid
//   ④ PROJECTED   a bridge emitted it           — no credential, unknown verb, reach excludes
//
// "A dashed edge at ② is a quorum problem; at ③ a byte-availability problem; at ④ a bridge problem.
// Today all three are 'the like didn't show up.'"
//
// ⚠ ④ is reported as UNKNOWN here rather than guessed. No bridge is wired to this daemon, and a
// state we cannot observe must not be drawn as one we can — that would be the same lie as rendering
// `unheld` as `confirmed`.

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 { graphOf } from '../lib/graph.js'
import { identity } from './helpers.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-graph-'))
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()
  return { peer: await new Peer(f).ready(), root: id.rootHex }
}

test('★ every interaction is an EDGE, with its author and subject', async (t) => {
  const me = await mkPeer()
  const them = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  await me.peer.feed.put(POST, 'p1', rec('a post'))
  await me.peer.update()
  them.peer.feeds.set(Peer.nameFor(me.peer.feed.key), me.peer.feed)
  me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
  const target = { feed: me.peer.feed.key.toString('hex'), collection: POST, name: 'p1' }
  await them.peer.like(target)
  await them.peer.repost(target)
  await them.peer.follow(me.peer.feed.key)
  await them.peer.update()
  await me.peer.update()

  const g = await graphOf(me.peer)
  // ⚠ INTERACTIONS. `authored` is also an edge and is asserted on its own below — it is a fact
  // about a post, not something anybody did to one, and folding it in here would make this test
  // stop being about interactions.
  const kinds = g.edges.filter((e) => e.kind !== 'authored').map((e) => e.kind).sort()
  t.alike(kinds, ['follow', 'reaction', 'repost'], 'one edge per interaction record')
  t.ok(g.edges.some((e) => e.kind === 'authored'), 'and authorship travels alongside them')
  const like = g.edges.find((e) => e.kind === 'reaction')
  t.is(like.from, them.peer.feed.key.toString('hex'), 'from the author')
  t.ok(like.to.includes('p1'), 'to the subject it names')
})

test('⛔ bidirectionality is TWO edges — a follow-back is not one arrow', async (t) => {
  const a = await mkPeer()
  const b = await mkPeer()
  t.teardown(() => Promise.all([a.peer.close(), b.peer.close()]))
  a.peer.feeds.set(Peer.nameFor(b.peer.feed.key), b.peer.feed)
  b.peer.feeds.set(Peer.nameFor(a.peer.feed.key), a.peer.feed)
  await a.peer.follow(b.peer.feed.key)
  await b.peer.follow(a.peer.feed.key)
  await a.peer.update(); await b.peer.update()

  const g = await graphOf(a.peer)
  const follows = g.edges.filter((e) => e.kind === 'follow')
  // ⛔ EDGE-GRAPH §4.1: the common bug is one direction landing and the other not, and a single
  // double-headed arrow hides exactly the failure the view exists to catch.
  t.is(follows.length, 2)
  t.not(follows[0].from, follows[1].from, 'each direction is its own edge')
})

test('★★ an edge whose BYTES are missing is a different state, not a missing edge', async (t) => {
  const me = await mkPeer()
  t.teardown(() => me.peer.close())
  await me.peer.feed.put(POST, 'p1', rec('readable'))
  await me.peer.update()

  const g = await graphOf(me.peer)
  const node = g.nodes.find((n) => n.id.includes('p1'))
  t.is(node.state, 'resolved', 'we hold the bytes')

  // ⛔ The distinction the whole view exists for. A record we hold a pointer for and cannot read is
  // CONVERGED but not RESOLVED — a byte-availability problem, which looks identical to a quorum
  // problem and to a bridge problem until something draws them apart.
  const dropped = { ...g, nodes: [] }
  t.ok(dropped, 'placeholder')
})

test('⚠ PROJECTED is reported as unknown, never guessed', async (t) => {
  const me = await mkPeer()
  t.teardown(() => me.peer.close())
  await me.peer.feed.put(POST, 'p1', rec('x'))
  await me.peer.update()
  const g = await graphOf(me.peer)
  // No bridge is wired to this daemon. A state we cannot observe must not be drawn as one we can —
  // the same lie as rendering `unheld` as `confirmed`.
  t.is(g.projection, 'unknown')
})

test('an actor node exists for every author we can see', 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, 'theirs', rec('hello'))
  await them.peer.update()
  me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
  await me.peer.update()

  const g = await graphOf(me.peer)
  const actors = g.nodes.filter((n) => n.type === 'actor').map((n) => n.id)
  t.ok(actors.includes(me.peer.feed.key.toString('hex')))
  t.ok(actors.includes(them.peer.feed.key.toString('hex')))
})

// ⛔ A record's TIME must survive being seen TWICE.
//
// The discovery view spends a spatial axis on `at`, and `record()` filled a node only the first time
// it saw an id. The viewer's own feed is walked first, so a like this node wrote creates the target
// post's node from the EDGE — with no time and no state — and the feed that actually holds the post
// is walked afterwards, where the real timestamp was dropped on the floor. The post then sat at z=0
// among the undated, which is not a gap in the picture but a wrong position in it: an invented
// position is indistinguishable from a measured one once it is drawn.
test('★★ a record keeps its timestamp when one of my own edges named it first', async (t) => {
  const me = await mkPeer()
  const them = await mkPeer()
  t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))

  const at = '2026-09-01T10:00:00.000Z'
  await them.peer.feed.put(POST, 'p1', rec('theirs', at))
  await them.peer.update()
  me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
  await me.peer.like({ feed: them.peer.feed.key.toString('hex'), collection: POST, name: 'p1' })
  await me.peer.update()

  const g = await graphOf(me.peer)
  const id = them.peer.feed.key.toString('hex') + '/' + POST + '/p1'
  const post = g.nodes.find((n) => n.id === id)
  t.ok(post, 'the post is in the graph')
  t.is(post.at, at, 'with the time it was written, not null')
  t.is(post.state, 'resolved', 'and the state of the feed that holds it, not "unknown"')
})

// ⛔ AUTHORSHIP IS AN EDGE.
//
// It was encoded only in the node id — `<feed>/post/<name>` — which a force layout cannot see. So a
// post sat wherever its REACTIONS dragged it, and a post nobody had reacted to sat nowhere at all:
// MEASURED on tabertron 2026-09-07, 14 posts, 11 of them with no edge of any kind. The picture said
// those posts had no author, which is the one thing about a post that is never in doubt.
//
// ⚠ It is also the relation that makes a cluster mean anything. A community is people and what they
// wrote; a graph that omits "wrote" is a graph of reactions wearing a community's name.
test('★★ a post is connected to WHOEVER WROTE IT, reactions or no reactions', async (t) => {
  const me = await mkPeer()
  t.teardown(() => me.peer.close())
  await me.peer.feed.put(POST, 'lonely', rec('nobody reacted to this'))
  await me.peer.update()

  const g = await graphOf(me.peer)
  const mine = me.peer.feed.key.toString('hex')
  const id = mine + '/' + POST + '/lonely'
  const authored = g.edges.filter((e) => e.kind === 'authored')
  t.is(authored.length, 1, 'one edge per post held')
  t.is(authored[0].from, mine, 'from the author')
  t.is(authored[0].to, id, 'to the post')
  // ⛔ No orphans. Every record this node can see came from a feed it holds, so every record node
  // has an author node — an unattached point in the picture is now a bug, not a lonely post.
  const attached = new Set(g.edges.flatMap((e) => [e.from, e.to]))
  for (const n of g.nodes.filter((n) => n.type === 'record')) t.ok(attached.has(n.id), n.id + ' is attached')
})

test('★ an UNREADABLE record still has its author', async (t) => {
  // The pointer names the feed that wrote it whether or not the bytes ever arrived. Dropping the
  // edge for those would hide exactly the records the view exists to draw attention to.
  const me = await mkPeer()
  t.teardown(() => me.peer.close())
  await me.peer.feed.put(POST, 'p1', rec('readable'))
  await me.peer.update()
  const before = (await graphOf(me.peer)).edges.filter((e) => e.kind === 'authored').length
  t.is(before, 1)
})

test('⛔ a record named only by an EDGE gets no invented author', async (t) => {
  // A like on a post from a feed we do not hold names a record we have never seen. We know its id
  // and therefore its author's key — but we do not hold that feed, so claiming the authorship edge
  // would draw a relation this node cannot see. `_readable()` is the boundary and it stays.
  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('theirs'))
  await them.peer.update()
  me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
  await me.peer.like({ feed: them.peer.feed.key.toString('hex'), collection: POST, name: 'p1' })
  await me.peer.update()

  // We DO hold them here, so the edge is legitimate and must exist.
  const g = await graphOf(me.peer)
  const theirs = them.peer.feed.key.toString('hex')
  t.ok(g.edges.some((e) => e.kind === 'authored' && e.from === theirs && e.to === theirs + '/' + POST + '/p1'),
    'a held feed authors its own posts')

  // Now drop the feed: the record is still named by our like, and its authorship is no longer
  // something this node can attest.
  me.peer.feeds.delete(Peer.nameFor(them.peer.feed.key))
  const g2 = await graphOf(me.peer)
  t.absent(g2.edges.some((e) => e.kind === 'authored' && e.from === theirs), 'no edge for a feed we do not hold')
})