From 7de494fcecccdea136fd9dd827352da43fa5b7fa Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 00:37:57 +0000 Subject: [PATCH] test: stabilize discord monitor ci isolation --- .../discord/src/monitor/monitor.test.ts | 53 ++++++++++++------- .../native-command.commands-allowfrom.test.ts | 11 +++- .../native-command.think-autocomplete.test.ts | 6 ++- .../src/monitor/provider.rest-proxy.test.ts | 10 +++- .../src/monitor/reply-delivery.test.ts | 7 ++- .../src/monitor/thread-session-close.test.ts | 6 +-- .../src/monitor/threading.auto-thread.test.ts | 12 ++--- 7 files changed, 66 insertions(+), 39 deletions(-) diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index 05c8325dd3c..e14a645b134 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -19,19 +19,7 @@ import { resolvePluginConversationBindingApprovalMock, upsertPairingRequestMock, } from "../../../../test/helpers/extensions/discord-component-runtime.js"; -import { - clearDiscordComponentEntries, - registerDiscordComponentEntries, - resolveDiscordComponentEntry, - resolveDiscordModalEntry, -} from "../components-registry.js"; -import type { DiscordComponentEntry, DiscordModalEntry } from "../components.js"; -import * as sendComponents from "../send.components.js"; -import { - createDiscordComponentButton, - createDiscordComponentStringSelect, - createDiscordComponentModal, -} from "./agent-components.js"; +import { type DiscordComponentEntry, type DiscordModalEntry } from "../components.js"; import type { DiscordChannelConfigResolved } from "./allow-list.js"; import { resolveDiscordMemberAllowed, @@ -53,6 +41,22 @@ import { resolveDiscordReplyDeliveryPlan, } from "./threading.js"; +type CreateDiscordComponentButton = + typeof import("./agent-components.js").createDiscordComponentButton; +type CreateDiscordComponentModal = + typeof import("./agent-components.js").createDiscordComponentModal; +type CreateDiscordComponentStringSelect = + typeof import("./agent-components.js").createDiscordComponentStringSelect; + +let createDiscordComponentButton: CreateDiscordComponentButton; +let createDiscordComponentStringSelect: CreateDiscordComponentStringSelect; +let createDiscordComponentModal: CreateDiscordComponentModal; +let clearDiscordComponentEntries: typeof import("../components-registry.js").clearDiscordComponentEntries; +let registerDiscordComponentEntries: typeof import("../components-registry.js").registerDiscordComponentEntries; +let resolveDiscordComponentEntry: typeof import("../components-registry.js").resolveDiscordComponentEntry; +let resolveDiscordModalEntry: typeof import("../components-registry.js").resolveDiscordModalEntry; +let sendComponents: typeof import("../send.components.js"); + const enqueueSystemEventMock = vi.hoisted(() => vi.fn()); const dispatchReplyMock = vi.hoisted(() => vi.fn()); const readSessionUpdatedAtMock = vi.hoisted(() => vi.fn()); @@ -136,9 +140,9 @@ describe("discord component interactions", () => { }; }; - const createComponentContext = ( - overrides?: Partial[0]>, - ) => + type ComponentContext = Parameters[0]; + + const createComponentContext = (overrides?: Partial) => ({ cfg: createCfg(), accountId: "default", @@ -147,7 +151,7 @@ describe("discord component interactions", () => { discordConfig: createDiscordConfig(), token: "token", ...overrides, - }) as Parameters[0]; + }) as ComponentContext; const createComponentButtonInteraction = (overrides: Partial = {}) => { const reply = vi.fn().mockResolvedValue(undefined); @@ -307,7 +311,20 @@ describe("discord component interactions", () => { expect(dispatchReplyMock).not.toHaveBeenCalled(); } - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ + createDiscordComponentButton, + createDiscordComponentStringSelect, + createDiscordComponentModal, + } = await import("./agent-components.js")); + ({ + clearDiscordComponentEntries, + registerDiscordComponentEntries, + resolveDiscordComponentEntry, + resolveDiscordModalEntry, + } = await import("../components-registry.js")); + sendComponents = await import("../send.components.js"); editDiscordComponentMessageMock = vi .spyOn(sendComponents, "editDiscordComponentMessage") .mockResolvedValue({ diff --git a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts index 87a65d4cf95..f475697cdf5 100644 --- a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts +++ b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts @@ -5,7 +5,7 @@ import * as dispatcherModule from "../../../../src/auto-reply/reply/provider-dis import type { OpenClawConfig } from "../../../../src/config/config.js"; import type { DiscordAccountConfig } from "../../../../src/config/types.discord.js"; import * as pluginCommandsModule from "../../../../src/plugins/commands.js"; -import { createDiscordNativeCommand } from "./native-command.js"; +import { __testing as nativeCommandTesting, createDiscordNativeCommand } from "./native-command.js"; import { createMockCommandInteraction, type MockCommandInteraction, @@ -68,13 +68,17 @@ function createCommand(cfg: OpenClawConfig, discordConfig?: DiscordAccountConfig } function createDispatchSpy() { - return vi.spyOn(dispatcherModule, "dispatchReplyWithDispatcher").mockResolvedValue({ + const dispatchSpy = vi.spyOn(dispatcherModule, "dispatchReplyWithDispatcher").mockResolvedValue({ counts: { final: 1, block: 0, tool: 0, }, } as never); + nativeCommandTesting.setDispatchReplyWithDispatcher( + dispatcherModule.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithDispatcher, + ); + return dispatchSpy; } async function runGuildSlashCommand(params?: { @@ -110,6 +114,9 @@ function expectUnauthorizedReply(interaction: MockCommandInteraction) { describe("Discord native slash commands with commands.allowFrom", () => { beforeEach(() => { vi.restoreAllMocks(); + nativeCommandTesting.setDispatchReplyWithDispatcher( + dispatcherModule.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithDispatcher, + ); }); it("authorizes guild slash commands when commands.allowFrom.discord matches the sender", async () => { diff --git a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts index 353877aa367..a0cc6e45703 100644 --- a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts +++ b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts @@ -10,7 +10,6 @@ import { import type { OpenClawConfig, loadConfig } from "../../../../src/config/config.js"; import { clearSessionStoreCacheForTest } from "../../../../src/config/sessions/store.js"; import { createConfiguredBindingConversationRuntimeModuleMock } from "../../../../test/helpers/extensions/configured-binding-runtime.js"; -import { resolveDiscordNativeChoiceContext } from "./native-command-ui.js"; import { createNoopThreadBindingManager } from "./thread-bindings.js"; const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => @@ -52,9 +51,12 @@ const STORE_PATH = path.join( `openclaw-discord-think-autocomplete-${process.pid}.json`, ); const SESSION_KEY = "agent:main:main"; +let resolveDiscordNativeChoiceContext: typeof import("./native-command-ui.js").resolveDiscordNativeChoiceContext; describe("discord native /think autocomplete", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ resolveDiscordNativeChoiceContext } = await import("./native-command-ui.js")); clearSessionStoreCacheForTest(); ensureConfiguredBindingRouteReadyMock.mockReset(); ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true }); diff --git a/extensions/discord/src/monitor/provider.rest-proxy.test.ts b/extensions/discord/src/monitor/provider.rest-proxy.test.ts index 47ed5bb6335..2a779c5d6f0 100644 --- a/extensions/discord/src/monitor/provider.rest-proxy.test.ts +++ b/extensions/discord/src/monitor/provider.rest-proxy.test.ts @@ -1,5 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; -import { resolveDiscordRestFetch } from "./rest-fetch.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({ undiciFetchMock: vi.fn(), @@ -23,7 +22,14 @@ vi.mock("undici", () => { }; }); +let resolveDiscordRestFetch: typeof import("./rest-fetch.js").resolveDiscordRestFetch; + describe("resolveDiscordRestFetch", () => { + beforeEach(async () => { + vi.resetModules(); + ({ resolveDiscordRestFetch } = await import("./rest-fetch.js")); + }); + it("uses undici proxy fetch when a proxy URL is configured", async () => { const runtime = { log: vi.fn(), diff --git a/extensions/discord/src/monitor/reply-delivery.test.ts b/extensions/discord/src/monitor/reply-delivery.test.ts index 4ee96d5237a..5348f79a259 100644 --- a/extensions/discord/src/monitor/reply-delivery.test.ts +++ b/extensions/discord/src/monitor/reply-delivery.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../../src/config/config.js"; import type { RuntimeEnv } from "../../../../src/runtime.js"; -import { deliverDiscordReply } from "./reply-delivery.js"; import { __testing as threadBindingTesting, createThreadBindingManager, @@ -59,6 +58,8 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { }; }); +let deliverDiscordReply: typeof import("./reply-delivery.js").deliverDiscordReply; + describe("deliverDiscordReply", () => { const runtime = {} as RuntimeEnv; const cfg = { @@ -111,7 +112,9 @@ describe("deliverDiscordReply", () => { return threadBindings; }; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ deliverDiscordReply } = await import("./reply-delivery.js")); sendMessageDiscordMock.mockClear().mockResolvedValue({ messageId: "msg-1", channelId: "channel-1", diff --git a/extensions/discord/src/monitor/thread-session-close.test.ts b/extensions/discord/src/monitor/thread-session-close.test.ts index 713bbd1690f..dc91c72f7d0 100644 --- a/extensions/discord/src/monitor/thread-session-close.test.ts +++ b/extensions/discord/src/monitor/thread-session-close.test.ts @@ -30,11 +30,9 @@ const MATCHED_KEY = `agent:main:discord:channel:${THREAD_ID}`; const UNMATCHED_KEY = `agent:main:discord:channel:${OTHER_ID}`; describe("closeDiscordThreadSessions", () => { - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ closeDiscordThreadSessions } = await import("./thread-session-close.js")); - }); - - beforeEach(() => { hoisted.updateSessionStore.mockClear(); hoisted.resolveStorePath.mockClear(); hoisted.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions.json"); diff --git a/extensions/discord/src/monitor/threading.auto-thread.test.ts b/extensions/discord/src/monitor/threading.auto-thread.test.ts index a675fd1c897..201d2c88a25 100644 --- a/extensions/discord/src/monitor/threading.auto-thread.test.ts +++ b/extensions/discord/src/monitor/threading.auto-thread.test.ts @@ -1,5 +1,5 @@ import { ChannelType } from "@buape/carbon"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../../src/config/config.js"; type MaybeCreateDiscordAutoThreadFn = typeof import("./threading.js").maybeCreateDiscordAutoThread; @@ -46,7 +46,8 @@ async function flushAsyncWork() { await Promise.resolve(); } -beforeAll(async () => { +beforeEach(async () => { + vi.resetModules(); postMock.mockReset(); getMock.mockReset(); patchMock.mockReset(); @@ -54,13 +55,6 @@ beforeAll(async () => { ({ maybeCreateDiscordAutoThread } = await import("./threading.js")); }); -beforeEach(() => { - postMock.mockReset(); - getMock.mockReset(); - patchMock.mockReset(); - generateThreadTitleMock.mockReset(); -}); - describe("maybeCreateDiscordAutoThread", () => { it("skips auto-thread if channelType is GuildForum", async () => { const result = await maybeCreateDiscordAutoThread(