diff --git a/extensions/discord/src/monitor/provider.lifecycle.test.ts b/extensions/discord/src/monitor/provider.lifecycle.test.ts index e4aed37362e..36dd78d506d 100644 --- a/extensions/discord/src/monitor/provider.lifecycle.test.ts +++ b/extensions/discord/src/monitor/provider.lifecycle.test.ts @@ -101,17 +101,19 @@ describe("runDiscordGatewayLifecycle", () => { } function createLifecycleHarness(params?: { - gateway?: MockGateway; + gateway?: MockGateway | null; isDisallowedIntentsError?: (err: unknown) => boolean; pendingGatewayEvents?: DiscordGatewayEvent[]; }) { const gateway = - params?.gateway ?? - (() => { - const defaultGateway = createGatewayHarness().gateway; - defaultGateway.isConnected = true; - return defaultGateway; - })(); + params && "gateway" in params + ? params.gateway + : (() => { + const defaultGateway = createGatewayHarness().gateway; + defaultGateway.isConnected = true; + return defaultGateway; + })(); + const gatewayEmitter = gateway?.emitter ?? new EventEmitter(); const threadStop = vi.fn(); const runtimeLog = vi.fn(); const runtimeError = vi.fn(); @@ -130,7 +132,7 @@ describe("runDiscordGatewayLifecycle", () => { return "continue"; }), dispose: vi.fn(), - emitter: gateway.emitter, + emitter: gatewayEmitter, }; const statusSink = vi.fn(); const runtime: RuntimeEnv = { @@ -146,7 +148,7 @@ describe("runDiscordGatewayLifecycle", () => { statusSink, lifecycleParams: { accountId: "default", - gateway: gateway as unknown as MutableDiscordGateway, + gateway: gateway ? (gateway as unknown as MutableDiscordGateway) : undefined, runtime, isDisallowedIntentsError: params?.isDisallowedIntentsError ?? (() => false), voiceManager: null, @@ -215,6 +217,38 @@ describe("runDiscordGatewayLifecycle", () => { ); }); + it("does not treat a missing gateway handle as ready", async () => { + vi.useFakeTimers(); + try { + const { lifecycleParams, threadStop, statusSink, gatewaySupervisor } = createLifecycleHarness( + { + gateway: null, + }, + ); + + const lifecyclePromise = runDiscordGatewayLifecycle(lifecycleParams); + lifecyclePromise.catch(() => {}); + await vi.advanceTimersByTimeAsync(0); + await vi.advanceTimersByTimeAsync(15_500); + + await expect(lifecyclePromise).rejects.toThrow( + "discord gateway did not reach READY within 15000ms", + ); + expect(statusSink).not.toHaveBeenCalledWith( + expect.objectContaining({ + connected: true, + }), + ); + expectLifecycleCleanup({ + threadStop, + waitCalls: 0, + gatewaySupervisor, + }); + } finally { + vi.useRealTimers(); + } + }); + it("restarts the gateway once when startup never reaches READY, then recovers", async () => { vi.useFakeTimers(); try { diff --git a/extensions/discord/src/monitor/provider.lifecycle.ts b/extensions/discord/src/monitor/provider.lifecycle.ts index 800f583b8cd..944dbda5a9f 100644 --- a/extensions/discord/src/monitor/provider.lifecycle.ts +++ b/extensions/discord/src/monitor/provider.lifecycle.ts @@ -287,7 +287,7 @@ async function waitForGatewayReady(params: { if ((await params.beforePoll?.()) === "stop") { return "stopped"; } - if (params.gateway?.isConnected ?? true) { + if (params.gateway?.isConnected === true) { const at = Date.now(); params.pushStatus?.({ ...createConnectedChannelStatusPatch(at), diff --git a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts b/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts index 4f641f21881..98e0fba9c32 100644 --- a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts +++ b/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts @@ -1,6 +1,5 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { diagnosticErrorCategory } from "../../../infra/diagnostic-error-metadata.js"; -export { diagnosticErrorCategory } from "../../../infra/diagnostic-error-metadata.js"; import { emitDiagnosticEvent, type DiagnosticEventInput,