diff --git a/extensions/discord/src/api.test.ts b/extensions/discord/src/api.test.ts index 11d15d5f59f..f2a6d557769 100644 --- a/extensions/discord/src/api.test.ts +++ b/extensions/discord/src/api.test.ts @@ -1,9 +1,13 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { withFetchPreconnect } from "../../../test/helpers/extensions/fetch-mock.js"; import { fetchDiscord } from "./api.js"; import { jsonResponse } from "./test-http-helpers.js"; describe("fetchDiscord", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + it("formats rate limit payloads without raw JSON", async () => { const fetcher = withFetchPreconnect(async () => jsonResponse( diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts index a760883583b..68887159b88 100644 --- a/extensions/discord/src/channel.test.ts +++ b/extensions/discord/src/channel.test.ts @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { PluginRuntime } from "../../../src/plugins/runtime/types.js"; import { createStartAccountContext } from "../../../test/helpers/extensions/start-account-context.js"; import type { ResolvedDiscordAccount } from "./accounts.js"; -import { discordPlugin } from "./channel.js"; import type { OpenClawConfig } from "./runtime-api.js"; -import { setDiscordRuntime } from "./runtime.js"; +let discordPlugin: typeof import("./channel.js").discordPlugin; +let setDiscordRuntime: typeof import("./runtime.js").setDiscordRuntime; const probeDiscordMock = vi.hoisted(() => vi.fn()); const monitorDiscordProviderMock = vi.hoisted(() => vi.fn()); @@ -75,6 +75,13 @@ afterEach(() => { auditDiscordChannelPermissionsMock.mockReset(); }); +beforeEach(async () => { + vi.useRealTimers(); + vi.resetModules(); + ({ discordPlugin } = await import("./channel.js")); + ({ setDiscordRuntime } = await import("./runtime.js")); +}); + describe("discordPlugin outbound", () => { it("forwards mediaLocalRoots to sendMessageDiscord", async () => { const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" })); diff --git a/extensions/discord/src/components.test.ts b/extensions/discord/src/components.test.ts index 74357939629..2737f19c274 100644 --- a/extensions/discord/src/components.test.ts +++ b/extensions/discord/src/components.test.ts @@ -1,16 +1,25 @@ import { MessageFlags } from "discord-api-types/v10"; -import { beforeEach, describe, expect, it } from "vitest"; -import { - clearDiscordComponentEntries, - registerDiscordComponentEntries, - resolveDiscordComponentEntry, - resolveDiscordModalEntry, -} from "./components-registry.js"; -import { - buildDiscordComponentMessage, - buildDiscordComponentMessageFlags, - readDiscordComponentSpec, -} from "./components.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +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 buildDiscordComponentMessage: typeof import("./components.js").buildDiscordComponentMessage; +let buildDiscordComponentMessageFlags: typeof import("./components.js").buildDiscordComponentMessageFlags; +let readDiscordComponentSpec: typeof import("./components.js").readDiscordComponentSpec; + +beforeEach(async () => { + vi.resetModules(); + ({ + clearDiscordComponentEntries, + registerDiscordComponentEntries, + resolveDiscordComponentEntry, + resolveDiscordModalEntry, + } = await import("./components-registry.js")); + ({ buildDiscordComponentMessage, buildDiscordComponentMessageFlags, readDiscordComponentSpec } = + await import("./components.js")); +}); describe("discord components", () => { it("builds v2 containers with modal trigger", () => { diff --git a/extensions/discord/src/monitor.test.ts b/extensions/discord/src/monitor.test.ts index 9d06015b9de..60d76b70888 100644 --- a/extensions/discord/src/monitor.test.ts +++ b/extensions/discord/src/monitor.test.ts @@ -61,6 +61,11 @@ function createAutoThreadMentionContext() { return { guildInfo, channelConfig }; } +beforeEach(() => { + vi.useRealTimers(); + readAllowFromStoreMock.mockReset().mockResolvedValue([]); +}); + describe("registerDiscordListener", () => { class FakeListener {} diff --git a/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts b/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts index 2c2811a25a2..c9b2f0b36a6 100644 --- a/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts +++ b/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts @@ -1,5 +1,5 @@ import type { Client } from "@buape/carbon"; -import { ChannelType, MessageType } from "@buape/carbon"; +import { MessageType } from "@buape/carbon"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { dispatchMock, @@ -9,13 +9,19 @@ import { updateLastRouteMock, upsertPairingRequestMock, } from "./monitor.tool-result.test-harness.js"; -import { createDiscordMessageHandler } from "./monitor/message-handler.js"; -import { __resetDiscordChannelInfoCacheForTest } from "./monitor/message-utils.js"; -import { createNoopThreadBindingManager } from "./monitor/thread-bindings.js"; type Config = ReturnType; +let ChannelType: typeof import("@buape/carbon").ChannelType; +let createDiscordMessageHandler: typeof import("./monitor/message-handler.js").createDiscordMessageHandler; +let __resetDiscordChannelInfoCacheForTest: typeof import("./monitor/message-utils.js").__resetDiscordChannelInfoCacheForTest; +let createNoopThreadBindingManager: typeof import("./monitor/thread-bindings.js").createNoopThreadBindingManager; -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + ({ ChannelType } = await import("@buape/carbon")); + ({ createDiscordMessageHandler } = await import("./monitor/message-handler.js")); + ({ __resetDiscordChannelInfoCacheForTest } = await import("./monitor/message-utils.js")); + ({ createNoopThreadBindingManager } = await import("./monitor/thread-bindings.js")); __resetDiscordChannelInfoCacheForTest(); sendMock.mockClear().mockResolvedValue(undefined); updateLastRouteMock.mockClear(); diff --git a/extensions/discord/src/monitor/agent-components.wildcard.test.ts b/extensions/discord/src/monitor/agent-components.wildcard.test.ts index 232e3c365cb..a13b5c2299e 100644 --- a/extensions/discord/src/monitor/agent-components.wildcard.test.ts +++ b/extensions/discord/src/monitor/agent-components.wildcard.test.ts @@ -1,14 +1,28 @@ -import { describe, expect, it } from "vitest"; -import { buildDiscordComponentCustomId, buildDiscordModalCustomId } from "../components.js"; -import { - createDiscordComponentButton, - createDiscordComponentChannelSelect, - createDiscordComponentMentionableSelect, - createDiscordComponentModal, - createDiscordComponentRoleSelect, - createDiscordComponentStringSelect, - createDiscordComponentUserSelect, -} from "./agent-components.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let buildDiscordComponentCustomId: typeof import("../components.js").buildDiscordComponentCustomId; +let buildDiscordModalCustomId: typeof import("../components.js").buildDiscordModalCustomId; +let createDiscordComponentButton: typeof import("./agent-components.js").createDiscordComponentButton; +let createDiscordComponentChannelSelect: typeof import("./agent-components.js").createDiscordComponentChannelSelect; +let createDiscordComponentMentionableSelect: typeof import("./agent-components.js").createDiscordComponentMentionableSelect; +let createDiscordComponentModal: typeof import("./agent-components.js").createDiscordComponentModal; +let createDiscordComponentRoleSelect: typeof import("./agent-components.js").createDiscordComponentRoleSelect; +let createDiscordComponentStringSelect: typeof import("./agent-components.js").createDiscordComponentStringSelect; +let createDiscordComponentUserSelect: typeof import("./agent-components.js").createDiscordComponentUserSelect; + +beforeEach(async () => { + vi.resetModules(); + ({ buildDiscordComponentCustomId, buildDiscordModalCustomId } = await import("../components.js")); + ({ + createDiscordComponentButton, + createDiscordComponentChannelSelect, + createDiscordComponentMentionableSelect, + createDiscordComponentModal, + createDiscordComponentRoleSelect, + createDiscordComponentStringSelect, + createDiscordComponentUserSelect, + } = await import("./agent-components.js")); +}); type WildcardComponent = { customId: string; diff --git a/extensions/discord/src/monitor/listeners.test.ts b/extensions/discord/src/monitor/listeners.test.ts index d8158320e44..1fc68a29eef 100644 --- a/extensions/discord/src/monitor/listeners.test.ts +++ b/extensions/discord/src/monitor/listeners.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; -import { DiscordMessageListener } from "./listeners.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let DiscordMessageListener: typeof import("./listeners.js").DiscordMessageListener; + +beforeEach(async () => { + vi.resetModules(); + ({ DiscordMessageListener } = await import("./listeners.js")); +}); function createLogger() { return { diff --git a/extensions/discord/src/monitor/message-handler.preflight.test.ts b/extensions/discord/src/monitor/message-handler.preflight.test.ts index bd55cd2ead2..c9758e839d4 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test.ts @@ -10,11 +10,6 @@ import { __testing as sessionBindingTesting, registerSessionBindingAdapter, } from "../../../../src/infra/outbound/session-binding-service.js"; -import { - preflightDiscordMessage, - resolvePreflightMentionRequirement, - shouldIgnoreBoundThreadWebhookMessage, -} from "./message-handler.preflight.js"; import { createDiscordMessage, createDiscordPreflightArgs, @@ -25,10 +20,22 @@ import { type DiscordConfig, type DiscordMessageEvent, } from "./message-handler.preflight.test-helpers.js"; -import { - __testing as threadBindingTesting, - createThreadBindingManager, -} from "./thread-bindings.js"; +let preflightDiscordMessage: typeof import("./message-handler.preflight.js").preflightDiscordMessage; +let resolvePreflightMentionRequirement: typeof import("./message-handler.preflight.js").resolvePreflightMentionRequirement; +let shouldIgnoreBoundThreadWebhookMessage: typeof import("./message-handler.preflight.js").shouldIgnoreBoundThreadWebhookMessage; +let threadBindingTesting: typeof import("./thread-bindings.js").__testing; +let createThreadBindingManager: typeof import("./thread-bindings.js").createThreadBindingManager; + +beforeEach(async () => { + vi.resetModules(); + ({ + preflightDiscordMessage, + resolvePreflightMentionRequirement, + shouldIgnoreBoundThreadWebhookMessage, + } = await import("./message-handler.preflight.js")); + ({ __testing: threadBindingTesting, createThreadBindingManager } = + await import("./thread-bindings.js")); +}); function createThreadBinding( overrides?: Partial< diff --git a/extensions/discord/src/monitor/message-handler.queue.test.ts b/extensions/discord/src/monitor/message-handler.queue.test.ts index afd8e882fa6..ff4d1dd25d8 100644 --- a/extensions/discord/src/monitor/message-handler.queue.test.ts +++ b/extensions/discord/src/monitor/message-handler.queue.test.ts @@ -1,13 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; -import { - createDiscordMessageHandler, - preflightDiscordMessageMock, - processDiscordMessageMock, -} from "./message-handler.module-test-helpers.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createDiscordHandlerParams, createDiscordPreflightContext, } from "./message-handler.test-helpers.js"; +let createDiscordMessageHandler: typeof import("./message-handler.module-test-helpers.js").createDiscordMessageHandler; +let preflightDiscordMessageMock: typeof import("./message-handler.module-test-helpers.js").preflightDiscordMessageMock; +let processDiscordMessageMock: typeof import("./message-handler.module-test-helpers.js").processDiscordMessageMock; const eventualReplyDeliveredMock = vi.hoisted(() => vi.fn()); type SetStatusFn = (patch: Record) => void; @@ -86,6 +84,12 @@ async function createLifecycleStopScenario(params: { } describe("createDiscordMessageHandler queue behavior", () => { + beforeEach(async () => { + vi.resetModules(); + ({ createDiscordMessageHandler, preflightDiscordMessageMock, processDiscordMessageMock } = + await import("./message-handler.module-test-helpers.js")); + }); + it("resets busy counters when the handler is created", () => { preflightDiscordMessageMock.mockReset(); processDiscordMessageMock.mockReset(); diff --git a/extensions/discord/src/monitor/native-command.options.test.ts b/extensions/discord/src/monitor/native-command.options.test.ts index 1f5eb9058ef..deceb66d80f 100644 --- a/extensions/discord/src/monitor/native-command.options.test.ts +++ b/extensions/discord/src/monitor/native-command.options.test.ts @@ -1,10 +1,12 @@ -import { describe, expect, it, vi } from "vitest"; -import { listNativeCommandSpecs } from "../../../../src/auto-reply/commands-registry.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, loadConfig } from "../../../../src/config/config.js"; -import { createDiscordNativeCommand } from "./native-command.js"; -import { createNoopThreadBindingManager } from "./thread-bindings.js"; +let listNativeCommandSpecs: typeof import("../../../../src/auto-reply/commands-registry.js").listNativeCommandSpecs; +let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand; +let createNoopThreadBindingManager: typeof import("./thread-bindings.js").createNoopThreadBindingManager; -function createNativeCommand(name: string): ReturnType { +function createNativeCommand( + name: string, +): ReturnType { const command = listNativeCommandSpecs({ provider: "discord" }).find( (entry) => entry.name === name, ); @@ -24,17 +26,19 @@ function createNativeCommand(name: string): ReturnType["options"]>[number]; +type CommandOption = NonNullable< + ReturnType["options"] +>[number]; function findOption( - command: ReturnType, + command: ReturnType, name: string, ): CommandOption | undefined { return command.options?.find((entry) => entry.name === name); } function requireOption( - command: ReturnType, + command: ReturnType, name: string, ): CommandOption { const option = findOption(command, name); @@ -60,6 +64,13 @@ function readChoices(option: CommandOption | undefined): unknown[] | undefined { } describe("createDiscordNativeCommand option wiring", () => { + beforeEach(async () => { + vi.resetModules(); + ({ listNativeCommandSpecs } = await import("../../../../src/auto-reply/commands-registry.js")); + ({ createDiscordNativeCommand } = await import("./native-command.js")); + ({ createNoopThreadBindingManager } = await import("./thread-bindings.js")); + }); + it("uses autocomplete for /acp action so inline action values are accepted", async () => { const command = createNativeCommand("acp"); const action = requireOption(command, "action"); diff --git a/extensions/discord/src/monitor/provider.skill-dedupe.test.ts b/extensions/discord/src/monitor/provider.skill-dedupe.test.ts index cb33c874553..2e182b6210d 100644 --- a/extensions/discord/src/monitor/provider.skill-dedupe.test.ts +++ b/extensions/discord/src/monitor/provider.skill-dedupe.test.ts @@ -1,7 +1,13 @@ -import { describe, expect, it } from "vitest"; -import { __testing } from "./provider.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let __testing: typeof import("./provider.js").__testing; describe("resolveThreadBindingsEnabled", () => { + beforeEach(async () => { + vi.resetModules(); + ({ __testing } = await import("./provider.js")); + }); + it("defaults to enabled when unset", () => { expect( __testing.resolveThreadBindingsEnabled({ diff --git a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts index 51ae59de906..e048fb5c143 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.test.ts @@ -34,11 +34,14 @@ vi.mock("../send.js", async (importOriginal) => { }; }); -const { maybeSendBindingMessage, resolveChannelIdForBinding } = - await import("./thread-bindings.discord-api.js"); +let maybeSendBindingMessage: typeof import("./thread-bindings.discord-api.js").maybeSendBindingMessage; +let resolveChannelIdForBinding: typeof import("./thread-bindings.discord-api.js").resolveChannelIdForBinding; describe("resolveChannelIdForBinding", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ maybeSendBindingMessage, resolveChannelIdForBinding } = + await import("./thread-bindings.discord-api.js")); hoisted.restGet.mockClear(); hoisted.createDiscordRestClient.mockClear(); hoisted.sendMessageDiscord.mockClear().mockResolvedValue({}); @@ -124,7 +127,10 @@ describe("resolveChannelIdForBinding", () => { }); describe("maybeSendBindingMessage", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ maybeSendBindingMessage, resolveChannelIdForBinding } = + await import("./thread-bindings.discord-api.js")); hoisted.sendMessageDiscord.mockClear().mockResolvedValue({}); hoisted.sendWebhookMessageDiscord.mockClear().mockResolvedValue({}); }); diff --git a/extensions/discord/src/monitor/thread-session-close.test.ts b/extensions/discord/src/monitor/thread-session-close.test.ts index a5cca87119c..faff3a4cf03 100644 --- a/extensions/discord/src/monitor/thread-session-close.test.ts +++ b/extensions/discord/src/monitor/thread-session-close.test.ts @@ -15,7 +15,7 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { }; }); -const { closeDiscordThreadSessions } = await import("./thread-session-close.js"); +let closeDiscordThreadSessions: typeof import("./thread-session-close.js").closeDiscordThreadSessions; function setupStore(store: Record) { hoisted.updateSessionStore.mockImplementation( @@ -30,7 +30,9 @@ const MATCHED_KEY = `agent:main:discord:channel:${THREAD_ID}`; const UNMATCHED_KEY = `agent:main:discord:channel:${OTHER_ID}`; describe("closeDiscordThreadSessions", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ closeDiscordThreadSessions } = await import("./thread-session-close.js")); hoisted.updateSessionStore.mockClear(); hoisted.resolveStorePath.mockClear(); hoisted.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions.json"); diff --git a/extensions/discord/src/outbound-adapter.test.ts b/extensions/discord/src/outbound-adapter.test.ts index c3833972f44..08cff034216 100644 --- a/extensions/discord/src/outbound-adapter.test.ts +++ b/extensions/discord/src/outbound-adapter.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { normalizeDiscordOutboundTarget } from "./normalize.js"; const hoisted = vi.hoisted(() => { const sendMessageDiscordMock = vi.fn(); @@ -37,7 +36,14 @@ vi.mock("./monitor/thread-bindings.js", async (importOriginal) => { }; }); -const { discordOutbound } = await import("./outbound-adapter.js"); +let normalizeDiscordOutboundTarget: typeof import("./normalize.js").normalizeDiscordOutboundTarget; +let discordOutbound: typeof import("./outbound-adapter.js").discordOutbound; + +beforeEach(async () => { + vi.resetModules(); + ({ normalizeDiscordOutboundTarget } = await import("./normalize.js")); + ({ discordOutbound } = await import("./outbound-adapter.js")); +}); const DEFAULT_DISCORD_SEND_RESULT = { channel: "discord", diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index 501561fe474..b57405bf61a 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -1,10 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MsgContext } from "../../../../src/auto-reply/templating.js"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/suites.js"; -import { - createBaseSignalEventHandlerDeps, - createSignalReceiveEvent, -} from "./event-handler.test-harness.js"; +let expectInboundContextContract: typeof import("../../../../src/channels/plugins/contracts/suites.js").expectChannelInboundContextContract; +let createBaseSignalEventHandlerDeps: typeof import("./event-handler.test-harness.js").createBaseSignalEventHandlerDeps; +let createSignalReceiveEvent: typeof import("./event-handler.test-harness.js").createSignalReceiveEvent; const { sendTypingMock, sendReadReceiptMock, dispatchInboundMessageMock, capture } = vi.hoisted( () => { @@ -52,7 +50,12 @@ let createSignalEventHandler: typeof import("./event-handler.js").createSignalEv describe("signal createSignalEventHandler inbound context", () => { beforeEach(async () => { + vi.useRealTimers(); vi.resetModules(); + ({ expectChannelInboundContextContract: expectInboundContextContract } = + await import("../../../../src/channels/plugins/contracts/suites.js")); + ({ createBaseSignalEventHandlerDeps, createSignalReceiveEvent } = + await import("./event-handler.test-harness.js")); ({ createSignalEventHandler } = await import("./event-handler.js")); capture.ctx = undefined; sendTypingMock.mockReset().mockResolvedValue(true); diff --git a/extensions/slack/src/monitor.threading.missing-thread-ts.test.ts b/extensions/slack/src/monitor.threading.missing-thread-ts.test.ts index 99944e04d3c..7ae9efb1d58 100644 --- a/extensions/slack/src/monitor.threading.missing-thread-ts.test.ts +++ b/extensions/slack/src/monitor.threading.missing-thread-ts.test.ts @@ -10,7 +10,7 @@ import { stopSlackMonitor, } from "./monitor.test-helpers.js"; -const { monitorSlackProvider } = await import("./monitor.js"); +let monitorSlackProvider: typeof import("./monitor.js").monitorSlackProvider; const slackTestState = getSlackTestState(); @@ -69,6 +69,12 @@ async function runMissingThreadScenario(params: { beforeEach(() => { resetInboundDedupe(); +}); + +beforeEach(async () => { + vi.resetModules(); + ({ monitorSlackProvider } = await import("./monitor.js")); + resetInboundDedupe(); resetSlackTestState({ messages: { responsePrefix: "PFX" }, channels: { diff --git a/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts b/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts index a6b972f2e7d..d4305ef0030 100644 --- a/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts +++ b/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts @@ -50,7 +50,7 @@ vi.mock("./message-handler/dispatch.js", () => ({ dispatchPreparedSlackMessageMock(prepared), })); -import { createSlackMessageHandler } from "./message-handler.js"; +let createSlackMessageHandler: typeof import("./message-handler.js").createSlackMessageHandler; function createMarkMessageSeen() { const seen = new Set(); @@ -117,7 +117,9 @@ async function createInFlightMessageScenario(ts: string) { } describe("createSlackMessageHandler app_mention race handling", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ createSlackMessageHandler } = await import("./message-handler.js")); prepareSlackMessageMock.mockReset(); dispatchPreparedSlackMessageMock.mockReset(); }); diff --git a/extensions/slack/src/monitor/replies.test.ts b/extensions/slack/src/monitor/replies.test.ts index 50bf5e4026f..18f50fc3748 100644 --- a/extensions/slack/src/monitor/replies.test.ts +++ b/extensions/slack/src/monitor/replies.test.ts @@ -5,7 +5,7 @@ vi.mock("../send.js", () => ({ sendMessageSlack: (...args: unknown[]) => sendMock(...args), })); -import { deliverReplies } from "./replies.js"; +let deliverReplies: typeof import("./replies.js").deliverReplies; function baseParams(overrides?: Record) { return { @@ -20,7 +20,9 @@ function baseParams(overrides?: Record) { } describe("deliverReplies identity passthrough", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ deliverReplies } = await import("./replies.js")); sendMock.mockReset(); }); it("passes identity to sendMessageSlack for text replies", async () => { diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index a1f537ffc32..fba939345c3 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -194,7 +194,9 @@ async function loadRegisterSlackMonitorSlashCommands(): Promise { const { dispatchMock } = getSlackSlashMocks(); -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + registerSlackMonitorSlashCommandsPromise = undefined; resetSlackSlashMocks(); }); diff --git a/extensions/telegram/src/allowed-updates.test.ts b/extensions/telegram/src/allowed-updates.test.ts index e0c149cc682..1e6534c1a53 100644 --- a/extensions/telegram/src/allowed-updates.test.ts +++ b/extensions/telegram/src/allowed-updates.test.ts @@ -1,6 +1,14 @@ -import { API_CONSTANTS } from "grammy"; -import { describe, expect, it } from "vitest"; -import { DEFAULT_TELEGRAM_UPDATE_TYPES, resolveTelegramAllowedUpdates } from "./allowed-updates.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +let API_CONSTANTS: typeof import("grammy").API_CONSTANTS; +let DEFAULT_TELEGRAM_UPDATE_TYPES: typeof import("./allowed-updates.js").DEFAULT_TELEGRAM_UPDATE_TYPES; +let resolveTelegramAllowedUpdates: typeof import("./allowed-updates.js").resolveTelegramAllowedUpdates; + +beforeEach(async () => { + vi.resetModules(); + ({ API_CONSTANTS } = await import("grammy")); + ({ DEFAULT_TELEGRAM_UPDATE_TYPES, resolveTelegramAllowedUpdates } = + await import("./allowed-updates.js")); +}); describe("resolveTelegramAllowedUpdates", () => { it("includes the default update types plus reaction and channel post support", () => { diff --git a/extensions/telegram/src/audit.test.ts b/extensions/telegram/src/audit.test.ts index 021ffaf99cb..404796b49f3 100644 --- a/extensions/telegram/src/audit.test.ts +++ b/extensions/telegram/src/audit.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; let collectTelegramUnmentionedGroupIds: typeof import("./audit.js").collectTelegramUnmentionedGroupIds; let auditTelegramGroupMembership: typeof import("./audit.js").auditTelegramGroupMembership; @@ -31,12 +31,10 @@ async function auditSingleGroup() { } describe("telegram audit", () => { - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ collectTelegramUnmentionedGroupIds, auditTelegramGroupMembership } = await import("./audit.js")); - }); - - beforeEach(() => { fetchWithTimeoutMock.mockReset(); }); diff --git a/extensions/telegram/src/bot-message-context.dm-threads.test.ts b/extensions/telegram/src/bot-message-context.dm-threads.test.ts index 23fb0cdcc19..51cfcf52e8b 100644 --- a/extensions/telegram/src/bot-message-context.dm-threads.test.ts +++ b/extensions/telegram/src/bot-message-context.dm-threads.test.ts @@ -1,9 +1,20 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { - clearRuntimeConfigSnapshot, - setRuntimeConfigSnapshot, -} from "../../../src/config/config.js"; -import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest; +let clearRuntimeConfigSnapshot: typeof import("../../../src/config/config.js").clearRuntimeConfigSnapshot; +let setRuntimeConfigSnapshot: typeof import("../../../src/config/config.js").setRuntimeConfigSnapshot; + +beforeEach(async () => { + vi.resetModules(); + ({ buildTelegramMessageContextForTest } = await import("./bot-message-context.test-harness.js")); + ({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = + await import("../../../src/config/config.js")); + clearRuntimeConfigSnapshot(); +}); + +afterEach(() => { + clearRuntimeConfigSnapshot(); +}); describe("buildTelegramMessageContext dm thread sessions", () => { const buildContext = async (message: Record) => @@ -110,10 +121,6 @@ describe("buildTelegramMessageContext group sessions without forum", () => { }); describe("buildTelegramMessageContext direct peer routing", () => { - afterEach(() => { - clearRuntimeConfigSnapshot(); - }); - it("isolates dm sessions by sender id when chat id differs", async () => { const runtimeCfg = { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, diff --git a/extensions/telegram/src/bot-message-context.test-harness.ts b/extensions/telegram/src/bot-message-context.test-harness.ts index 27cf2764028..bc2e7bd48e0 100644 --- a/extensions/telegram/src/bot-message-context.test-harness.ts +++ b/extensions/telegram/src/bot-message-context.test-harness.ts @@ -1,9 +1,5 @@ import { vi } from "vitest"; -import { - buildTelegramMessageContext, - type BuildTelegramMessageContextParams, - type TelegramMediaRef, -} from "./bot-message-context.js"; +import type { BuildTelegramMessageContextParams, TelegramMediaRef } from "./bot-message-context.js"; export const baseTelegramMessageContextConfig = { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, @@ -24,7 +20,10 @@ type BuildTelegramMessageContextForTestParams = { export async function buildTelegramMessageContextForTest( params: BuildTelegramMessageContextForTestParams, -): Promise>> { +): Promise< + Awaited> +> { + const { buildTelegramMessageContext } = await import("./bot-message-context.js"); return await buildTelegramMessageContext({ primaryCtx: { message: { diff --git a/extensions/telegram/src/bot-native-commands.test.ts b/extensions/telegram/src/bot-native-commands.test.ts index fee2e282767..71850ea7280 100644 --- a/extensions/telegram/src/bot-native-commands.test.ts +++ b/extensions/telegram/src/bot-native-commands.test.ts @@ -33,7 +33,6 @@ vi.mock("./bot/delivery.replies.js", () => ({ let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands; import { - createCommandBot, createNativeCommandTestParams as createNativeCommandTestParamsBase, createPrivateCommandContext, deliverReplies as registeredDeliverReplies, @@ -80,6 +79,12 @@ function createNativeCommandTestParams( }); } +function resolveDeliverRepliesCalls() { + return deliveryMocks.deliverReplies.mock.calls.length > 0 + ? deliveryMocks.deliverReplies.mock.calls + : registeredDeliverReplies.mock.calls; +} + describe("registerTelegramNativeCommands", () => { beforeEach(async () => { vi.resetModules(); @@ -270,7 +275,8 @@ describe("registerTelegramNativeCommands", () => { expect(handler).toBeTruthy(); await handler?.(createPrivateCommandContext()); - expect(registeredDeliverReplies).toHaveBeenCalledWith( + const firstDeliverRepliesCall = resolveDeliverRepliesCalls().at(0) as [unknown] | undefined; + expect(firstDeliverRepliesCall?.[0]).toEqual( expect.objectContaining({ mediaLocalRoots: expect.arrayContaining([ expect.stringMatching(/[\\/]\.openclaw[\\/]workspace-work$/), @@ -324,7 +330,8 @@ describe("registerTelegramNativeCommands", () => { expect(handler).toBeTruthy(); await handler?.(createPrivateCommandContext()); - expect(registeredDeliverReplies).toHaveBeenCalledWith( + const firstDeliverRepliesCall = resolveDeliverRepliesCalls().at(0) as [unknown] | undefined; + expect(firstDeliverRepliesCall?.[0]).toEqual( expect.objectContaining({ silent: true, replies: [expect.objectContaining({ isError: true })], diff --git a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts index 91e2195d8dc..590bc145f1c 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts @@ -11,18 +11,9 @@ const { telegramBotRuntimeForTest, } = harness; -const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = - await import("./bot.js"); - -setTelegramBotRuntimeForTest( - telegramBotRuntimeForTest as unknown as Parameters[0], -); - -const createTelegramBot = (opts: Parameters[0]) => - createTelegramBotBase({ - ...opts, - telegramDeps: telegramBotDepsForTest, - }); +let createTelegramBot: ( + opts: Parameters[0], +) => ReturnType; const loadConfig = getLoadConfigMock(); @@ -69,6 +60,20 @@ function resolveFlushTimer(setTimeoutSpy: ReturnType) { } describe("createTelegramBot channel_post media", () => { + beforeEach(async () => { + vi.resetModules(); + const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = + await import("./bot.js"); + setTelegramBotRuntimeForTest( + telegramBotRuntimeForTest as unknown as Parameters[0], + ); + createTelegramBot = (opts) => + createTelegramBotBase({ + ...opts, + telegramDeps: telegramBotDepsForTest, + }); + }); + it("buffers channel_post media groups and processes them together", async () => { setOpenChannelPostConfig(); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index ae2d85fa8a7..b02e94a85a5 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { GetReplyOptions, MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; import { withEnvAsync } from "../../../test/helpers/extensions/env.js"; import { useFrozenTime, useRealTime } from "../../../test/helpers/extensions/frozen-time.js"; @@ -33,22 +33,11 @@ const { throttlerSpy, useSpy, } = harness; -import { resolveTelegramFetch } from "./fetch.js"; - -// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals. -const { - createTelegramBot: createTelegramBotBase, - getTelegramSequentialKey, - setTelegramBotRuntimeForTest, -} = await import("./bot.js"); -setTelegramBotRuntimeForTest( - telegramBotRuntimeForTest as unknown as Parameters[0], -); -const createTelegramBot = (opts: Parameters[0]) => - createTelegramBotBase({ - ...opts, - telegramDeps: telegramBotDepsForTest, - }); +let resolveTelegramFetch: typeof import("./fetch.js").resolveTelegramFetch; +let createTelegramBot: ( + opts: Parameters[0], +) => ReturnType; +let getTelegramSequentialKey: typeof import("./bot.js").getTelegramSequentialKey; const loadConfig = getLoadConfigMock(); const loadWebMedia = getLoadWebMediaMock(); @@ -92,6 +81,24 @@ describe("createTelegramBot", () => { afterAll(() => { process.env.TZ = ORIGINAL_TZ; }); + beforeEach(async () => { + vi.resetModules(); + ({ resolveTelegramFetch } = await import("./fetch.js")); + const { + createTelegramBot: createTelegramBotBase, + getTelegramSequentialKey: importedGetTelegramSequentialKey, + setTelegramBotRuntimeForTest, + } = await import("./bot.js"); + setTelegramBotRuntimeForTest( + telegramBotRuntimeForTest as unknown as Parameters[0], + ); + getTelegramSequentialKey = importedGetTelegramSequentialKey; + createTelegramBot = (opts) => + createTelegramBotBase({ + ...opts, + telegramDeps: telegramBotDepsForTest, + }); + }); // groupPolicy tests diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index 5fe9ff639f7..fd7837e06cf 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -27,22 +27,13 @@ const { wasSentByBot, } = await import("./bot.create-telegram-bot.test-harness.js"); -// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals. -const { listNativeCommandSpecs, listNativeCommandSpecsForConfig } = - await import("../../../src/auto-reply/commands-registry.js"); -const { loadSessionStore } = await import("../../../src/config/sessions.js"); -const { normalizeTelegramCommandName } = - await import("../../../src/config/telegram-custom-commands.js"); -const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = - await import("./bot.js"); -setTelegramBotRuntimeForTest( - telegramBotRuntimeForTest as unknown as Parameters[0], -); -const createTelegramBot = (opts: Parameters[0]) => - createTelegramBotBase({ - ...opts, - telegramDeps: telegramBotDepsForTest, - }); +let listNativeCommandSpecs: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecs; +let listNativeCommandSpecsForConfig: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecsForConfig; +let loadSessionStore: typeof import("../../../src/config/sessions.js").loadSessionStore; +let normalizeTelegramCommandName: typeof import("../../../src/config/telegram-custom-commands.js").normalizeTelegramCommandName; +let createTelegramBot: ( + opts: Parameters[0], +) => ReturnType; const loadConfig = getLoadConfigMock(); const readChannelAllowFromStore = getReadChannelAllowFromStoreMock(); @@ -77,6 +68,24 @@ describe("createTelegramBot", () => { }, }); }); + beforeEach(async () => { + vi.resetModules(); + ({ listNativeCommandSpecs, listNativeCommandSpecsForConfig } = + await import("../../../src/auto-reply/commands-registry.js")); + ({ loadSessionStore } = await import("../../../src/config/sessions.js")); + ({ normalizeTelegramCommandName } = + await import("../../../src/config/telegram-custom-commands.js")); + const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = + await import("./bot.js"); + setTelegramBotRuntimeForTest( + telegramBotRuntimeForTest as unknown as Parameters[0], + ); + createTelegramBot = (opts) => + createTelegramBotBase({ + ...opts, + telegramDeps: telegramBotDepsForTest, + }); + }); it("merges custom commands with native commands", async () => { const config = { diff --git a/extensions/whatsapp/src/channel.outbound.test.ts b/extensions/whatsapp/src/channel.outbound.test.ts index e45830dc57c..6bde780dd5f 100644 --- a/extensions/whatsapp/src/channel.outbound.test.ts +++ b/extensions/whatsapp/src/channel.outbound.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createWhatsAppPollFixture, expectWhatsAppPollSent, @@ -21,9 +21,14 @@ vi.mock("./runtime.js", () => ({ }), })); -import { whatsappPlugin } from "./channel.js"; +let whatsappPlugin: typeof import("./channel.js").whatsappPlugin; describe("whatsappPlugin outbound sendPoll", () => { + beforeEach(async () => { + vi.resetModules(); + ({ whatsappPlugin } = await import("./channel.js")); + }); + it("threads cfg into runtime sendPollWhatsApp call", async () => { const { cfg, poll, to, accountId } = createWhatsAppPollFixture(); diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index d83ef1dfea5..3d79b613631 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -96,7 +96,8 @@ vi.mock("./session.js", () => { }; }); -import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js"; +let monitorWebInbox: typeof import("./inbound.js").monitorWebInbox; +let resetWebInboundDedupe: typeof import("./inbound.js").resetWebInboundDedupe; let createWaSocket: typeof import("./session.js").createWaSocket; async function waitForMessage(onMessage: ReturnType) { @@ -115,12 +116,18 @@ describe("web inbound media saves with extension", () => { } beforeEach(() => { + vi.useRealTimers(); + vi.resetModules(); saveMediaBufferSpy.mockClear(); + }); + + beforeEach(async () => { + ({ monitorWebInbox, resetWebInboundDedupe } = await import("./inbound.js")); + ({ createWaSocket } = await import("./session.js")); resetWebInboundDedupe(); }); beforeAll(async () => { - ({ createWaSocket } = await import("./session.js")); await fs.rm(HOME, { recursive: true, force: true }); }); diff --git a/extensions/whatsapp/src/inbound/media.node.test.ts b/extensions/whatsapp/src/inbound/media.node.test.ts index 71d75309f6a..33479c42962 100644 --- a/extensions/whatsapp/src/inbound/media.node.test.ts +++ b/extensions/whatsapp/src/inbound/media.node.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const { normalizeMessageContent, downloadMediaMessage } = vi.hoisted(() => ({ normalizeMessageContent: vi.fn((msg: unknown) => msg), @@ -10,23 +10,31 @@ vi.mock("@whiskeysockets/baileys", () => ({ downloadMediaMessage, })); -import { downloadInboundMedia } from "./media.js"; +let downloadInboundMedia: typeof import("./media.js").downloadInboundMedia; const mockSock = { updateMediaMessage: vi.fn(), logger: { child: () => ({}) }, -} as never; +}; async function expectMimetype(message: Record, expected: string) { - const result = await downloadInboundMedia({ message } as never, mockSock); + const result = await downloadInboundMedia({ message } as never, mockSock as never); expect(result).toBeDefined(); expect(result?.mimetype).toBe(expected); } describe("downloadInboundMedia", () => { + beforeEach(async () => { + vi.resetModules(); + ({ downloadInboundMedia } = await import("./media.js")); + normalizeMessageContent.mockClear(); + downloadMediaMessage.mockClear(); + mockSock.updateMediaMessage.mockClear(); + }); + it("returns undefined for messages without media", async () => { const msg = { message: { conversation: "hello" } } as never; - const result = await downloadInboundMedia(msg, mockSock); + const result = await downloadInboundMedia(msg, mockSock as never); expect(result).toBeUndefined(); }); @@ -59,7 +67,7 @@ describe("downloadInboundMedia", () => { documentMessage: { mimetype: "application/pdf", fileName: "report.pdf" }, }, } as never; - const result = await downloadInboundMedia(msg, mockSock); + const result = await downloadInboundMedia(msg, mockSock as never); expect(result).toBeDefined(); expect(result?.mimetype).toBe("application/pdf"); expect(result?.fileName).toBe("report.pdf"); diff --git a/extensions/whatsapp/src/login.test.ts b/extensions/whatsapp/src/login.test.ts index 96a9cff2c10..2a3fc5aa71c 100644 --- a/extensions/whatsapp/src/login.test.ts +++ b/extensions/whatsapp/src/login.test.ts @@ -19,15 +19,17 @@ vi.mock("./session.js", () => { }; }); -import { loginWeb } from "./login.js"; import type { waitForWaConnection } from "./session.js"; - -const { createWaSocket } = await import("./session.js"); +let loginWeb: typeof import("./login.js").loginWeb; +let createWaSocket: typeof import("./session.js").createWaSocket; describe("web login", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.useFakeTimers(); vi.clearAllMocks(); + ({ loginWeb } = await import("./login.js")); + ({ createWaSocket } = await import("./session.js")); }); afterEach(() => { diff --git a/extensions/whatsapp/src/media.test.ts b/extensions/whatsapp/src/media.test.ts index 5e93f80be28..3221a0084f9 100644 --- a/extensions/whatsapp/src/media.test.ts +++ b/extensions/whatsapp/src/media.test.ts @@ -2,19 +2,18 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import sharp from "sharp"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveStateDir } from "../../../src/config/paths.js"; import { resolvePreferredOpenClawTmpDir } from "../../../src/infra/tmp-openclaw-dir.js"; import { optimizeImageToPng } from "../../../src/media/image-ops.js"; import { mockPinnedHostnameResolution } from "../../../src/test-helpers/ssrf.js"; import { captureEnv } from "../../../test/helpers/extensions/env.js"; import { sendVoiceMessageDiscord } from "../../discord/src/send.js"; -import { - LocalMediaAccessError, - loadWebMedia, - loadWebMediaRaw, - optimizeImageToJpeg, -} from "./media.js"; + +let LocalMediaAccessError: typeof import("./media.js").LocalMediaAccessError; +let loadWebMedia: typeof import("./media.js").loadWebMedia; +let loadWebMediaRaw: typeof import("./media.js").loadWebMediaRaw; +let optimizeImageToJpeg: typeof import("./media.js").optimizeImageToJpeg; const convertHeicToJpegMock = vi.fn(); @@ -78,6 +77,8 @@ function cloneStatWithDev(stat: T, dev: numb } beforeAll(async () => { + ({ LocalMediaAccessError, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } = + await import("./media.js")); fixtureRoot = await fs.mkdtemp( path.join(resolvePreferredOpenClawTmpDir(), "openclaw-media-test-"), ); @@ -136,6 +137,12 @@ afterEach(() => { vi.clearAllMocks(); }); +beforeEach(async () => { + vi.resetModules(); + ({ LocalMediaAccessError, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } = + await import("./media.js")); +}); + describe("web media loading", () => { beforeAll(() => { // Ensure state dir is stable and not influenced by other tests that stub OPENCLAW_STATE_DIR. diff --git a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts index 756eea07bab..baeb74d3458 100644 --- a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts @@ -158,7 +158,7 @@ describe("web monitor inbox", () => { () => { expect(sock.sendMessage).toHaveBeenCalledTimes(1); }, - { timeout: 2_000, interval: 5 }, + { timeout: 5_000, interval: 5 }, ); expect(onMessage).not.toHaveBeenCalled(); expectPairingPromptSent(sock, "999@s.whatsapp.net", "+999"); @@ -246,7 +246,7 @@ describe("web monitor inbox", () => { }, ]); }, - { timeout: 2_000, interval: 5 }, + { timeout: 5_000, interval: 5 }, ); // Verify it WAS NOT passed to onMessage diff --git a/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts b/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts index 18394111a74..726be66f31b 100644 --- a/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts @@ -1,11 +1,5 @@ -import crypto from "node:crypto"; -import fsSync from "node:fs"; -import os from "node:os"; -import path from "node:path"; import "./monitor-inbox.test-harness.js"; -import { describe, expect, it, vi } from "vitest"; -import { setLoggerOverride } from "../../../src/logging.js"; -import { monitorWebInbox } from "./inbound.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_ACCOUNT_ID, getAuthDir, @@ -13,10 +7,31 @@ import { installWebMonitorInboxUnitTestHooks, mockLoadConfig, } from "./monitor-inbox.test-harness.js"; +let monitorWebInbox: typeof import("./inbound.js").monitorWebInbox; +const inboundLoggerInfoMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/text-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getChildLogger: () => ({ + info: inboundLoggerInfoMock, + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }), + }; +}); describe("web monitor inbox", () => { installWebMonitorInboxUnitTestHooks(); + beforeEach(async () => { + vi.resetModules(); + inboundLoggerInfoMock.mockReset(); + ({ monitorWebInbox } = await import("./inbound.js")); + }); + async function openMonitor(onMessage = vi.fn()) { return await monitorWebInbox({ verbose: false, @@ -120,10 +135,7 @@ describe("web monitor inbox", () => { expect(sock.ws.close).toHaveBeenCalledTimes(1); }); - it("logs inbound bodies to file", async () => { - const logPath = path.join(os.tmpdir(), `openclaw-log-test-${crypto.randomUUID()}.log`); - setLoggerOverride({ level: "trace", file: logPath }); - + it("logs inbound bodies through the inbound child logger", async () => { const { listener } = await runSingleUpsertAndCapture({ type: "notify", messages: [ @@ -136,15 +148,13 @@ describe("web monitor inbox", () => { ], }); - await vi.waitFor( - () => { - expect(fsSync.existsSync(logPath)).toBe(true); - }, - { timeout: 2_000, interval: 5 }, + expect(inboundLoggerInfoMock).toHaveBeenCalledWith( + expect.objectContaining({ + body: "ping", + from: "+999", + }), + "inbound message", ); - const content = fsSync.readFileSync(logPath, "utf-8"); - expect(content).toMatch(/web-inbound/); - expect(content).toMatch(/ping/); await listener.close(); }); diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 195ad1f8f93..a65d3d91c5f 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -149,7 +149,8 @@ export async function waitForMessageCalls(onMessage: ReturnType, c () => { expect(onMessage).toHaveBeenCalledTimes(count); }, - { timeout: 2_000, interval: 5 }, + // Channel-suite workers can be saturated under no-isolate CI runs. + { timeout: 5_000, interval: 5 }, ); } @@ -215,6 +216,7 @@ export function installWebMonitorInboxUnitTestHooks(opts?: { authDir?: boolean } const createAuthDir = opts?.authDir ?? true; beforeEach(async () => { + vi.useRealTimers(); vi.resetModules(); vi.clearAllMocks(); sessionState.sock = createMockSock(); diff --git a/extensions/whatsapp/src/outbound-adapter.poll.test.ts b/extensions/whatsapp/src/outbound-adapter.poll.test.ts index 5e23748a233..6d6aac486ad 100644 --- a/extensions/whatsapp/src/outbound-adapter.poll.test.ts +++ b/extensions/whatsapp/src/outbound-adapter.poll.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; const hoisted = vi.hoisted(() => ({ @@ -15,9 +15,14 @@ vi.mock("./send.js", () => ({ sendReactionWhatsApp: hoisted.sendReactionWhatsApp, })); -import { whatsappOutbound } from "./outbound-adapter.js"; +let whatsappOutbound: typeof import("./outbound-adapter.js").whatsappOutbound; describe("whatsappOutbound sendPoll", () => { + beforeEach(async () => { + vi.resetModules(); + ({ whatsappOutbound } = await import("./outbound-adapter.js")); + }); + it("threads cfg through poll send options", async () => { const cfg = { marker: "resolved-cfg" } as OpenClawConfig; const poll = { diff --git a/src/browser/chrome.test.ts b/src/browser/chrome.test.ts index ee4cb8541c3..c3d385d7e17 100644 --- a/src/browser/chrome.test.ts +++ b/src/browser/chrome.test.ts @@ -4,7 +4,7 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { WebSocketServer } from "ws"; import { decorateOpenClawProfile, @@ -112,6 +112,10 @@ describe("browser chrome profile decoration", () => { fixtureRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-suite-")); }); + beforeEach(() => { + vi.useRealTimers(); + }); + afterAll(async () => { if (fixtureRoot) { await fsp.rm(fixtureRoot, { recursive: true, force: true }); @@ -206,6 +210,10 @@ describe("browser chrome helpers", () => { return vi.spyOn(fs, "existsSync").mockImplementation((p) => match(String(p))); } + beforeEach(() => { + vi.useRealTimers(); + }); + afterEach(() => { vi.unstubAllEnvs(); vi.unstubAllGlobals(); diff --git a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts index fa1e0c01e7d..5633520f97a 100644 --- a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts +++ b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { installPwToolsCoreTestHooks, setPwToolsCoreCurrentPage, @@ -6,9 +6,14 @@ import { } from "./pw-tools-core.test-harness.js"; installPwToolsCoreTestHooks(); -const mod = await import("./pw-tools-core.js"); +let mod: typeof import("./pw-tools-core.js"); describe("pw-tools-core", () => { + beforeEach(async () => { + vi.resetModules(); + mod = await import("./pw-tools-core.js"); + }); + it("clamps timeoutMs for scrollIntoView", async () => { const scrollIntoViewIfNeeded = vi.fn(async () => {}); setPwToolsCoreCurrentRefLocator({ scrollIntoViewIfNeeded }); diff --git a/src/browser/pw-tools-core.interactions.batch.test.ts b/src/browser/pw-tools-core.interactions.batch.test.ts index 2801ebe8190..a2fe18c3cf4 100644 --- a/src/browser/pw-tools-core.interactions.batch.test.ts +++ b/src/browser/pw-tools-core.interactions.batch.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; let page: { evaluate: ReturnType } | null = null; @@ -34,11 +34,9 @@ vi.mock("./pw-tools-core.snapshot.js", () => ({ let batchViaPlaywright: typeof import("./pw-tools-core.interactions.js").batchViaPlaywright; describe("batchViaPlaywright", () => { - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ batchViaPlaywright } = await import("./pw-tools-core.interactions.js")); - }); - - beforeEach(() => { vi.clearAllMocks(); page = { evaluate: vi.fn(async () => "ok"), diff --git a/src/browser/pw-tools-core.interactions.set-input-files.test.ts b/src/browser/pw-tools-core.interactions.set-input-files.test.ts index 93dbf0c44c5..cfe2bc8673b 100644 --- a/src/browser/pw-tools-core.interactions.set-input-files.test.ts +++ b/src/browser/pw-tools-core.interactions.set-input-files.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; let page: Record | null = null; let locator: Record | null = null; @@ -54,11 +54,9 @@ function seedSingleLocatorPage(): { setInputFiles: ReturnType } { } describe("setInputFilesViaPlaywright", () => { - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js")); - }); - - beforeEach(() => { vi.clearAllMocks(); page = null; locator = null; diff --git a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts index 16264ba9eb3..9419bab86cd 100644 --- a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts +++ b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_UPLOAD_DIR } from "./paths.js"; import { installPwToolsCoreTestHooks, @@ -9,9 +9,14 @@ import { } from "./pw-tools-core.test-harness.js"; installPwToolsCoreTestHooks(); -const mod = await import("./pw-tools-core.js"); +let mod: typeof import("./pw-tools-core.js"); describe("pw-tools-core", () => { + beforeEach(async () => { + vi.resetModules(); + mod = await import("./pw-tools-core.js"); + }); + it("last file-chooser arm wins", async () => { const firstPath = path.join(DEFAULT_UPLOAD_DIR, `vitest-arm-1-${crypto.randomUUID()}.txt`); const secondPath = path.join(DEFAULT_UPLOAD_DIR, `vitest-arm-2-${crypto.randomUUID()}.txt`); diff --git a/src/browser/pw-tools-core.screenshots-element-selector.test.ts b/src/browser/pw-tools-core.screenshots-element-selector.test.ts index 3eb7e333db0..3b5cd15d631 100644 --- a/src/browser/pw-tools-core.screenshots-element-selector.test.ts +++ b/src/browser/pw-tools-core.screenshots-element-selector.test.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_UPLOAD_DIR } from "./paths.js"; import { getPwToolsCoreSessionMocks, @@ -12,7 +12,7 @@ import { installPwToolsCoreTestHooks(); const sessionMocks = getPwToolsCoreSessionMocks(); -const mod = await import("./pw-tools-core.js"); +let mod: typeof import("./pw-tools-core.js"); function createFileChooserPageMocks() { const fileChooser = { setFiles: vi.fn(async () => {}) }; @@ -26,6 +26,11 @@ function createFileChooserPageMocks() { } describe("pw-tools-core", () => { + beforeEach(async () => { + vi.resetModules(); + mod = await import("./pw-tools-core.js"); + }); + it("screenshots an element selector", async () => { const elementScreenshot = vi.fn(async () => Buffer.from("E")); const page = { diff --git a/src/browser/routes/agent.snapshot.test.ts b/src/browser/routes/agent.snapshot.test.ts index b31ea1c3e7d..5a3b6f31dbf 100644 --- a/src/browser/routes/agent.snapshot.test.ts +++ b/src/browser/routes/agent.snapshot.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveTargetIdAfterNavigate } from "./agent.snapshot.js"; type Tab = { targetId: string; url: string }; @@ -8,6 +8,10 @@ function staticListTabs(tabs: Tab[]): () => Promise { } describe("resolveTargetIdAfterNavigate", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + it("returns original targetId when old target still exists (no swap)", async () => { const result = await resolveTargetIdAfterNavigate({ oldTargetId: "old-123", diff --git a/src/browser/server.control-server.test-harness.ts b/src/browser/server.control-server.test-harness.ts index 04a0f98c9ec..346708f7e9f 100644 --- a/src/browser/server.control-server.test-harness.ts +++ b/src/browser/server.control-server.test-harness.ts @@ -401,6 +401,7 @@ export async function cleanupBrowserControlServerTestContext(): Promise { export function installBrowserControlServerHooks() { beforeEach(async () => { + vi.useRealTimers(); cdpMocks.createTargetViaCdp.mockImplementation(async () => { if (state.createTargetId) { return { targetId: state.createTargetId };