From c7b9e9b9bd7e06e7d6f3efbf18a594acba4d34ef Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 12 Sep 2026 15:38:50 +0200 Subject: [PATCH] fix(voice): stop room_finished evicting a whole channel (#2720) --- .../infrastructure/LiveKitWebhookService.ts | 19 ------ .../tests/LiveKitWebhookService.test.ts | 63 +++++++++++++++++++ 2 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts diff --git a/fluxer_api/src/api/infrastructure/LiveKitWebhookService.ts b/fluxer_api/src/api/infrastructure/LiveKitWebhookService.ts index 579782b2f..096338968 100644 --- a/fluxer_api/src/api/infrastructure/LiveKitWebhookService.ts +++ b/fluxer_api/src/api/infrastructure/LiveKitWebhookService.ts @@ -185,25 +185,6 @@ export class LiveKitWebhookService { {guildId: context.guildId.toString(), channelId: context.channelId.toString()}, 'Cleared guild voice room server pinning', ); - try { - const result = await this.gatewayService.disconnectAllVoiceUsersInChannel({ - guildId: context.guildId, - channelId: context.channelId, - }); - Logger.info( - { - guildId: context.guildId.toString(), - channelId: context.channelId.toString(), - disconnectedCount: result.disconnectedCount, - }, - 'Cleaned up zombie voice connections for finished room', - ); - } catch (error) { - Logger.error( - {error, guildId: context.guildId.toString(), channelId: context.channelId.toString()}, - 'Failed to clean up voice connections for finished room', - ); - } } } diff --git a/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts b/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts new file mode 100644 index 000000000..a0374f80c --- /dev/null +++ b/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {WebhookEvent} from 'livekit-server-sdk'; +import {describe, expect, it, vi} from 'vitest'; +import type {ChannelID, GuildID} from '../../BrandedTypes'; +import {createChannelID, createGuildID} from '../../BrandedTypes'; +import {VoiceTopology} from '../../voice/VoiceTopology'; +import type {IGatewayService} from '../IGatewayService'; +import type {ILiveKitService} from '../ILiveKitService'; +import type {IVoiceRoomStore} from '../IVoiceRoomStore'; +import {LiveKitWebhookService} from '../LiveKitWebhookService'; + +const GUILD_ID = createGuildID(1n); +const CHANNEL_ID = createChannelID(2n); + +function roomFinished(name: string): WebhookEvent { + return { + event: 'room_finished', + room: {name, sid: 'RM_test', emptyTimeout: 300, creationTime: 0n}, + } as unknown as WebhookEvent; +} + +function harness(pinnedServerId: string | null) { + const deleteRoomServer = vi.fn(async () => {}); + const disconnectAllVoiceUsersInChannel = vi.fn(async () => ({disconnectedCount: 0})); + const voiceRoomStore = { + getPinnedRoomServer: vi.fn(async () => (pinnedServerId ? {regionId: 'eu', serverId: pinnedServerId} : null)), + deleteRoomServer, + } as unknown as IVoiceRoomStore; + const gatewayService = {disconnectAllVoiceUsersInChannel} as unknown as IGatewayService; + const liveKitService = {} as unknown as ILiveKitService; + const service = new LiveKitWebhookService(voiceRoomStore, gatewayService, liveKitService, new VoiceTopology()); + return {service, deleteRoomServer, disconnectAllVoiceUsersInChannel}; +} + +describe('LiveKitWebhookService room_finished', () => { + it('clears the guild pin without disconnecting anybody', async () => { + const {service, deleteRoomServer, disconnectAllVoiceUsersInChannel} = harness('eu-1'); + + await service.handleRoomFinished(roomFinished(`guild_${GUILD_ID}_channel_${CHANNEL_ID}`), 'unknown-key'); + + expect(deleteRoomServer).toHaveBeenCalledTimes(1); + expect(disconnectAllVoiceUsersInChannel).not.toHaveBeenCalled(); + }); + + it('clears the pin even when no server is pinned, and still disconnects nobody', async () => { + const {service, deleteRoomServer, disconnectAllVoiceUsersInChannel} = harness(null); + + await service.handleRoomFinished(roomFinished(`guild_${GUILD_ID}_channel_${CHANNEL_ID}`), 'unknown-key'); + + expect(deleteRoomServer).toHaveBeenCalledTimes(1); + expect(disconnectAllVoiceUsersInChannel).not.toHaveBeenCalled(); + }); + + it('ignores a room name it cannot parse', async () => { + const {service, deleteRoomServer, disconnectAllVoiceUsersInChannel} = harness('eu-1'); + + await service.handleRoomFinished(roomFinished('not_a_voice_room'), 'unknown-key'); + + expect(deleteRoomServer).not.toHaveBeenCalled(); + expect(disconnectAllVoiceUsersInChannel).not.toHaveBeenCalled(); + }); +});