// serveMembrane `labels` — a labeler DID's signed label log, served beside its firehose (hyper-social #80,
// member labels step 2).
//
// The shared __label__ log lives in the DID's StateBase, which only co-owners replicate. A keyless
// consumer (the gateway's lens, answering bsky's queryLabels/subscribeLabels) reaches a DID through the
// membrane announce, so the labels must be projected into a plain hypercore and named there — the same
// pattern as the firehose. Without `labels` nothing changes: no core is opened and the announce carries
// labelKey:null, so a consumer can branch on the field.
//
// Run: bare test/membrane-labels.test.mjs
import '../lib/bare-globals.js'
import test from 'brittle'
import b4a from 'b4a'
import fs from 'bare-fs'
import { serveMembrane } from '../lib/serve-membrane.js'
import { LabelRoom } from '../lib/label-room.js'
import { openCorestore } from 'atproto-store/lib/corestore.js'
import { decodeLabel } from 'atproto-identity/lib/labels.js'
const DID = 'did:plc:membranelabels000000000'
const tmp = () => { const d = '/tmp/ar-ml-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const fakeSwarm = () => ({ on () {}, off () {}, keyPair: { publicKey: b4a.alloc(32, 7) }, join () { return { flushed: async () => {} } } })
const fakeDevice = () => ({ repo: { listRecords: async () => [] }, async firehoseLog () { return [] } })
function mockSb () {
const m = new Map()
return {
async set (k, v) { m.set(k, v) },
async del (k) { m.delete(k) },
async get (k) { return m.has(k) ? m.get(k) : null },
async list (prefix) { const o = {}; for (const [k, v] of m) if (k.startsWith(prefix)) o[k] = v; return o }
}
}
const KEY = b4a.from('0b1b6cd4c1ef3ea5a8b0f9c2c5e0d3e7a1c4f6b8d0e2f4a6c8e0b2d4f6a8c0e2', 'hex')
test('★ labels: the signed log is projected into a served core and named in the announce', async (t) => {
const room = new LabelRoom(mockSb(), { src: DID, privateKey: KEY, holds: () => true })
await room.ensure('did:plc:member', 'loser-club-member', { cts: '2026-10-13T00:00:00.000Z' })
await room.tick()
const m = await serveMembrane(fakeDevice(), { corestore: openCorestore(tmp()), swarm: fakeSwarm(), did: DID, labels: room })
t.ok(m.labelKey, 'a label core is opened and its key exported')
const a = m.announcement()
t.is(a.labelKey, m.labelKey, 'the announce names it')
t.is(a.labelLength, 0, 'nothing projected before the first syncLabels')
t.is(await m.syncLabels(), 1, 'syncLabels appends the signed label')
t.is(m.announcement().labelLength, 1, 'and the announce reports the new length')
const l = decodeLabel(await m.labelCore.get(0))
t.is(l.src, DID)
t.is(l.uri, 'did:plc:member')
t.is(l.val, 'loser-club-member')
t.is(await m.syncLabels(), 0, 'idempotent: nothing new, nothing appended')
})
test('without labels nothing changes: no core, labelKey:null, syncLabels is a no-op', async (t) => {
const m = await serveMembrane(fakeDevice(), { corestore: openCorestore(tmp()), swarm: fakeSwarm(), did: DID })
t.is(m.labelKey, null)
t.is(m.labelCore, null)
const a = m.announcement()
t.is(a.labelKey, null, 'the field exists, null')
t.is(a.labelLength, null)
t.is(await m.syncLabels(), 0)
t.ok(a.firehoseKey, 'the existing announce fields are unchanged')
})