// ── The bot's half of Team voice channels (TEAMS.md §7.3, phase 9) ──────── // // Nothing here talks to Discord. `fakeGuild` records the calls, and the // assertions are about the four things this side genuinely owns — the ones the // site cannot decide because it cannot see the guild: // // 1. **The overwrite set.** @everyone denied, the Team's role allowed, each // configured staff role allowed — and a staff role the operator has since // deleted is FILTERED, because Discord rejects the whole set for one bad id // and that would take the Team's own grant down with it. // 2. **The membership diff is bounded and the remainder is reported.** Each // grant is its own API call; an unbounded first pass on a large guild // outlives its own timeout, which is the one failure that leaves the site // not knowing what was applied. // 3. **A member who linked Discord but never joined the guild is skipped // silently.** That is §2.6 hop 3 without hop 4 — an ordinary state, not an // error, and certainly not a hundred log lines. // 4. **A missing target is success.** A teardown that finds its channel already // deleted has reached the desired end state; a sync that finds one deleted // simply creates it again. const { test } = require('node:test') const assert = require('node:assert/strict') const { ChannelType, PermissionFlagsBits } = require('discord.js') const teamVoice = require('../src/discord/teamVoice') const EVERYONE = 'guild-everyone' function fakeMember(id, { canGrant = true } = {}) { const roles = new Set() return { id, roles: { cache: roles, add: async (role) => { if (!canGrant) throw new Error('Missing Permissions') roles.add(role.id) }, remove: async (role) => { roles.delete(role.id) }, }, } } function fakeGuild({ members = [], roles = [], channels = [], botPermissions = [PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles], } = {}) { const memberMap = new Map(members.map((m) => [m.id, m])) const roleMap = new Map(roles.map((r) => [r.id, r])) const channelMap = new Map(channels.map((c) => [c.id, c])) const created = { roles: [], channels: [] } let nextId = 1000 const guild = { id: 'guild-1', created, roles: { everyone: { id: EVERYONE }, cache: roleMap, fetch: async (id) => roleMap.get(id) || null, create: async (opts) => { const role = { id: String(nextId++), name: opts.name, members: [], setName: async (name) => { role.name = name }, delete: async () => { roleMap.delete(role.id) }, } roleMap.set(role.id, role) created.roles.push(opts) return role }, }, channels: { cache: channelMap, fetch: async (id) => channelMap.get(id) || null, create: async (opts) => { const channel = { id: String(nextId++), name: opts.name, type: opts.type, parentId: opts.parent || null, overwrites: opts.permissionOverwrites || [], permissionOverwrites: { set: async (list) => { channel.overwrites = list }, }, setParent: async (parentId) => { channel.parentId = parentId }, setName: async (name) => { channel.name = name }, delete: async () => { channelMap.delete(channel.id) }, } channelMap.set(channel.id, channel) created.channels.push(opts) return channel }, }, members: { me: { permissions: { has: (bit) => botPermissions.includes(bit) }, roles: { highest: { position: 7 } } }, cache: memberMap, fetch: async () => memberMap, }, } return guild } const fakeClient = (guild) => ({ guilds: { fetch: async () => guild } }) const voiceChannel = (id, over = {}) => { const channel = { id, name: 'The Silver Hand', type: ChannelType.GuildVoice, parentId: '500', overwrites: [], permissionOverwrites: { set: async (list) => { channel.overwrites = list } }, setParent: async (parentId) => { channel.parentId = parentId }, setName: async (name) => { channel.name = name }, delete: async () => {}, ...over, } return channel } const category = (id = '500') => ({ id, type: ChannelType.GuildCategory }) const role = (id, name = 'The Silver Hand', members = []) => { const r = { id, name, members, setName: async (next) => { r.name = next }, delete: async () => {}, } return r } // ── Preflight ────────────────────────────────────────────────────────────── test('preflight reports both permissions and the guild-wide role count', async () => { const guild = fakeGuild({ roles: [role('1'), role('2')] }) const result = await teamVoice.preflight(fakeClient(guild), 'guild-1') assert.equal(result.can_manage_channels, true) assert.equal(result.can_manage_roles, true) // The GUILD's roles, not ours. The 250 cap is shared with everything the // operator made themselves, so counting only ours would promise headroom that // is not there. assert.equal(result.role_count, 2) assert.equal(result.bot_role_position, 7) }) test('preflight reports a missing permission rather than throwing', async () => { const guild = fakeGuild({ botPermissions: [PermissionFlagsBits.ManageChannels] }) const result = await teamVoice.preflight(fakeClient(guild), 'guild-1') assert.equal(result.can_manage_channels, true) assert.equal(result.can_manage_roles, false) }) // ── Overwrites ───────────────────────────────────────────────────────────── test('the overwrite set denies @everyone and allows the Team role', () => { const guild = fakeGuild() const list = teamVoice.overwritesFor(guild, role('900'), []) assert.equal(list.length, 2) assert.equal(list[0].id, EVERYONE) assert.deepEqual(list[0].deny, teamVoice.ACCESS_BITS) assert.equal(list[1].id, '900') assert.deepEqual(list[1].allow, teamVoice.ACCESS_BITS) }) test('a configured staff role that still exists gets an allow', () => { const staff = role('777', 'Moderators') const guild = fakeGuild({ roles: [staff] }) const list = teamVoice.overwritesFor(guild, role('900'), ['777']) assert.equal(list.length, 3) assert.equal(list[2].id, '777') }) test('a staff role deleted in Discord is skipped, not sent — it would void the whole set', () => { const guild = fakeGuild({ roles: [] }) const list = teamVoice.overwritesFor(guild, role('900'), ['deleted-1']) assert.equal(list.length, 2) assert.ok(!list.some((o) => o.id === 'deleted-1')) }) // ── Ensure ───────────────────────────────────────────────────────────────── test('a missing category is created; an existing one is reused', async () => { const guild = fakeGuild() const made = await teamVoice.ensureCategory(guild, null) assert.equal(guild.created.channels.length, 1) assert.equal(guild.created.channels[0].type, ChannelType.GuildCategory) const again = await teamVoice.ensureCategory(guild, made.id) assert.equal(again.id, made.id) assert.equal(guild.created.channels.length, 1) }) test('a category id pointing at something that is not a category makes a new one', async () => { const guild = fakeGuild({ channels: [voiceChannel('700')] }) await teamVoice.ensureCategory(guild, '700') assert.equal(guild.created.channels.length, 1) }) test('the Team role is created not mentionable and not hoisted', async () => { const guild = fakeGuild() const { role: made, created } = await teamVoice.ensureRole(guild, null, 'The Silver Hand') assert.equal(created, true) assert.equal(made.name, 'The Silver Hand') // A Team with two hundred members must not become a way to ping them all, or a // second copy of the member list down the sidebar. assert.equal(guild.created.roles[0].mentionable, false) assert.equal(guild.created.roles[0].hoist, false) }) test('a renamed Team renames its role rather than making a second', async () => { const existing = role('900', 'Old Name') const guild = fakeGuild({ roles: [existing] }) const { role: made, created } = await teamVoice.ensureRole(guild, '900', 'New Name') assert.equal(created, false) assert.equal(made.name, 'New Name') assert.equal(guild.created.roles.length, 0) }) test('a rename Discord refuses does not fail the pass — access matters more than a label', async () => { const existing = role('900', 'Old Name') existing.setName = async () => { throw new Error('rate limited') } const guild = fakeGuild({ roles: [existing] }) const { role: made } = await teamVoice.ensureRole(guild, '900', 'New Name') assert.equal(made.id, '900') }) test('a channel a human deleted is simply created again', async () => { const guild = fakeGuild() const { channel, created } = await teamVoice.ensureChannel(guild, 'gone-1', { name: 'The Silver Hand', category: category(), role: role('900'), staffRoleIds: [], }) assert.equal(created, true) assert.equal(channel.type, ChannelType.GuildVoice) assert.equal(channel.parentId, '500') }) test('an existing channel has its overwrites re-asserted every pass', async () => { const existing = voiceChannel('600') const guild = fakeGuild({ channels: [existing] }) const { created } = await teamVoice.ensureChannel(guild, '600', { name: 'The Silver Hand', category: category(), role: role('900'), staffRoleIds: [], }) assert.equal(created, false) // Re-setting rather than diffing is what repairs a channel somebody edited by // hand. assert.equal(existing.overwrites.length, 2) }) test('a channel that is no longer a voice channel is left alone and a new one made', async () => { const text = voiceChannel('600', { type: ChannelType.GuildText }) const guild = fakeGuild({ channels: [text] }) const { channel, created } = await teamVoice.ensureChannel(guild, '600', { name: 'The Silver Hand', category: category(), role: role('900'), staffRoleIds: [], }) assert.equal(created, true) assert.notEqual(channel.id, '600') }) // ── Membership ───────────────────────────────────────────────────────────── test('the role is granted to the members the site named', async () => { const alice = fakeMember('a') const bob = fakeMember('b') const guild = fakeGuild({ members: [alice, bob] }) const teamRole = role('900', 'The Silver Hand', []) const result = await teamVoice.syncRoleMembers(guild, teamRole, ['a', 'b'], 50) assert.equal(result.added, 2) assert.equal(result.removed, 0) assert.equal(result.pending, 0) }) test('a member who left the Team has the role taken away', async () => { const alice = fakeMember('a') const bob = fakeMember('b') const guild = fakeGuild({ members: [alice, bob] }) const teamRole = role('900', 'The Silver Hand', [alice, bob]) const result = await teamVoice.syncRoleMembers(guild, teamRole, ['a'], 50) assert.equal(result.added, 0) assert.equal(result.removed, 1) }) test('a member who linked Discord but never joined the guild is skipped without an error', async () => { const guild = fakeGuild({ members: [] }) const result = await teamVoice.syncRoleMembers(guild, role('900', 'x', []), ['not-in-guild'], 50) assert.equal(result.added, 0) assert.equal(result.pending, 0) }) test('the diff is bounded and the remainder is REPORTED, not dropped', async () => { const members = Array.from({ length: 10 }, (_, i) => fakeMember(`m${i}`)) const guild = fakeGuild({ members }) const result = await teamVoice.syncRoleMembers(guild, role('900', 'x', []), members.map((m) => m.id), 4) assert.equal(result.added, 4) assert.equal(result.pending, 6) }) test('one member the bot cannot touch does not cost the other forty-nine', async () => { const ok1 = fakeMember('a') const nope = fakeMember('b', { canGrant: false }) const ok2 = fakeMember('c') const guild = fakeGuild({ members: [ok1, nope, ok2] }) const result = await teamVoice.syncRoleMembers(guild, role('900', 'x', []), ['a', 'b', 'c'], 50) assert.equal(result.added, 2) }) // ── Teardown ─────────────────────────────────────────────────────────────── test('a teardown deletes the channel and the role together', async () => { const channel = voiceChannel('600') const teamRole = role('900') let deletedChannel = false let deletedRole = false channel.delete = async () => { deletedChannel = true } teamRole.delete = async () => { deletedRole = true } const guild = fakeGuild({ channels: [channel], roles: [teamRole] }) const result = await teamVoice.removeTeamVoice(fakeClient(guild), 'guild-1', { channelId: '600', roleId: '900' }) assert.equal(deletedChannel, true) assert.equal(deletedRole, true) assert.equal(result.channel_deleted, true) assert.equal(result.role_deleted, true) }) test('a teardown whose target is already gone is success, not a failure to retry forever', async () => { const guild = fakeGuild({ channels: [], roles: [] }) const result = await teamVoice.removeTeamVoice(fakeClient(guild), 'guild-1', { channelId: 'gone', roleId: 'gone' }) assert.equal(result.channel_deleted, false) assert.equal(result.role_deleted, false) }) // ── The whole thing ──────────────────────────────────────────────────────── test('a first sync creates the category, the role and the channel, and grants the members', async () => { const alice = fakeMember('a') const guild = fakeGuild({ members: [alice] }) const result = await teamVoice.syncTeamVoice(fakeClient(guild), 'guild-1', { teamId: 1, name: 'The Silver Hand', categoryId: null, channelId: null, roleId: null, staffRoleIds: [], memberIds: ['a'], maxMemberOps: 50, }) assert.equal(result.created.channel, true) assert.equal(result.created.role, true) assert.ok(result.category_id) assert.ok(result.channel_id) assert.ok(result.role_id) assert.equal(result.members.added, 1) })