OverviewFilesCommitsIssuesDocsReleasesPeers

favela-bridge / test / sink-injection.test.js

101 lines · javascript ref:
// The sink must work with no socket at all — because on a phone there cannot be one.
//
// ⛔ WHY THIS MATTERS. The bsky projection runs next to the SIGNING KEY. On a desktop that is a
// co-located hpp reached over a unix socket; on a handset hpp is in the same worklet and SELinux
// refuses an unprivileged app a `sock_file` at all. So the phone supplies a transport and no path.
//
// Two things stopped that working, and both are the same mistake in different clothes — asking about
// the SOCKET when the question is about the TRANSPORT:
//
//   1. `get configured ()` returned `!!this.sock`, so every method short-circuited to "no bsky sink
//      configured" for a caller that had already supplied a working connect.
//   2. `import { Client } from 'hyper-svc'` sat at module scope. bare-pack bundles by IMPORT, not by
//      use, so naming it there ships bare-net and a unix-socket client into a worklet that can never
//      call them.
//
// The measured consequence of getting this wrong is not an error — it is a projection that silently
// does nothing, which is the state a handset was in with a Bluesky identity that resolved correctly
// and an atproto repo holding one bootstrap record.

import test from 'brittle'
import fs from 'bare-fs'
import { BskySink, PROFILE_COLLECTION, PROFILE_RKEY } from '../lib/bsky.js'

const ROOT = new URL('..', import.meta.url).pathname

// A stand-in for the co-located device: the same `call(cmd, args)` shape hpp's UDS surface exposes,
// which is also the shape favela-host's inProcessClient already hands the surface.
function fakeDevice () {
  const records = new Map()
  const calls = []
  return {
    records,
    calls,
    client: {
      call: async (cmd, args = {}) => {
        calls.push(cmd)
        if (cmd === 'get-state') {
          return { records: [...records.keys()].map((k) => { const i = k.indexOf('/'); return { collection: k.slice(0, i), rkey: k.slice(i + 1) } }) }
        }
        if (cmd === 'apply-write') { records.set(args.collection + '/' + args.rkey, args.record); return { ok: true } }
        if (cmd === 'get-record') return records.get(args.collection + '/' + args.rkey) || null
        if (cmd === 'upload-blob') return { cid: 'bafyfake', mimeType: args.mimeType, size: 3 }
        throw new Error('unexpected command ' + cmd)
      }
      // ⛔ NO `close`. The sink closes its client after every call — correct for a socket it opened,
      // fatal for a device that must outlive the projection. bsky.js guards with `if (client.close)`,
      // so omitting it is the adapter saying "this one is not mine to close".
    }
  }
}

test('★★★ a sink with an injected transport and NO socket is configured', (t) => {
  const d = fakeDevice()
  const sink = new BskySink(null, { connect: async () => d.client })
  t.ok(sink.configured, 'a transport is what makes it usable, not a path')
  t.absent(new BskySink(null).configured, 'and with neither it is still correctly unconfigured')
})

test('★★ the whole surface the projector uses works over an injected transport', async (t) => {
  const d = fakeDevice()
  const sink = new BskySink(null, { connect: async () => d.client })

  const profile = { $type: PROFILE_COLLECTION, displayName: 'Fleeky' }
  t.alike(await sink.putProfile(profile), { sent: true }, 'the profile is written')
  t.alike(await sink.getProfile(), profile, 'and read back from the repo, not from a local note')

  const post = { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-09-11T00:00:00.000Z' }
  const r = await sink.send('a-favela-record', post)
  t.ok(r.sent, 'a post is sent')
  t.ok(/^[0-9a-z]{13}$/.test(r.rkey), 'under a real TID rkey — the AppView drops posts without one')

  const have = await sink.have()
  t.ok(have instanceof Set && have.has(r.rkey), 'and `have` reports it, so a re-run does not duplicate it')

  const blob = await sink.uploadBlob(ROOT + 'package.json', 'image/png')
  t.is(blob.$type, 'blob')
  t.is(blob.ref.$link, 'bafyfake', 'blobs travel the same transport')
})

test('★★ absent and unreadable stay distinguishable over the injected transport', async (t) => {
  const sink = new BskySink(null, { connect: async () => ({ call: async () => { throw new Error('device is busy') } }) })

  const got = await sink.getProfile()
  t.ok(got && got.unknown, 'a failed read is `unknown`, never null')
  t.absent(await new BskySink(null, { connect: async () => ({ call: async () => null }) }).getProfile(),
    'and a genuinely absent record is null — writing because a read failed republishes on every blip')
})

// ⛔ SOURCE-LEVEL, because the cost is paid by bare-pack at BUILD time and is invisible at runtime:
// the socket client works perfectly well on a desktop while bloating a bundle that can never use it.
test('★★ hyper-svc is not imported at module scope', (t) => {
  const src = fs.readFileSync(ROOT + 'lib/bsky.js', 'utf8')
  t.absent(/^import .*from 'hyper-svc'/m.test(src),
    'a top-level import ships bare-net and a unix socket into the worklet')
  // ⛔ AND OPAQUE. A plain `await import('hyper-svc')` is a static string that bare-pack FOLLOWS, so
  // deferring alone bundled the transport regardless — measured: a third copy of hyper-svc appeared
  // under favela-host/node_modules/favela-bridge, 610 files to 915.
  t.ok(/await import\('hyper-' \+ 'svc'\)/.test(src),
    'a computed specifier resolves at runtime and is unfollowable at pack time')
})