From 17b40c4a594923afbc947bd1a7d9238e78671ff1 Mon Sep 17 00:00:00 2001 From: Mitch McAlister Date: Tue, 3 Mar 2026 00:49:01 +0000 Subject: [PATCH] fix: guard isConnected check against already-aborted signal When abortSignal is already aborted at lifecycle start, onAbort() fires synchronously and pushes connected: false. Without a lifecycleStopping guard, the subsequent gateway.isConnected check could push a spurious connected: true, contradicting the shutdown. Adds !lifecycleStopping to the isConnected guard and a test verifying no connected: true is emitted when the signal is pre-aborted. Co-Authored-By: Claude Opus 4.6 --- .../monitor/provider.lifecycle.test.ts | 36 +++++++++++++++++++ src/discord/monitor/provider.lifecycle.ts | 5 ++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/discord/monitor/provider.lifecycle.test.ts b/src/discord/monitor/provider.lifecycle.test.ts index 278a6b257be..d7aedf30d00 100644 --- a/src/discord/monitor/provider.lifecycle.test.ts +++ b/src/discord/monitor/provider.lifecycle.test.ts @@ -408,4 +408,40 @@ describe("runDiscordGatewayLifecycle", () => { vi.useRealTimers(); } }); + + it("does not push connected: true when abortSignal is already aborted", async () => { + const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js"); + const emitter = new EventEmitter(); + const gateway = { + isConnected: true, + options: { reconnect: { maxAttempts: 3 } }, + disconnect: vi.fn(), + connect: vi.fn(), + emitter, + }; + getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter); + + const abortController = new AbortController(); + abortController.abort(); + + const statusUpdates: Array> = []; + const statusSink = (patch: Record) => { + statusUpdates.push({ ...patch }); + }; + + const { lifecycleParams } = createLifecycleHarness({ gateway }); + lifecycleParams.abortSignal = abortController.signal; + (lifecycleParams as Record).statusSink = statusSink; + + await expect(runDiscordGatewayLifecycle(lifecycleParams)).resolves.toBeUndefined(); + + // onAbort should have pushed connected: false + const connectedFalse = statusUpdates.find((s) => s.connected === false); + expect(connectedFalse).toBeDefined(); + + // No connected: true should appear — the isConnected check must be + // guarded by !lifecycleStopping to avoid contradicting the abort. + const connectedTrue = statusUpdates.find((s) => s.connected === true); + expect(connectedTrue).toBeUndefined(); + }); }); diff --git a/src/discord/monitor/provider.lifecycle.ts b/src/discord/monitor/provider.lifecycle.ts index 23e414840cb..6291d09a7b2 100644 --- a/src/discord/monitor/provider.lifecycle.ts +++ b/src/discord/monitor/provider.lifecycle.ts @@ -247,7 +247,10 @@ export async function runDiscordGatewayLifecycle(params: { // If the gateway is already connected when the lifecycle starts (the // "WebSocket connection opened" debug event was emitted before we // registered the listener above), push the initial connected status now. - if (gateway?.isConnected) { + // Guard against lifecycleStopping: if the abortSignal was already aborted, + // onAbort() ran synchronously above and pushed connected: false — don't + // contradict it with a spurious connected: true. + if (gateway?.isConnected && !lifecycleStopping) { const at = Date.now(); pushStatus({ connected: true,