// Protocol 4 membership routing: guild.roster (board state, possibly chunked) and // guild.leave (a real-time departure, logged like its guild.join counterpart). const { test, beforeEach } = require('node:test') const assert = require('node:assert/strict') const shardIngest = require('../utils/shardIngest') function makeDeps() { const calls = { roster: [], memberRemove: [], appended: [], broadcast: [] } const noop = async () => {} return { calls, shardEvents: { append: async (row) => { calls.appended.push(row); return true } }, shardState: { upsertGuildRoster: async (ev) => { calls.roster.push(ev) }, removeGuildMember: async (ev) => { calls.memberRemove.push(ev) }, upsertGuild: noop, removeGuild: noop, clearOnline: noop, upsertOnline: noop, setOffline: noop, addEconomySample: noop, }, shardLinks: { removeByAccount: noop }, uoLinkConfig: { recordStatus: noop }, broadcast: (ev) => { calls.broadcast.push(ev) }, pushDispatch: async () => {}, log: { warn() {}, info() {}, error() {} }, } } beforeEach(() => shardIngest.reset()) test('guild.roster routes to upsertGuildRoster and is NOT logged', async () => { // It is board state like guild.update, and the one fat frame on the wire — // logging it would put a full membership snapshot in shard_events on every // membership change. const deps = makeDeps() const r = await shardIngest.ingest( { kind: 'guild.roster', id: 7, seq: 0, more: false, total: 2, members: [{ serial: '0x1', name: 'Ada' }, { serial: '0x2', name: 'Bo' }], t: 1 }, deps, ) assert.equal(deps.calls.roster.length, 1) assert.equal(deps.calls.roster[0].id, 7) assert.equal(deps.calls.roster[0].members.length, 2) assert.equal(r.logged, false) }) test('every frame of a chunked roster reaches the model, seq intact', async () => { // The sidecar reassembles for its own board, but the live feed and the /history // backfill both carry individual frames — so the model must see each one with its // seq, which is what tells it whether to clear the guild first. const deps = makeDeps() for (const [seq, more, serial] of [[0, true, '0x1'], [1, true, '0x2'], [2, false, '0x3']]) { await shardIngest.ingest( { kind: 'guild.roster', id: 7, seq, more, total: 3, members: [{ serial, name: serial }], t: 1 }, deps, ) } assert.deepEqual(deps.calls.roster.map((e) => e.seq), [0, 1, 2]) assert.deepEqual(deps.calls.roster.map((e) => e.more), [true, true, false]) }) test('guild.leave is logged and broadcast, like guild.join', async () => { const deps = makeDeps() const r = await shardIngest.ingest( { kind: 'guild.leave', id: 7, name: 'The Cartographers', who: '0x2', t: 2 }, deps) assert.equal(r.logged, true) assert.equal(deps.calls.appended.length, 1) assert.equal(deps.calls.appended[0].kind, 'guild.leave') assert.equal(deps.calls.broadcast.length, 1) assert.deepEqual(deps.calls.memberRemove.map((e) => e.who), ['0x2']) })