OverviewFilesCommitsIssuesDocsReleasesPeers

favela-host / test / project.test.js

112 lines · javascript ref:
// The device-side half of the bsky projection: two adapters and a choice.
//
// ⛔ WHAT THIS IS FOR. atproto records exist only because something next to the SIGNING KEY authored
// them — the membrane is keyless and re-serves what the device signed. The fediverse is the opposite:
// ap-bridge holds the feed and projects it server-side. So a handset federates with nothing running on
// it and publishes to bsky with nothing running on it either, which is how a phone ended up with a
// correct PLC record, a resolving handle, an active relay, nine fediverse posts — and an atproto repo
// holding one bootstrap record with no profile at all.
//
// The projection loop itself is favela-bridge's and is tested there. What is new here is talking to
// hpp and favela-peer WITHOUT sockets, because SELinux gives an Android app no `sock_file`.

import test from 'brittle'
import { deviceClient, pickNode, peerClient } from '../lib/project.js'

function fakeHppNode (did = 'did:plc:example') {
  const written = []
  const records = new Map()
  const blobsPut = []
  return {
    did,
    written,
    records,
    blobsPut,
    device: {
      state: async () => ({ records: [...records.keys()].map((k) => { const i = k.indexOf('/'); return { collection: k.slice(0, i), rkey: k.slice(i + 1) } }) }),
      applyWrite: async (op) => { written.push(op); records.set(op.collection + '/' + op.rkey, op.record); return { ok: true } },
      repo: { getRecord: async (collection, rkey) => records.get(collection + '/' + rkey) || null }
    },
    blobs: { put: async (buf, mimeType) => { blobsPut.push({ size: buf.length, mimeType }); return { cid: 'bafyfake', size: buf.length } } }
  }
}

test('★★★ the adapter serves exactly the commands the sink calls, against the device', async (t) => {
  const n = fakeHppNode()
  const c = deviceClient(n)

  const rec = { $type: 'app.bsky.actor.profile', displayName: 'Fleeky' }
  await c.call('apply-write', { action: 'put', collection: 'app.bsky.actor.profile', rkey: 'self', record: rec })
  t.is(n.written.length, 1, 'apply-write reaches device.applyWrite')
  t.alike(await c.call('get-record', { collection: 'app.bsky.actor.profile', rkey: 'self' }), rec, 'and is readable back')

  const st = await c.call('get-state', {})
  t.is(st.records.length, 1, 'get-state lists it')
})

test('★★ an absent record is null, never a throw', async (t) => {
  const c = deviceClient(fakeHppNode())
  t.is(await c.call('get-record', { collection: 'app.bsky.feed.post', rkey: 'nope' }), null,
    '"not written yet" is an ordinary answer for a caller deciding whether to write')
})

test('★★★ the adapter has no close — the device must outlive the projection', (t) => {
  const c = deviceClient(fakeHppNode())
  t.absent(c.close, 'BskySink closes its client after every call; this device is not its to close')
})

test('★★ a refused command says so rather than silently doing nothing', async (t) => {
  const c = deviceClient(fakeHppNode())
  await t.exception(() => c.call('get-repo-car', {}), /does not serve/)
})

test('★★ blobs travel by path, and reach the device blob store', async (t) => {
  const n = fakeHppNode()
  const c = deviceClient(n)
  const r = await c.call('upload-blob', { path: new URL('../package.json', import.meta.url).pathname, mimeType: 'image/png' })
  t.is(r.cid, 'bafyfake')
  t.is(r.mimeType, 'image/png')
  t.is(n.blobsPut.length, 1, 'the bytes reached blobs.put')
})

test('★★★ with several identities and none designated it refuses rather than guessing', (t) => {
  const a = fakeHppNode('did:plc:aaa')
  const b = fakeHppNode('did:plc:bbb')
  const hpp = { nodes: [a, b], byDid: { 'did:plc:aaa': a, 'did:plc:bbb': b } }

  const guess = pickNode(hpp, null)
  t.absent(guess.node, 'no guess is made')
  t.ok(/designated/.test(guess.error), guess.error)

  // ⛔ Publishing under the wrong identity is not an error anyone can take back.
  t.is(pickNode(hpp, 'did:plc:bbb').node, b, 'the designated credential decides')
  t.is(pickNode({ nodes: [a], byDid: { 'did:plc:aaa': a } }, null).node, a, 'one identity needs no designation')
  t.ok(/no booted atproto identity/.test(pickNode({ nodes: [] }, null).error), 'and none is said plainly')
})

// ⛔ hpp's bootMultiDevice returns `byDid` as a MAP (device-node.js:879). pickNode indexed it like an
// object, so a designated credential never matched and a node with two DIDs refused every pass even after
// the operator designated one (tabertron, 2026-10-13).
test('pickNode honours the designated credential when byDid is a Map, as hpp returns it', async (t) => {
  const a = { did: 'did:plc:aaa' }
  const b = { did: 'did:plc:bbb' }
  const hpp = { nodes: [a, b], byDid: new Map([[a.did, a], [b.did, b]]) }
  const r = pickNode(hpp, 'did:plc:bbb')
  t.is(r.error, null)
  t.is(r.node, b, 'the designated identity, not a refusal')
  t.ok(pickNode(hpp, null).error, 'still refuses with several and none designated')
})

// ⛔ The projection retracts from bsky only what the peer reports POSITIVELY deleted. Without this
// method on the adapter, a post deleted on the phone stays on bsky for ever — favela-bridge treats a
// peer with no ownDeleted as one that cannot say.
test('★★ the peer adapter reports my deleted posts and replies', async (t) => {
  const node = {
    peer: {
      update: async () => {},
      feed: { tombstones: async (c) => (c === 'post' ? ['p1'] : ['r1']) }
    }
  }
  t.alike(await peerClient(node).ownDeleted(), { post: ['p1'], reply: ['r1'] })
})