From bee98477df5d477a0cd86fd3adbe6327e55cb059 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 15:54:22 +0100 Subject: [PATCH] test: clear discord provider broad matchers --- .../discord/src/monitor/provider.test.ts | 115 ++++++++++-------- 1 file changed, 67 insertions(+), 48 deletions(-) diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index c7eac5fbf0a..6a3354d97e0 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -102,6 +102,26 @@ function createConfigWithDiscordAccount(overrides: Record = {}) } as OpenClawConfig; } +type MockCallReader = { mock: { calls: unknown[][] } }; + +function mockMessages(mock: unknown): string[] { + return (mock as MockCallReader).mock.calls.map((call) => String(call[0] ?? "")); +} + +function expectMockLogContains(mock: unknown, expected: string): void { + expect(mockMessages(mock).some((message) => message.includes(expected))).toBe(true); +} + +function expectMockLogNotContains(mock: unknown, expected: string): void { + expect(mockMessages(mock).every((message) => !message.includes(expected))).toBe(true); +} + +function expectMessagesContainAll(messages: string[], expected: string[]): void { + for (const entry of expected) { + expect(messages.some((message) => message.includes(entry))).toBe(true); + } +} + vi.mock("../voice/manager.runtime.js", () => { voiceRuntimeModuleLoadedMock(); return { @@ -163,6 +183,21 @@ describe("monitorDiscordProvider", () => { return reconcileParams.healthProbe; }; + const getMonitorLifecycleParams = (): { + gatewayReadyTimeoutMs?: number; + gatewayRuntimeReadyTimeoutMs?: number; + } => { + expect(monitorLifecycleMock).toHaveBeenCalledTimes(1); + const firstCall = monitorLifecycleMock.mock.calls.at(0); + const params = firstCall?.[0] as + | { gatewayReadyTimeoutMs?: number; gatewayRuntimeReadyTimeoutMs?: number } + | undefined; + if (!params) { + throw new Error("expected lifecycle monitor params"); + } + return params; + }; + beforeAll(async () => { vi.doMock("openclaw/plugin-sdk/plugin-runtime", async () => { const actual = await vi.importActual( @@ -330,8 +365,9 @@ describe("monitorDiscordProvider", () => { expect( emitter.emit("error", new Error("Max reconnect attempts (0) reached after code 1005")), ).toBe(true); - expect(runtime.error).toHaveBeenCalledWith( - expect.stringContaining("suppressed late gateway reconnect-exhausted error after dispose"), + expectMockLogContains( + runtime.error, + "suppressed late gateway reconnect-exhausted error after dispose", ); }); @@ -350,7 +386,7 @@ describe("monitorDiscordProvider", () => { expect(monitorLifecycleMock).not.toHaveBeenCalled(); expect(createdBindingManagers).toHaveLength(1); expect(createdBindingManagers[0]?.stop).toHaveBeenCalledTimes(1); - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("identity offline")); + expectMockLogContains(runtime.error, "identity offline"); }); it("fails closed before lifecycle when Discord bot identity has no usable id", async () => { @@ -368,7 +404,7 @@ describe("monitorDiscordProvider", () => { expect(monitorLifecycleMock).not.toHaveBeenCalled(); expect(createdBindingManagers).toHaveLength(1); expect(createdBindingManagers[0]?.stop).toHaveBeenCalledTimes(1); - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("no usable id")); + expectMockLogContains(runtime.error, "no usable id"); }); it("does not double-stop thread bindings when lifecycle performs cleanup", async () => { @@ -402,12 +438,9 @@ describe("monitorDiscordProvider", () => { runtime: baseRuntime(), }); - expect(monitorLifecycleMock).toHaveBeenCalledWith( - expect.objectContaining({ - gatewayReadyTimeoutMs: 90_000, - gatewayRuntimeReadyTimeoutMs: 120_000, - }), - ); + const lifecycleParams = getMonitorLifecycleParams(); + expect(lifecycleParams.gatewayReadyTimeoutMs).toBe(90_000); + expect(lifecycleParams.gatewayRuntimeReadyTimeoutMs).toBe(120_000); }); it("does not load the Discord voice runtime when voice is disabled", async () => { @@ -852,9 +885,7 @@ describe("monitorDiscordProvider", () => { await vi.waitFor(() => expect(clientDeployCommandsMock).toHaveBeenCalledTimes(1)); expect(monitorLifecycleMock).toHaveBeenCalledTimes(1); - expect(runtime.error).not.toHaveBeenCalledWith( - expect.stringContaining("failed to deploy native commands"), - ); + expectMockLogNotContains(runtime.error, "failed to deploy native commands"); expect( vi .mocked(runtime.log) @@ -908,9 +939,7 @@ describe("monitorDiscordProvider", () => { expect(warningMessages[0]).toContain("retry after 0s"); expect(warningMessages[0]).toContain("Message send/receive is unaffected."); expect(warningMessages[0]).not.toContain("body="); - expect(runtime.error).not.toHaveBeenCalledWith( - expect.stringContaining("native-slash-command-deploy-rest"), - ); + expectMockLogNotContains(runtime.error, "native-slash-command-deploy-rest"); }); it("formats Discord deploy rate limits without raw response bodies", () => { @@ -985,11 +1014,10 @@ describe("monitorDiscordProvider", () => { await vi.waitFor(() => expect(clientDeployCommandsMock).toHaveBeenCalledTimes(1)); expect(clientDeployCommandsMock).toHaveBeenCalledWith({ mode: "reconcile" }); - expect(getConstructedClientOptions().requestOptions).toMatchObject({ - timeout: 15_000, - runtimeProfile: "persistent", - maxQueueSize: 1000, - }); + const requestOptions = getConstructedClientOptions().requestOptions; + expect(requestOptions?.timeout).toBe(15_000); + expect(requestOptions?.runtimeProfile).toBe("persistent"); + expect(requestOptions?.maxQueueSize).toBe(1000); expect(getConstructedClientOptions().eventQueue?.listenerTimeout).toBe(120_000); }); @@ -1017,9 +1045,7 @@ describe("monitorDiscordProvider", () => { expect(listNativeCommandSpecsForConfigMock).not.toHaveBeenCalled(); expect(getPluginCommandSpecsMock).not.toHaveBeenCalled(); expect(clientDeployCommandsMock).not.toHaveBeenCalled(); - expect(runtime.log).not.toHaveBeenCalledWith( - expect.stringContaining("cleared native commands"), - ); + expectMockLogNotContains(runtime.log, "cleared native commands"); }); it("derives application id from token before probing Discord over REST", async () => { @@ -1082,8 +1108,9 @@ describe("monitorDiscordProvider", () => { setStatus, }); - expect(setStatus.mock.calls).toContainEqual([expect.objectContaining({ connected: true })]); - expect(setStatus.mock.calls).toContainEqual([expect.objectContaining({ connected: false })]); + const statuses = setStatus.mock.calls.map((call) => call[0] as { connected?: boolean }); + expect(statuses.some((status) => status.connected === true)).toBe(true); + expect(statuses.some((status) => status.connected === false)).toBe(true); }); it("logs Discord startup phases and early gateway debug events", async () => { @@ -1104,27 +1131,21 @@ describe("monitorDiscordProvider", () => { runtime, }); - await vi.waitFor(() => - expect(vi.mocked(runtime.log).mock.calls.map((call) => String(call[0]))).toEqual( - expect.arrayContaining([expect.stringContaining("deploy-commands:done")]), - ), - ); + await vi.waitFor(() => expectMockLogContains(runtime.log, "deploy-commands:done")); const messages = vi.mocked(runtime.log).mock.calls.map((call) => String(call[0])); - expect(messages).toEqual( - expect.arrayContaining([ - expect.stringContaining("fetch-application-id:start"), - expect.stringContaining("fetch-application-id:done"), - expect.stringContaining("deploy-commands:schedule"), - expect.stringContaining("deploy-commands:scheduled"), - expect.stringContaining("deploy-commands:done"), - expect.stringContaining("fetch-bot-identity:start"), - expect.stringContaining("fetch-bot-identity:done"), - ]), - ); - expect(messages).toEqual( - expect.arrayContaining([expect.stringMatching(/gateway-debug.*Gateway websocket opened/)]), - ); + expectMessagesContainAll(messages, [ + "fetch-application-id:start", + "fetch-application-id:done", + "deploy-commands:schedule", + "deploy-commands:scheduled", + "deploy-commands:done", + "fetch-bot-identity:start", + "fetch-bot-identity:done", + ]); + expect( + messages.some((message) => /gateway-debug.*Gateway websocket opened/.test(message)), + ).toBe(true); }); it("keeps Discord startup chatter quiet by default", async () => { @@ -1136,8 +1157,6 @@ describe("monitorDiscordProvider", () => { }); const messages = vi.mocked(runtime.log).mock.calls.map((call) => String(call[0])); - expect(messages).not.toEqual( - expect.arrayContaining([expect.stringContaining("discord startup [")]), - ); + expect(messages.every((message) => !message.includes("discord startup ["))).toBe(true); }); });