// The voice reconciler — what actually reaches Discord (TEAMS.md §7.3, phase 9). // // teamVoice.test.js proves the rules; this proves the pass that applies them, // which is a different set of mistakes: // // 1. **The three suspensions.** Voice off, a stale Team projection, or a bot // that cannot act each stop the pass ENTIRELY — in both directions. The // stale one is §7.3 verbatim and is the whole reason the file is careful: a // sidecar that has been down for an hour reports rosters that look exactly // like "every Team lost its members", and a voice channel must never be // destroyed because a sidecar was down. // 2. **A per-Team failure does not abort the pass.** One Team whose channel a // human deleted is one Team's problem, the same shape as §2.4's gate 3. // 3. **A failed sync does not clear the refs it could not confirm.** Clearing // them would orphan a real channel and make the next pass create a second. // 4. **A failed teardown does not extend the window.** Granting another seven // days every time a delete fails means it never happens. // 5. **The role cap is checked per create.** A pass with headroom for three // Teams must stop after the third rather than discover it in a rejection. // 6. **The membership grant is the hop-3 set** — a Team member with no Discord // identity cannot be handed a role, so the query, not `linked_count`, is what // the pass sends. const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') const sync = require('../src/utils/teamVoiceSync') const voice = require('../src/model/teams/teamVoice.model') const voiceDb = require('../src/model/teams/teamVoice.db') const settings = require('../src/model/teams/teamVoiceSettings.model') const teamsModel = require('../src/model/teams/teams.model') const botClient = require('../src/utils/botInternalClient') const saved = new Map() function patch(mod, name, fn) { if (!saved.has(mod)) saved.set(mod, new Map()) if (!saved.get(mod).has(name)) saved.get(mod).set(name, mod[name]) mod[name] = fn } function restore() { for (const [mod, names] of saved) for (const [name, fn] of names) mod[name] = fn saved.clear() } let plan let recorded let forgotten let calls let stale let flight const okPreflight = { connected: true, can_manage_channels: true, can_manage_roles: true, role_count: 12, bot_role_position: 5, } beforeEach(() => { sync._reset() recorded = [] forgotten = [] calls = { sync: [], remove: [], preflight: 0 } stale = false flight = { ok: true, status: 200, data: { ...okPreflight } } plan = { config: { enabled: true, minMembers: 5, graceDays: 7, categoryRef: '500', staffRoles: [] }, provision: [], scheduled: [], removals: [] } patch(voice, 'plan', async () => plan) patch(voice, 'memberRefs', async () => ['111111111111111111']) patch(voice, 'record', async (row) => { recorded.push(row); return row }) patch(voice, 'forget', async (teamId) => { forgotten.push(teamId) }) patch(voice, 'getForTeam', async () => null) patch(teamsModel, 'syncStatus', async () => ({ stale, lastSyncAt: new Date(), configured: true })) patch(settings, 'setCategoryRef', async () => {}) patch(botClient, 'voicePreflight', async () => { calls.preflight += 1; return flight }) patch(botClient, 'voiceSync', async (body) => { calls.sync.push(body) return { ok: true, status: 200, data: { category_id: body.categoryRef || '500', channel_id: '900', role_id: '901', created: { channel: !body.channelRef, role: !body.roleRef }, members: { added: 1, removed: 0, pending: 0 }, }, } }) patch(botClient, 'voiceRemove', async (body) => { calls.remove.push(body); return { ok: true, status: 200, data: {} } }) }) afterEach(restore) const item = (over = {}) => ({ team: { team_id: 1, external_ref: null, role_ref: null, remove_after: null, synced_at: null, ...over.team }, name: 'The Silver Hand', hasRow: false, recovering: false, ...over, }) // ── The three suspensions ────────────────────────────────────────────────── test('voice off: the pass does not run, and makes no calls in either direction', async () => { plan = null const result = await sync.runOnce('test') assert.equal(result.ran, false) assert.equal(calls.preflight, 0) assert.equal(calls.sync.length, 0) assert.equal(calls.remove.length, 0) }) test('a stale Team projection stops the pass before a single Discord call', async () => { stale = true plan.provision = [item()] plan.removals = [{ team: { team_id: 2, external_ref: '900', role_ref: '901' }, reason: 'below_threshold' }] const result = await sync.runOnce('test') assert.equal(result.ran, false) assert.match(result.reason, /stale/) // The removal half is the one that matters: nothing is destroyed on data core // does not trust. assert.equal(calls.remove.length, 0) assert.equal(calls.sync.length, 0) assert.equal(recorded.length, 0) }) test('a bot missing Manage Roles stops the pass once, not forty times', async () => { flight = { ok: true, status: 200, data: { ...okPreflight, can_manage_roles: false } } plan.provision = [item(), item({ team: { team_id: 2 } })] const result = await sync.runOnce('test') assert.equal(result.ran, false) assert.match(result.reason, /Manage Roles/) // No per-Team error rows: this is one deployment misconfiguration, and writing // it into every Team's last_error would bury the one fact that matters. assert.equal(recorded.length, 0) }) test('a bot that is not connected reads as not connected, not as a permission problem', async () => { flight = { ok: false, status: 503, error: 'bot responded 503' } const result = await sync.preflight() assert.equal(result.ready, false) assert.match(result.reason, /not connected/) }) // ── Provisioning ─────────────────────────────────────────────────────────── test('a new Team is created, and the pass sends the hop-3 member set', async () => { patch(voice, 'memberRefs', async () => ['111111111111111111', '222222222222222222']) plan.provision = [item()] const result = await sync.runOnce('test') assert.equal(result.ran, true) assert.equal(result.synced, 1) assert.equal(result.created, 1) assert.deepEqual(calls.sync[0].memberRefs, ['111111111111111111', '222222222222222222']) assert.equal(recorded[0].state, 'active') assert.equal(recorded[0].channelRef, '900') assert.equal(recorded[0].roleRef, '901') }) test('a recovering Team has its removal window cleared', async () => { plan.provision = [item({ recovering: true, team: { team_id: 1, external_ref: '900', role_ref: '901', remove_after: '2026-08-30T00:00:00Z' }, })] await sync.runOnce('test') assert.equal(recorded[0].state, 'active') assert.equal(recorded[0].removeAfter, null) }) test('a failed sync records the error and KEEPS the refs it could not confirm', async () => { patch(botClient, 'voiceSync', async () => ({ ok: false, status: 400, data: { message: 'Missing Access' } })) plan.provision = [item({ team: { team_id: 1, external_ref: '900', role_ref: '901' } })] const result = await sync.runOnce('test') assert.equal(result.failed, 1) assert.equal(recorded[0].state, 'error') assert.equal(recorded[0].lastError, 'Missing Access') // Cleared refs would orphan a real channel and make the next pass build a second. assert.equal(recorded[0].channelRef, '900') assert.equal(recorded[0].roleRef, '901') }) test('one Team failing does not stop the others', async () => { let n = 0 patch(botClient, 'voiceSync', async (body) => { n += 1 if (n === 1) return { ok: false, status: 400, data: { message: 'Missing Access' } } return { ok: true, status: 200, data: { channel_id: '9', role_id: '8', created: {}, members: { pending: 0 } } } }) plan.provision = [item(), item({ team: { team_id: 2 } }), item({ team: { team_id: 3 } })] const result = await sync.runOnce('test') assert.equal(result.failed, 1) assert.equal(result.synced, 2) }) test('the role cap is enforced per create, before Discord is asked', async () => { flight = { ok: true, status: 200, data: { ...okPreflight, role_count: settings.ROLE_CAP - 1 } } plan.provision = [item(), item({ team: { team_id: 2 } })] const result = await sync.runOnce('test') assert.equal(result.created, 1) assert.equal(result.failed, 1) const capped = recorded.find((r) => r.state === 'error') assert.match(capped.lastError, /limit of 250 roles/) // The second Team was never handed to the bot. assert.equal(calls.sync.length, 1) }) test('a Team that already HAS a role is synced even at the cap — the cap gates creates, not updates', async () => { flight = { ok: true, status: 200, data: { ...okPreflight, role_count: settings.ROLE_CAP } } plan.provision = [item({ team: { team_id: 1, external_ref: '900', role_ref: '901' } })] const result = await sync.runOnce('test') assert.equal(result.synced, 1) assert.equal(result.failed, 0) }) test('a category the bot had to create is persisted, so the next pass does not make another', async () => { let stored = null patch(settings, 'setCategoryRef', async (value) => { stored = value }) patch(botClient, 'voiceSync', async () => ({ ok: true, status: 200, data: { category_id: '777', channel_id: '900', role_id: '901', created: { channel: true, role: true }, members: { pending: 0 } }, })) plan.config.categoryRef = null plan.provision = [item()] await sync.runOnce('test') assert.equal(stored, '777') }) test('a truncated membership diff asks for another pass rather than waiting out the interval', async () => { patch(botClient, 'voiceSync', async () => ({ ok: true, status: 200, data: { channel_id: '900', role_id: '901', created: {}, members: { added: 50, removed: 0, pending: 30 } }, })) plan.provision = [item()] const result = await sync.runOnce('test') assert.equal(result.pendingMemberOps, 30) }) // ── Removal ──────────────────────────────────────────────────────────────── test('a scheduled removal writes the window and touches nothing in Discord', async () => { const removeAfter = new Date('2026-08-26T00:00:00Z') plan.scheduled = [{ team: { team_id: 4, external_ref: '900', role_ref: '901', synced_at: null }, removeAfter, reason: 'below_threshold' }] await sync.runOnce('test') assert.equal(calls.remove.length, 0) assert.equal(recorded[0].state, 'pending_removal') assert.equal(recorded[0].removeAfter, removeAfter) }) test('an expired removal deletes the channel AND the role, then forgets the row', async () => { plan.removals = [{ team: { team_id: 4, external_ref: '900', role_ref: '901' }, reason: 'below_threshold' }] const result = await sync.runOnce('test') assert.equal(result.removed, 1) assert.deepEqual(calls.remove[0], { channelRef: '900', roleRef: '901' }) assert.deepEqual(forgotten, [4]) }) test('a failed teardown keeps the expired window instead of granting another seven days', async () => { patch(botClient, 'voiceRemove', async () => ({ ok: false, status: 0, error: 'fetch failed' })) const expired = '2026-08-18T00:00:00Z' plan.removals = [{ team: { team_id: 4, external_ref: '900', role_ref: '901', remove_after: expired }, reason: 'archived' }] const result = await sync.runOnce('test') assert.equal(result.failed, 1) assert.equal(result.removed, 0) assert.equal(recorded[0].state, 'error') assert.equal(recorded[0].removeAfter, expired) // The row survives, so the next pass retries the same teardown. assert.deepEqual(forgotten, []) }) // ── The admin's own removal ──────────────────────────────────────────────── test('an admin removal ignores the grace window entirely', async () => { patch(voice, 'getForTeam', async () => ({ external_ref: '900', role_ref: '901', remove_after: null })) const result = await sync.removeNow(7) assert.equal(result.ok, true) assert.deepEqual(calls.remove[0], { channelRef: '900', roleRef: '901' }) assert.deepEqual(forgotten, [7]) }) test('removing a Team that has no channel is a 404, not a silent success', async () => { const result = await sync.removeNow(7) assert.equal(result.ok, false) assert.equal(result.status, 404) }) // ── The pass never throws ────────────────────────────────────────────────── test('a pass that throws is reported, not raised — it hangs off a background timer', async () => { patch(voice, 'plan', async () => { throw new Error('database is on fire') }) const result = await sync.passNow('test') assert.equal(result.ran, false) assert.equal(result.reason, 'database is on fire') assert.equal(sync.lastPass().ran, false) }) test('a pass records what it concluded, for the panel', async () => { plan.provision = [item()] await sync.passNow('test') const last = sync.lastPass() assert.equal(last.ran, true) assert.equal(last.synced, 1) assert.ok(last.at instanceof Date) }) test('the db layer is untouched by these tests — the queries are proved by their own file', () => { // A guard against a future edit here reaching the real db module: every test in // this file stubs the model, and one that did not would connect to the dead port // the harness pins and hang. assert.equal(typeof voiceDb.desiredTeams, 'function') })