From c51c222c479ba26297e0b40c9b55861b2a8b00a0 Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 8 Oct 2026 03:04:10 +0200 Subject: [PATCH] fix(voice): stop stale mute echoes from toggling mute (#3268) --- .../core/reducers/gateway.reconcile.test.ts | 53 +++++++++++++++++++ .../src/core/reducers/gateway.ts | 7 +-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/voice_engine_v2/src/core/reducers/gateway.reconcile.test.ts b/packages/voice_engine_v2/src/core/reducers/gateway.reconcile.test.ts index 4bd26a8d8..2e9303a29 100644 --- a/packages/voice_engine_v2/src/core/reducers/gateway.reconcile.test.ts +++ b/packages/voice_engine_v2/src/core/reducers/gateway.reconcile.test.ts @@ -265,6 +265,7 @@ describe('shouldApplyGatewayVoiceStateEcho', () => { it('applies the echo when it matches the pending local write', () => { const snapshot = connectedSnapshot({ + desiredVoiceState: desired({selfMute: false}), desiredVoiceStateWrite: { guildId: 'guild-1', channelId: 'channel-1', @@ -279,6 +280,7 @@ describe('shouldApplyGatewayVoiceStateEcho', () => { it('rejects a stale echo that differs from the pending local write', () => { const snapshot = connectedSnapshot({ + desiredVoiceState: desired({selfMute: false}), desiredVoiceStateWrite: { guildId: 'guild-1', channelId: 'channel-1', @@ -290,4 +292,55 @@ describe('shouldApplyGatewayVoiceStateEcho', () => { }); expect(shouldApplyGatewayVoiceStateEcho(snapshot, reported({selfMute: true}))).toBe(false); }); + + it('rejects the echo of a pending write the user has already reverted', () => { + const snapshot = connectedSnapshot({ + selfVoiceState: reported({selfMute: false}), + desiredVoiceState: desired({selfMute: false}), + desiredVoiceStateWrite: { + guildId: 'guild-1', + channelId: 'channel-1', + selfMute: true, + selfDeaf: false, + selfVideo: false, + selfStream: false, + }, + }); + expect(shouldApplyGatewayVoiceStateEcho(snapshot, reported({selfMute: true}))).toBe(false); + }); + + it('settles on the last intent after a mute and unmute faster than the echo', () => { + let snapshot = connectedSnapshot({selfVoiceState: reported({selfMute: false})}); + const muted = transitionVoiceEngineV2(snapshot, { + type: 'gateway.desiredVoiceStateChanged', + desired: desired({selfMute: true}), + }); + expect(muted.commands).toMatchObject([{type: 'gateway.voiceState.write', options: {selfMute: true}}]); + snapshot = transitionVoiceEngineV2(muted.snapshot, { + type: 'gateway.voiceStateWriteSucceeded', + operationId: muted.snapshot.gateway.operationId as number, + }).snapshot; + const unmuted = transitionVoiceEngineV2(snapshot, { + type: 'gateway.desiredVoiceStateChanged', + desired: desired({selfMute: false}), + }); + expect(unmuted.commands).toEqual([]); + snapshot = unmuted.snapshot; + + const staleEcho = reported({selfMute: true}); + expect(shouldApplyGatewayVoiceStateEcho(snapshot, staleEcho)).toBe(false); + snapshot = transitionVoiceEngineV2(snapshot, {type: 'gateway.voiceStateUpdated', voiceState: staleEcho}).snapshot; + const corrective = transitionVoiceEngineV2(snapshot, {type: 'gateway.voiceStateReconcileRequested'}); + expect(corrective.commands).toMatchObject([{type: 'gateway.voiceState.write', options: {selfMute: false}}]); + snapshot = transitionVoiceEngineV2(corrective.snapshot, { + type: 'gateway.voiceStateWriteSucceeded', + operationId: corrective.snapshot.gateway.operationId as number, + }).snapshot; + + const finalEcho = reported({selfMute: false}); + expect(shouldApplyGatewayVoiceStateEcho(snapshot, finalEcho)).toBe(true); + snapshot = transitionVoiceEngineV2(snapshot, {type: 'gateway.voiceStateUpdated', voiceState: finalEcho}).snapshot; + expect(snapshot.gateway.desiredVoiceStateWrite).toBeNull(); + expect(transitionVoiceEngineV2(snapshot, {type: 'gateway.voiceStateReconcileRequested'}).commands).toEqual([]); + }); }); diff --git a/packages/voice_engine_v2/src/core/reducers/gateway.ts b/packages/voice_engine_v2/src/core/reducers/gateway.ts index 7423d2d64..a4f6efb57 100644 --- a/packages/voice_engine_v2/src/core/reducers/gateway.ts +++ b/packages/voice_engine_v2/src/core/reducers/gateway.ts @@ -226,9 +226,10 @@ export function shouldApplyGatewayVoiceStateEcho( ): boolean { assert.ok(snapshot != null, 'shouldApplyGatewayVoiceStateEcho snapshot must not be null'); assert.ok(echo != null, 'shouldApplyGatewayVoiceStateEcho echo must not be null'); - const pending = snapshot.gateway.desiredVoiceStateWrite; - if (pending == null) return true; - return gatewayVoiceStateWriteMatchesReported(pending, echo); + if (snapshot.gateway.desiredVoiceStateWrite == null) return true; + const desired = deriveVoiceEngineV2DesiredGatewayVoiceState(snapshot); + if (desired == null) return false; + return gatewayVoiceStateWriteMatchesReported(desired, echo); } export function gatewayVoiceStateWriteMatchesReported(