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 <noreply@anthropic.com>
This commit is contained in:
Mitch McAlister
2026-03-03 00:49:01 +00:00
committed by Peter Steinberger
parent d9119f0791
commit 17b40c4a59
2 changed files with 40 additions and 1 deletions

View File

@@ -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<Record<string, unknown>> = [];
const statusSink = (patch: Record<string, unknown>) => {
statusUpdates.push({ ...patch });
};
const { lifecycleParams } = createLifecycleHarness({ gateway });
lifecycleParams.abortSignal = abortController.signal;
(lifecycleParams as Record<string, unknown>).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();
});
});

View File

@@ -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,