mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(discord): normalize gateway fatal type errors
This commit is contained in:
@@ -89,7 +89,7 @@ describe("waitForDiscordGatewayStop", () => {
|
||||
|
||||
emitGatewayEvent(fatalEvent);
|
||||
|
||||
await expect(promise).rejects.toThrow("boom");
|
||||
await expect(promise).rejects.toThrow("discord gateway fatal: Error: boom");
|
||||
expect(disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(detachLifecycle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -178,7 +178,7 @@ describe("waitForDiscordGatewayStop", () => {
|
||||
|
||||
emitGatewayEvent(firstEvent);
|
||||
|
||||
await expect(promise).rejects.toThrow("first failure");
|
||||
await expect(promise).rejects.toThrow("discord gateway fatal: Error: first failure");
|
||||
expect(seenEvents.map((event) => event.message)).toEqual([
|
||||
firstEvent.message,
|
||||
secondEvent.message,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { EventEmitter } from "node:events";
|
||||
import type { DiscordGatewayHandle } from "./monitor/gateway-handle.js";
|
||||
import type {
|
||||
import {
|
||||
DiscordGatewayEvent,
|
||||
DiscordGatewayLifecycleError,
|
||||
DiscordGatewaySupervisor,
|
||||
} from "./monitor/gateway-supervisor.js";
|
||||
|
||||
@@ -59,7 +60,7 @@ export async function waitForDiscordGatewayStop(
|
||||
const onGatewayEvent = (event: DiscordGatewayEvent) => {
|
||||
const shouldStop = (params.onGatewayEvent?.(event) ?? "stop") === "stop";
|
||||
if (shouldStop) {
|
||||
finishReject(event.err);
|
||||
finishReject(new DiscordGatewayLifecycleError(event));
|
||||
}
|
||||
};
|
||||
const onForceStop = (err: unknown) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
classifyDiscordGatewayEvent,
|
||||
DiscordGatewayLifecycleError,
|
||||
createDiscordGatewaySupervisor,
|
||||
} from "./gateway-supervisor.js";
|
||||
|
||||
@@ -32,6 +33,20 @@ describe("classifyDiscordGatewayEvent", () => {
|
||||
expect(transientEvent.message).toBe("TypeError");
|
||||
expect(transientEvent.shouldStopLifecycle).toBe(true);
|
||||
});
|
||||
|
||||
it("wraps fatal lifecycle stops with discord-specific context", () => {
|
||||
const event = classifyDiscordGatewayEvent({
|
||||
err: new TypeError(),
|
||||
isDisallowedIntentsError: () => false,
|
||||
});
|
||||
|
||||
const wrapped = new DiscordGatewayLifecycleError(event);
|
||||
|
||||
expect(wrapped.name).toBe("DiscordGatewayLifecycleError");
|
||||
expect(wrapped.message).toBe("discord gateway fatal: TypeError");
|
||||
expect(wrapped.eventType).toBe("fatal");
|
||||
expect(wrapped.cause).toBeInstanceOf(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDiscordGatewaySupervisor", () => {
|
||||
@@ -101,4 +116,23 @@ describe("createDiscordGatewaySupervisor", () => {
|
||||
expect.stringContaining("suppressed late gateway reconnect-exhausted error after dispose"),
|
||||
);
|
||||
});
|
||||
|
||||
it("dedupes identical late gateway errors after dispose", () => {
|
||||
const emitter = new EventEmitter();
|
||||
const runtime = { error: vi.fn() };
|
||||
const supervisor = createDiscordGatewaySupervisor({
|
||||
gateway: { emitter },
|
||||
isDisallowedIntentsError: () => false,
|
||||
runtime: runtime as never,
|
||||
});
|
||||
|
||||
supervisor.dispose();
|
||||
emitter.emit("error", new TypeError());
|
||||
emitter.emit("error", new TypeError());
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("suppressed late gateway fatal error after dispose: TypeError"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,18 @@ export type DiscordGatewayEvent = {
|
||||
shouldStopLifecycle: boolean;
|
||||
};
|
||||
|
||||
export class DiscordGatewayLifecycleError extends Error {
|
||||
readonly eventType: DiscordGatewayEventType;
|
||||
|
||||
constructor(event: Pick<DiscordGatewayEvent, "type" | "message" | "err">) {
|
||||
super(`discord gateway ${event.type}: ${event.message}`, {
|
||||
cause: event.err instanceof Error ? event.err : undefined,
|
||||
});
|
||||
this.name = "DiscordGatewayLifecycleError";
|
||||
this.eventType = event.type;
|
||||
}
|
||||
}
|
||||
|
||||
export type DiscordGatewaySupervisor = {
|
||||
emitter?: EventEmitter;
|
||||
attachLifecycle: (handler: (event: DiscordGatewayEvent) => void) => void;
|
||||
@@ -97,9 +109,15 @@ export function createDiscordGatewaySupervisor(params: {
|
||||
|
||||
let lifecycleHandler: ((event: DiscordGatewayEvent) => void) | undefined;
|
||||
let phase: GatewaySupervisorPhase = "buffering";
|
||||
const seenLateEventKeys = new Set<string>();
|
||||
const logLateEvent =
|
||||
(state: Extract<GatewaySupervisorPhase, "disposed" | "teardown">) =>
|
||||
(event: DiscordGatewayEvent) => {
|
||||
const key = `${state}:${event.type}:${event.message}`;
|
||||
if (seenLateEventKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
seenLateEventKeys.add(key);
|
||||
params.runtime.error?.(
|
||||
danger(
|
||||
`discord: suppressed late gateway ${event.type} error ${
|
||||
|
||||
@@ -383,7 +383,7 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
});
|
||||
|
||||
await expect(runDiscordGatewayLifecycle(lifecycleParams)).rejects.toThrow(
|
||||
"Fatal Gateway error: 4000",
|
||||
"discord gateway fatal: Error: Fatal Gateway error: 4000",
|
||||
);
|
||||
|
||||
expectLifecycleCleanup({
|
||||
@@ -406,7 +406,7 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
});
|
||||
|
||||
await expect(runDiscordGatewayLifecycle(lifecycleParams)).rejects.toThrow(
|
||||
"Max reconnect attempts (50) reached after code 1005",
|
||||
"discord gateway reconnect-exhausted: Error: Max reconnect attempts (50) reached after code 1005",
|
||||
);
|
||||
|
||||
expectLifecycleCleanup({
|
||||
@@ -438,9 +438,11 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
lifecyclePromise.catch(() => {});
|
||||
await vi.advanceTimersByTimeAsync(1_500);
|
||||
|
||||
await expect(lifecyclePromise).rejects.toThrow("Fatal Gateway error: 4001");
|
||||
await expect(lifecyclePromise).rejects.toThrow(
|
||||
"discord gateway fatal: Error: Fatal Gateway error: 4001",
|
||||
);
|
||||
expect(runtimeError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("discord gateway error: Error: Fatal Gateway error: 4001"),
|
||||
expect.stringContaining("discord gateway fatal: Error: Fatal Gateway error: 4001"),
|
||||
);
|
||||
expect(gateway.disconnect).not.toHaveBeenCalled();
|
||||
expect(gateway.connect).not.toHaveBeenCalled();
|
||||
|
||||
@@ -6,7 +6,11 @@ import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.
|
||||
import type { DiscordVoiceManager } from "../voice/manager.js";
|
||||
import type { MutableDiscordGateway } from "./gateway-handle.js";
|
||||
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
|
||||
import type { DiscordGatewayEvent, DiscordGatewaySupervisor } from "./gateway-supervisor.js";
|
||||
import {
|
||||
DiscordGatewayLifecycleError,
|
||||
type DiscordGatewayEvent,
|
||||
type DiscordGatewaySupervisor,
|
||||
} from "./gateway-supervisor.js";
|
||||
import type { DiscordMonitorStatusSink } from "./status.js";
|
||||
|
||||
const DISCORD_GATEWAY_READY_TIMEOUT_MS = 15_000;
|
||||
@@ -401,7 +405,13 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
if (event.shouldStopLifecycle) {
|
||||
lifecycleStopping = true;
|
||||
}
|
||||
params.runtime.error?.(danger(`discord gateway error: ${event.message}`));
|
||||
params.runtime.error?.(
|
||||
danger(
|
||||
event.shouldStopLifecycle
|
||||
? `discord gateway ${event.type}: ${event.message}`
|
||||
: `discord gateway error: ${event.message}`,
|
||||
),
|
||||
);
|
||||
return event.shouldStopLifecycle ? "stop" : "continue";
|
||||
};
|
||||
const drainPendingGatewayErrors = (): "continue" | "stop" =>
|
||||
@@ -413,7 +423,7 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
if (event.type === "disallowed-intents") {
|
||||
return "stop";
|
||||
}
|
||||
throw event.err;
|
||||
throw new DiscordGatewayLifecycleError(event);
|
||||
});
|
||||
try {
|
||||
if (params.execApprovalsHandler) {
|
||||
|
||||
Reference in New Issue
Block a user