From 5fb7a1363f25c6a23eacae034a1ee36768650eee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Mar 2026 06:53:29 +0000 Subject: [PATCH] fix: stabilize full gate --- .../bot.create-telegram-bot.test-harness.ts | 83 ++++++----- scripts/test-parallel.mjs | 86 +++++++++-- src/acp/control-plane/manager.test.ts | 9 +- src/acp/persistent-bindings.test.ts | 25 +++- src/acp/runtime/session-meta.test.ts | 8 +- src/channels/plugins/actions/actions.test.ts | 18 ++- .../plugins/whatsapp-heartbeat.test.ts | 41 +++--- src/channels/session.test.ts | 15 +- src/cli/command-secret-gateway.test.ts | 10 +- src/cli/memory-cli.test.ts | 21 +-- src/cli/pairing-cli.test.ts | 8 +- src/cli/prompt.test.ts | 33 +++-- src/config/logging.test.ts | 10 +- src/config/sessions/delivery-info.test.ts | 7 +- .../store.pruning.integration.test.ts | 15 +- ...ent.delivery-target-thread-session.test.ts | 73 +++++----- .../run.sandbox-config-preserved.test.ts | 14 +- ...runs-one-shot-main-job-disables-it.test.ts | 133 ------------------ src/infra/boundary-file-read.test.ts | 10 +- src/infra/channel-summary.test.ts | 12 +- src/infra/env.test.ts | 15 +- src/infra/exec-approval-surface.test.ts | 118 ++++++++++------ src/infra/exec-approvals-store.test.ts | 39 +++-- src/infra/net/proxy-fetch.test.ts | 17 ++- src/infra/net/ssrf.dispatcher.test.ts | 11 +- .../net/undici-global-dispatcher.test.ts | 19 ++- src/infra/openclaw-root.test.ts | 8 +- src/infra/outbound/agent-delivery.test.ts | 12 +- src/infra/outbound/channel-selection.test.ts | 23 ++- src/infra/outbound/deliver.lifecycle.test.ts | 10 +- src/infra/outbound/deliver.test.ts | 13 +- src/infra/outbound/identity.test.ts | 12 +- .../message-action-runner.media.test.ts | 24 +++- .../message-action-runner.poll.test.ts | 41 +++--- .../message-action-runner.threading.test.ts | 31 ++-- src/infra/outbound/message.channels.test.ts | 8 +- src/infra/outbound/message.test.ts | 7 +- .../outbound/outbound-send-service.test.ts | 9 +- src/infra/outbound/session-context.test.ts | 17 ++- .../outbound/target-normalization.test.ts | 63 ++++++--- src/infra/outbound/target-resolver.test.ts | 31 ++-- .../targets.channel-resolution.test.ts | 7 +- src/infra/pairing-token.test.ts | 14 +- src/infra/ports.test.ts | 13 +- src/infra/provider-usage.auth.plugin.test.ts | 6 +- src/infra/provider-usage.load.plugin.test.ts | 6 +- src/infra/restart-stale-pids.test.ts | 14 +- src/infra/restart.test.ts | 13 +- src/infra/secure-random.test.ts | 10 +- src/infra/session-maintenance-warning.test.ts | 43 +++--- src/infra/transport-ready.test.ts | 62 ++++---- src/infra/windows-task-restart.test.ts | 11 +- src/infra/wsl.test.ts | 13 +- src/media-understanding/apply.test.ts | 62 ++++---- .../providers/image.test.ts | 80 +++++------ src/media/fetch.telegram-network.test.ts | 18 ++- src/media/input-files.fetch-guard.test.ts | 10 +- src/media/store.outside-workspace.test.ts | 15 +- src/memory/batch-http.test.ts | 16 ++- src/memory/embedding-manager.test-harness.ts | 26 ++-- src/memory/embeddings-remote-fetch.test.ts | 9 +- src/memory/embeddings-voyage.test.ts | 15 +- src/memory/embeddings.test.ts | 21 ++- src/memory/index.test.ts | 9 +- src/memory/manager.atomic-reindex.test.ts | 14 +- src/memory/manager.batch.test.ts | 25 ++-- src/memory/manager.embedding-batches.test.ts | 17 +-- src/memory/manager.get-concurrency.test.ts | 18 ++- src/memory/manager.mistral-provider.test.ts | 8 +- src/memory/manager.vector-dedupe.test.ts | 11 +- src/memory/manager.watcher-config.test.ts | 13 +- src/memory/post-json.test.ts | 15 +- src/memory/test-manager-helpers.ts | 4 +- src/pairing/setup-code.test.ts | 11 +- src/plugin-sdk/outbound-media.test.ts | 13 +- src/plugins/contracts/auth.contract.test.ts | 40 ++++-- src/plugins/provider-runtime.test.ts | 73 ++++++---- src/plugins/providers.test.ts | 9 +- src/plugins/tools.optional.test.ts | 7 +- src/plugins/wired-hooks-compaction.test.ts | 34 ++--- src/process/command-queue.test.ts | 42 ++++-- src/process/exec.no-output-timer.test.ts | 11 +- src/process/exec.windows.test.ts | 10 +- src/process/kill-tree.test.ts | 7 +- src/process/supervisor/adapters/child.test.ts | 8 +- src/process/supervisor/adapters/pty.test.ts | 8 +- .../supervisor/supervisor.pty-command.test.ts | 8 +- src/security/windows-acl.test.ts | 31 ++-- src/tts/edge-tts-validation.test.ts | 11 +- src/tts/tts.test.ts | 64 ++++++--- src/utils/message-channel.ts | 48 +++++-- src/whatsapp/resolve-outbound-target.test.ts | 7 +- 92 files changed, 1381 insertions(+), 838 deletions(-) diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index 69c0557ee3a..24f8e50b706 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -68,28 +68,59 @@ export function getUpsertChannelPairingRequestMock(): AnyAsyncMock { return upsertChannelPairingRequest; } -vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readChannelAllowFromStore, + upsertChannelPairingRequest, + }; +}); const skillCommandsHoisted = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), + replySpy: vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { + await opts?.onReplyStart?.(); + return undefined; + }) as MockFn< + ( + ctx: MsgContext, + opts?: GetReplyOptions, + configOverride?: OpenClawConfig, + ) => Promise + >, })); export const listSkillCommandsForAgents = skillCommandsHoisted.listSkillCommandsForAgents; +export const replySpy = skillCommandsHoisted.replySpy; -vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ - listSkillCommandsForAgents, -})); +vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, + getReplyFromConfig: skillCommandsHoisted.replySpy, + __replySpy: skillCommandsHoisted.replySpy, + dispatchReplyWithBufferedBlockDispatcher: vi.fn( + async ({ ctx, replyOptions }: { ctx: MsgContext; replyOptions?: GetReplyOptions }) => { + await skillCommandsHoisted.replySpy(ctx, replyOptions); + return { queuedFinal: false }; + }, + ), + }; +}); const systemEventsHoisted = vi.hoisted(() => ({ enqueueSystemEventSpy: vi.fn(), })); export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy; -vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({ - enqueueSystemEvent: enqueueSystemEventSpy, -})); +vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy, + }; +}); const sentMessageCacheHoisted = vi.hoisted(() => ({ wasSentByBot: vi.fn(() => false), @@ -97,7 +128,7 @@ const sentMessageCacheHoisted = vi.hoisted(() => ({ export const wasSentByBot = sentMessageCacheHoisted.wasSentByBot; vi.mock("./sent-message-cache.js", () => ({ - wasSentByBot, + wasSentByBot: sentMessageCacheHoisted.wasSentByBot, recordSentMessage: vi.fn(), clearSentMessageCache: vi.fn(), })); @@ -182,36 +213,24 @@ vi.mock("grammy", () => ({ InputFile: class {}, })); -const sequentializeMiddleware = vi.fn(); -export const sequentializeSpy: AnyMock = vi.fn(() => sequentializeMiddleware); +const runnerHoisted = vi.hoisted(() => ({ + sequentializeMiddleware: vi.fn(), + sequentializeSpy: vi.fn(), + throttlerSpy: vi.fn(() => "throttler"), +})); +export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy; export let sequentializeKey: ((ctx: unknown) => string) | undefined; vi.mock("@grammyjs/runner", () => ({ sequentialize: (keyFn: (ctx: unknown) => string) => { sequentializeKey = keyFn; - return sequentializeSpy(); + return runnerHoisted.sequentializeSpy(); }, })); -export const throttlerSpy: AnyMock = vi.fn(() => "throttler"); +export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy; vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -export const replySpy: MockFn< - ( - ctx: MsgContext, - opts?: GetReplyOptions, - configOverride?: OpenClawConfig, - ) => Promise -> = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; -}); - -vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ - getReplyFromConfig: replySpy, - __replySpy: replySpy, + apiThrottler: () => runnerHoisted.throttlerSpy(), })); export const getOnHandler = (event: string) => { diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 76a0be3b466..dd933b4e4ae 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -93,6 +93,19 @@ const unitIsolatedFilesRaw = [ "src/infra/git-commit.test.ts", ]; const unitIsolatedFiles = unitIsolatedFilesRaw.filter((file) => fs.existsSync(file)); +const unitSingletonIsolatedFilesRaw = []; +const unitSingletonIsolatedFiles = unitSingletonIsolatedFilesRaw.filter((file) => + fs.existsSync(file), +); +const unitVmForkSingletonFilesRaw = [ + "src/channels/plugins/contracts/inbound.telegram.contract.test.ts", +]; +const unitVmForkSingletonFiles = unitVmForkSingletonFilesRaw.filter((file) => fs.existsSync(file)); +const groupedUnitIsolatedFiles = unitIsolatedFiles.filter( + (file) => !unitSingletonIsolatedFiles.includes(file), +); +const channelSingletonFilesRaw = []; +const channelSingletonFiles = channelSingletonFilesRaw.filter((file) => fs.existsSync(file)); const children = new Set(); const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; @@ -139,20 +152,55 @@ const runs = [ "vitest.unit.config.ts", `--pool=${useVmForks ? "vmForks" : "forks"}`, ...(disableIsolation ? ["--isolate=false"] : []), - ...unitIsolatedFiles.flatMap((file) => ["--exclude", file]), + ...[ + ...unitIsolatedFiles, + ...unitSingletonIsolatedFiles, + ...unitVmForkSingletonFiles, + ].flatMap((file) => ["--exclude", file]), ], }, - { - name: "unit-isolated", + ...(groupedUnitIsolatedFiles.length > 0 + ? [ + { + name: "unit-isolated", + args: [ + "vitest", + "run", + "--config", + "vitest.unit.config.ts", + "--pool=forks", + ...groupedUnitIsolatedFiles, + ], + }, + ] + : []), + ...unitSingletonIsolatedFiles.map((file) => ({ + name: `${path.basename(file, ".test.ts")}-isolated`, args: [ "vitest", "run", "--config", "vitest.unit.config.ts", - "--pool=forks", - ...unitIsolatedFiles, + `--pool=${useVmForks ? "vmForks" : "forks"}`, + file, ], - }, + })), + ...unitVmForkSingletonFiles.map((file) => ({ + name: `${path.basename(file, ".test.ts")}-vmforks`, + args: [ + "vitest", + "run", + "--config", + "vitest.unit.config.ts", + `--pool=${useVmForks ? "vmForks" : "forks"}`, + ...(disableIsolation ? ["--isolate=false"] : []), + file, + ], + })), + ...channelSingletonFiles.map((file) => ({ + name: `${path.basename(file, ".test.ts")}-channels-isolated`, + args: ["vitest", "run", "--config", "vitest.channels.config.ts", "--pool=forks", file], + })), ] : [ { @@ -380,9 +428,24 @@ const resolveFilterMatches = (fileFilter) => { } return allKnownTestFiles.filter((file) => file.includes(normalizedFilter)); }; +const isVmForkSingletonUnitFile = (fileFilter) => unitVmForkSingletonFiles.includes(fileFilter); const createTargetedEntry = (owner, isolated, filters) => { const name = isolated ? `${owner}-isolated` : owner; const forceForks = isolated; + if (owner === "unit-vmforks") { + return { + name, + args: [ + "vitest", + "run", + "--config", + "vitest.unit.config.ts", + `--pool=${useVmForks ? "vmForks" : "forks"}`, + ...(disableIsolation ? ["--isolate=false"] : []), + ...filters, + ], + }; + } if (owner === "unit") { return { name, @@ -460,16 +523,19 @@ const targetedEntries = (() => { const groups = passthroughFileFilters.reduce((acc, fileFilter) => { const matchedFiles = resolveFilterMatches(fileFilter); if (matchedFiles.length === 0) { - const target = inferTarget(normalizeRepoPath(fileFilter)); - const key = `${target.owner}:${target.isolated ? "isolated" : "default"}`; + const normalizedFile = normalizeRepoPath(fileFilter); + const target = inferTarget(normalizedFile); + const owner = isVmForkSingletonUnitFile(normalizedFile) ? "unit-vmforks" : target.owner; + const key = `${owner}:${target.isolated ? "isolated" : "default"}`; const files = acc.get(key) ?? []; - files.push(normalizeRepoPath(fileFilter)); + files.push(normalizedFile); acc.set(key, files); return acc; } for (const matchedFile of matchedFiles) { const target = inferTarget(matchedFile); - const key = `${target.owner}:${target.isolated ? "isolated" : "default"}`; + const owner = isVmForkSingletonUnitFile(matchedFile) ? "unit-vmforks" : target.owner; + const key = `${owner}:${target.isolated ? "isolated" : "default"}`; const files = acc.get(key) ?? []; files.push(matchedFile); acc.set(key, files); diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index 8152944834c..66faa84b1d3 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js"; -import { AcpRuntimeError } from "../runtime/errors.js"; import type { AcpRuntime, AcpRuntimeCapabilities } from "../runtime/types.js"; const hoisted = vi.hoisted(() => { @@ -32,7 +31,8 @@ vi.mock("../runtime/registry.js", async (importOriginal) => { }; }); -const { AcpSessionManager } = await import("./manager.js"); +let AcpSessionManager: typeof import("./manager.js").AcpSessionManager; +let AcpRuntimeError: typeof import("../runtime/errors.js").AcpRuntimeError; const baseCfg = { acp: { @@ -146,7 +146,10 @@ function extractRuntimeOptionsFromUpserts(): Array { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ AcpSessionManager } = await import("./manager.js")); + ({ AcpRuntimeError } = await import("../runtime/errors.js")); hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]); hoisted.readAcpSessionEntryMock.mockReset(); hoisted.upsertAcpSessionMetaMock.mockReset().mockResolvedValue(null); diff --git a/src/acp/persistent-bindings.test.ts b/src/acp/persistent-bindings.test.ts index 147c4a455c9..cb815b9d948 100644 --- a/src/acp/persistent-bindings.test.ts +++ b/src/acp/persistent-bindings.test.ts @@ -27,13 +27,13 @@ vi.mock("./runtime/session-meta.js", () => ({ readAcpSessionEntry: sessionMetaMocks.readAcpSessionEntry, })); -import { - buildConfiguredAcpSessionKey, - ensureConfiguredAcpBindingSession, - resetAcpSessionInPlace, - resolveConfiguredAcpBindingRecord, - resolveConfiguredAcpBindingSpecBySessionKey, -} from "./persistent-bindings.js"; +type PersistentBindingsModule = typeof import("./persistent-bindings.js"); + +let buildConfiguredAcpSessionKey: PersistentBindingsModule["buildConfiguredAcpSessionKey"]; +let ensureConfiguredAcpBindingSession: PersistentBindingsModule["ensureConfiguredAcpBindingSession"]; +let resetAcpSessionInPlace: PersistentBindingsModule["resetAcpSessionInPlace"]; +let resolveConfiguredAcpBindingRecord: PersistentBindingsModule["resolveConfiguredAcpBindingRecord"]; +let resolveConfiguredAcpBindingSpecBySessionKey: PersistentBindingsModule["resolveConfiguredAcpBindingSpecBySessionKey"]; type ConfiguredBinding = NonNullable[number]; type BindingRecordInput = Parameters[0]; @@ -184,6 +184,17 @@ beforeEach(() => { sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined); }); +beforeEach(async () => { + vi.resetModules(); + ({ + buildConfiguredAcpSessionKey, + ensureConfiguredAcpBindingSession, + resetAcpSessionInPlace, + resolveConfiguredAcpBindingRecord, + resolveConfiguredAcpBindingSpecBySessionKey, + } = await import("./persistent-bindings.js")); +}); + describe("resolveConfiguredAcpBindingRecord", () => { it("resolves discord channel ACP binding from top-level typed bindings", () => { const cfg = createCfgWithBindings([ diff --git a/src/acp/runtime/session-meta.test.ts b/src/acp/runtime/session-meta.test.ts index f9a0f399f81..b5279d6f0ac 100644 --- a/src/acp/runtime/session-meta.test.ts +++ b/src/acp/runtime/session-meta.test.ts @@ -22,10 +22,14 @@ vi.mock("../../config/sessions.js", async () => { }; }); -const { listAcpSessionEntries } = await import("./session-meta.js"); +type SessionMetaModule = typeof import("./session-meta.js"); + +let listAcpSessionEntries: SessionMetaModule["listAcpSessionEntries"]; describe("listAcpSessionEntries", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ listAcpSessionEntries } = await import("./session-meta.js")); vi.clearAllMocks(); }); diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index cd33be0a3e2..322e0f618f4 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -24,11 +24,11 @@ vi.mock("../../../agents/tools/slack-actions.js", () => ({ handleSlackAction, })); -const { discordMessageActions } = await import("./discord.js"); -const { handleDiscordMessageAction } = await import("./discord/handle-action.js"); -const { telegramMessageActions } = await import("./telegram.js"); -const { signalMessageActions } = await import("./signal.js"); -const { createSlackActions } = await import("../slack.actions.js"); +let discordMessageActions: typeof import("./discord.js").discordMessageActions; +let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction; +let telegramMessageActions: typeof import("./telegram.js").telegramMessageActions; +let signalMessageActions: typeof import("./signal.js").signalMessageActions; +let createSlackActions: typeof import("../slack.actions.js").createSlackActions; function telegramCfg(): OpenClawConfig { return { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig; @@ -191,7 +191,13 @@ async function expectSlackSendRejected(params: Record, error: R expect(handleSlackAction).not.toHaveBeenCalled(); } -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + ({ discordMessageActions } = await import("./discord.js")); + ({ handleDiscordMessageAction } = await import("./discord/handle-action.js")); + ({ telegramMessageActions } = await import("./telegram.js")); + ({ signalMessageActions } = await import("./signal.js")); + ({ createSlackActions } = await import("../slack.actions.js")); vi.clearAllMocks(); }); diff --git a/src/channels/plugins/whatsapp-heartbeat.test.ts b/src/channels/plugins/whatsapp-heartbeat.test.ts index f4b0945a400..3cc6531eca1 100644 --- a/src/channels/plugins/whatsapp-heartbeat.test.ts +++ b/src/channels/plugins/whatsapp-heartbeat.test.ts @@ -1,18 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../../config/sessions.js", () => ({ - loadSessionStore: vi.fn(), - resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"), -})); - -vi.mock("../../pairing/pairing-store.js", () => ({ - readChannelAllowFromStoreSync: vi.fn(() => []), -})); - import type { OpenClawConfig } from "../../config/config.js"; -import { loadSessionStore } from "../../config/sessions.js"; -import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js"; -import { resolveWhatsAppHeartbeatRecipients } from "./whatsapp-heartbeat.js"; + +const loadSessionStoreMock = vi.hoisted(() => vi.fn()); +const readChannelAllowFromStoreSyncMock = vi.hoisted(() => vi.fn<() => string[]>(() => [])); + +type WhatsAppHeartbeatModule = typeof import("./whatsapp-heartbeat.js"); + +let resolveWhatsAppHeartbeatRecipients: WhatsAppHeartbeatModule["resolveWhatsAppHeartbeatRecipients"]; function makeCfg(overrides?: Partial): OpenClawConfig { return { @@ -23,12 +17,12 @@ function makeCfg(overrides?: Partial): OpenClawConfig { } describe("resolveWhatsAppHeartbeatRecipients", () => { - function setSessionStore(store: ReturnType) { - vi.mocked(loadSessionStore).mockReturnValue(store); + function setSessionStore(store: Record) { + loadSessionStoreMock.mockReturnValue(store); } function setAllowFromStore(entries: string[]) { - vi.mocked(readChannelAllowFromStoreSync).mockReturnValue(entries); + readChannelAllowFromStoreSyncMock.mockReturnValue(entries); } function resolveWith( @@ -45,9 +39,18 @@ describe("resolveWhatsAppHeartbeatRecipients", () => { setAllowFromStore(["+15550000001"]); } - beforeEach(() => { - vi.mocked(loadSessionStore).mockClear(); - vi.mocked(readChannelAllowFromStoreSync).mockClear(); + beforeEach(async () => { + vi.resetModules(); + loadSessionStoreMock.mockReset(); + readChannelAllowFromStoreSyncMock.mockReset(); + vi.doMock("../../config/sessions.js", () => ({ + loadSessionStore: loadSessionStoreMock, + resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"), + })); + vi.doMock("../../pairing/pairing-store.js", () => ({ + readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock, + })); + ({ resolveWhatsAppHeartbeatRecipients } = await import("./whatsapp-heartbeat.js")); setAllowFromStore([]); }); diff --git a/src/channels/session.test.ts b/src/channels/session.test.ts index b1415bbb53d..530346bddb4 100644 --- a/src/channels/session.test.ts +++ b/src/channels/session.test.ts @@ -9,6 +9,10 @@ vi.mock("../config/sessions.js", () => ({ updateLastRoute: (args: unknown) => updateLastRouteMock(args), })); +type SessionModule = typeof import("./session.js"); + +let recordInboundSession: SessionModule["recordInboundSession"]; + describe("recordInboundSession", () => { const ctx: MsgContext = { Provider: "telegram", @@ -17,14 +21,14 @@ describe("recordInboundSession", () => { OriginatingTo: "telegram:1234", }; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ recordInboundSession } = await import("./session.js")); recordSessionMetaFromInboundMock.mockClear(); updateLastRouteMock.mockClear(); }); it("does not pass ctx when updating a different session key", async () => { - const { recordInboundSession } = await import("./session.js"); - await recordInboundSession({ storePath: "/tmp/openclaw-session-store.json", sessionKey: "agent:main:telegram:1234:thread:42", @@ -50,8 +54,6 @@ describe("recordInboundSession", () => { }); it("passes ctx when updating the same session key", async () => { - const { recordInboundSession } = await import("./session.js"); - await recordInboundSession({ storePath: "/tmp/openclaw-session-store.json", sessionKey: "agent:main:telegram:1234:thread:42", @@ -77,8 +79,6 @@ describe("recordInboundSession", () => { }); it("normalizes mixed-case session keys before recording and route updates", async () => { - const { recordInboundSession } = await import("./session.js"); - await recordInboundSession({ storePath: "/tmp/openclaw-session-store.json", sessionKey: "Agent:Main:Telegram:1234:Thread:42", @@ -105,7 +105,6 @@ describe("recordInboundSession", () => { }); it("skips last-route updates when main DM owner pin mismatches sender", async () => { - const { recordInboundSession } = await import("./session.js"); const onSkip = vi.fn(); await recordInboundSession({ diff --git a/src/cli/command-secret-gateway.test.ts b/src/cli/command-secret-gateway.test.ts index 6a2dff29582..87e171d7ce4 100644 --- a/src/cli/command-secret-gateway.test.ts +++ b/src/cli/command-secret-gateway.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 "../config/config.js"; const callGateway = vi.fn(); @@ -7,7 +7,13 @@ vi.mock("../gateway/call.js", () => ({ callGateway, })); -const { resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js"); +let resolveCommandSecretRefsViaGateway: typeof import("./command-secret-gateway.js").resolveCommandSecretRefsViaGateway; + +beforeEach(async () => { + vi.resetModules(); + callGateway.mockReset(); + ({ resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js")); +}); describe("resolveCommandSecretRefsViaGateway", () => { function makeTalkApiKeySecretRefConfig(envKey: string): OpenClawConfig { diff --git a/src/cli/memory-cli.test.ts b/src/cli/memory-cli.test.ts index 2405055adc6..3738616cb2c 100644 --- a/src/cli/memory-cli.test.ts +++ b/src/cli/memory-cli.test.ts @@ -2,15 +2,17 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Command } from "commander"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const getMemorySearchManager = vi.fn(); -const loadConfig = vi.fn(() => ({})); -const resolveDefaultAgentId = vi.fn(() => "main"); -const resolveCommandSecretRefsViaGateway = vi.fn(async ({ config }: { config: unknown }) => ({ - resolvedConfig: config, - diagnostics: [] as string[], -})); +const getMemorySearchManager = vi.hoisted(() => vi.fn()); +const loadConfig = vi.hoisted(() => vi.fn(() => ({}))); +const resolveDefaultAgentId = vi.hoisted(() => vi.fn(() => "main")); +const resolveCommandSecretRefsViaGateway = vi.hoisted(() => + vi.fn(async ({ config }: { config: unknown }) => ({ + resolvedConfig: config, + diagnostics: [] as string[], + })), +); vi.mock("../memory/index.js", () => ({ getMemorySearchManager, @@ -33,7 +35,8 @@ let defaultRuntime: typeof import("../runtime.js").defaultRuntime; let isVerbose: typeof import("../globals.js").isVerbose; let setVerbose: typeof import("../globals.js").setVerbose; -beforeAll(async () => { +beforeEach(async () => { + vi.resetModules(); ({ registerMemoryCli } = await import("./memory-cli.js")); ({ defaultRuntime } = await import("../runtime.js")); ({ isVerbose, setVerbose } = await import("../globals.js")); diff --git a/src/cli/pairing-cli.test.ts b/src/cli/pairing-cli.test.ts index 97d9c9c7751..c05cdb61050 100644 --- a/src/cli/pairing-cli.test.ts +++ b/src/cli/pairing-cli.test.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const listChannelPairingRequests = vi.fn(); const approveChannelPairingCode = vi.fn(); @@ -47,11 +47,9 @@ vi.mock("../config/config.js", () => ({ describe("pairing cli", () => { let registerPairingCli: typeof import("./pairing-cli.js").registerPairingCli; - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ registerPairingCli } = await import("./pairing-cli.js")); - }); - - beforeEach(() => { listChannelPairingRequests.mockClear(); listChannelPairingRequests.mockResolvedValue([]); approveChannelPairingCode.mockClear(); diff --git a/src/cli/prompt.test.ts b/src/cli/prompt.test.ts index da5843dcbda..ee68e646700 100644 --- a/src/cli/prompt.test.ts +++ b/src/cli/prompt.test.ts @@ -1,12 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; -import { isYes, setVerbose, setYes } from "../globals.js"; - -vi.mock("node:readline/promises", () => { - const question = vi.fn(async () => ""); - const close = vi.fn(); - const createInterface = vi.fn(() => ({ question, close })); - return { default: { createInterface } }; -}); +import { beforeEach, describe, expect, it, vi } from "vitest"; type ReadlineMock = { default: { @@ -17,8 +9,27 @@ type ReadlineMock = { }; }; -const { promptYesNo } = await import("./prompt.js"); -const readline = (await import("node:readline/promises")) as unknown as ReadlineMock; +type PromptModule = typeof import("./prompt.js"); +type GlobalsModule = typeof import("../globals.js"); + +let promptYesNo: PromptModule["promptYesNo"]; +let readline: ReadlineMock; +let isYes: GlobalsModule["isYes"]; +let setVerbose: GlobalsModule["setVerbose"]; +let setYes: GlobalsModule["setYes"]; + +beforeEach(async () => { + vi.resetModules(); + vi.doMock("node:readline/promises", () => { + const question = vi.fn(async () => ""); + const close = vi.fn(); + const createInterface = vi.fn(() => ({ question, close })); + return { default: { createInterface } }; + }); + ({ promptYesNo } = await import("./prompt.js")); + ({ isYes, setVerbose, setYes } = await import("../globals.js")); + readline = (await import("node:readline/promises")) as unknown as ReadlineMock; +}); describe("promptYesNo", () => { it("returns true when global --yes is set", async () => { diff --git a/src/config/logging.test.ts b/src/config/logging.test.ts index 6c55961d80d..e410c3f81ba 100644 --- a/src/config/logging.test.ts +++ b/src/config/logging.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ createConfigIO: vi.fn().mockReturnValue({ @@ -10,7 +10,13 @@ vi.mock("./io.js", () => ({ createConfigIO: mocks.createConfigIO, })); -import { formatConfigPath, logConfigUpdated } from "./logging.js"; +let formatConfigPath: typeof import("./logging.js").formatConfigPath; +let logConfigUpdated: typeof import("./logging.js").logConfigUpdated; + +beforeEach(async () => { + vi.resetModules(); + ({ formatConfigPath, logConfigUpdated } = await import("./logging.js")); +}); describe("config logging", () => { it("formats the live config path when no explicit path is provided", () => { diff --git a/src/config/sessions/delivery-info.test.ts b/src/config/sessions/delivery-info.test.ts index 23717338ea3..2f315fd807e 100644 --- a/src/config/sessions/delivery-info.test.ts +++ b/src/config/sessions/delivery-info.test.ts @@ -17,7 +17,8 @@ vi.mock("./store.js", () => ({ loadSessionStore: () => storeState.store, })); -import { extractDeliveryInfo, parseSessionThreadInfo } from "./delivery-info.js"; +let extractDeliveryInfo: typeof import("./delivery-info.js").extractDeliveryInfo; +let parseSessionThreadInfo: typeof import("./delivery-info.js").parseSessionThreadInfo; const buildEntry = (deliveryContext: SessionEntry["deliveryContext"]): SessionEntry => ({ sessionId: "session-1", @@ -25,8 +26,10 @@ const buildEntry = (deliveryContext: SessionEntry["deliveryContext"]): SessionEn deliveryContext, }); -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); storeState.store = {}; + ({ extractDeliveryInfo, parseSessionThreadInfo } = await import("./delivery-info.js")); }); describe("extractDeliveryInfo", () => { diff --git a/src/config/sessions/store.pruning.integration.test.ts b/src/config/sessions/store.pruning.integration.test.ts index d5cf106c520..3fde5236294 100644 --- a/src/config/sessions/store.pruning.integration.test.ts +++ b/src/config/sessions/store.pruning.integration.test.ts @@ -3,15 +3,19 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore } from "./store.js"; import type { SessionEntry } from "./types.js"; // Keep integration tests deterministic: never read a real openclaw.json. vi.mock("../config.js", () => ({ loadConfig: vi.fn().mockReturnValue({}), })); -const { loadConfig } = await import("../config.js"); -const mockLoadConfig = vi.mocked(loadConfig) as ReturnType; + +type StoreModule = typeof import("./store.js"); + +let clearSessionStoreCacheForTest: StoreModule["clearSessionStoreCacheForTest"]; +let loadSessionStore: StoreModule["loadSessionStore"]; +let saveSessionStore: StoreModule["saveSessionStore"]; +let mockLoadConfig: ReturnType; const DAY_MS = 24 * 60 * 60 * 1000; @@ -77,6 +81,11 @@ describe("Integration: saveSessionStore with pruning", () => { }); beforeEach(async () => { + vi.resetModules(); + ({ clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore } = + await import("./store.js")); + const { loadConfig } = await import("../config.js"); + mockLoadConfig = vi.mocked(loadConfig) as ReturnType; testDir = await createCaseDir("pruning-integ"); storePath = path.join(testDir, "sessions.json"); savedCacheTtl = process.env.OPENCLAW_SESSION_CACHE_TTL_MS; diff --git a/src/cron/isolated-agent.delivery-target-thread-session.test.ts b/src/cron/isolated-agent.delivery-target-thread-session.test.ts index 51f9c645a03..3a4537b4929 100644 --- a/src/cron/isolated-agent.delivery-target-thread-session.test.ts +++ b/src/cron/isolated-agent.delivery-target-thread-session.test.ts @@ -1,44 +1,51 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { parseTelegramTarget } from "../../extensions/telegram/src/targets.js"; import type { OpenClawConfig } from "../config/config.js"; // Mock session store so we can control what entries exist. const mockStore: Record> = {}; -vi.mock("../config/sessions.js", () => ({ - loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}), - resolveAgentMainSessionKey: vi.fn(({ agentId }: { agentId: string }) => `agent:${agentId}:main`), - resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"), -})); +type DeliveryTargetModule = typeof import("./isolated-agent/delivery-target.js"); -// Mock channel-selection to avoid real config resolution. -vi.mock("../infra/outbound/channel-selection.js", () => ({ - resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })), -})); +let resolveDeliveryTarget: DeliveryTargetModule["resolveDeliveryTarget"]; -// Minimal mock for channel plugins (Telegram resolveTarget is an identity). -vi.mock("../channels/plugins/index.js", () => ({ - getChannelPlugin: vi.fn(() => ({ - meta: { label: "Telegram" }, - config: {}, - messaging: { - parseExplicitTarget: ({ raw }: { raw: string }) => { - const target = parseTelegramTarget(raw); - return { - to: target.chatId, - threadId: target.messageThreadId, - chatType: target.chatType === "unknown" ? undefined : target.chatType, - }; +beforeEach(async () => { + vi.resetModules(); + for (const key of Object.keys(mockStore)) { + delete mockStore[key]; + } + vi.doMock("../config/sessions.js", () => ({ + loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}), + resolveAgentMainSessionKey: vi.fn( + ({ agentId }: { agentId: string }) => `agent:${agentId}:main`, + ), + resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"), + })); + vi.doMock("../infra/outbound/channel-selection.js", () => ({ + resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })), + })); + vi.doMock("../channels/plugins/index.js", () => ({ + getChannelPlugin: vi.fn(() => ({ + meta: { label: "Telegram" }, + config: {}, + messaging: { + parseExplicitTarget: ({ raw }: { raw: string }) => { + const target = parseTelegramTarget(raw); + return { + to: target.chatId, + threadId: target.messageThreadId, + chatType: target.chatType === "unknown" ? undefined : target.chatType, + }; + }, }, - }, - outbound: { - resolveTarget: ({ to }: { to?: string }) => - to ? { ok: true, to } : { ok: false, error: new Error("missing") }, - }, - })), - normalizeChannelId: vi.fn((id: string) => id), -})); - -const { resolveDeliveryTarget } = await import("./isolated-agent/delivery-target.js"); + outbound: { + resolveTarget: ({ to }: { to?: string }) => + to ? { ok: true, to } : { ok: false, error: new Error("missing") }, + }, + })), + normalizeChannelId: vi.fn((id: string) => id), + })); + ({ resolveDeliveryTarget } = await import("./isolated-agent/delivery-target.js")); +}); describe("resolveDeliveryTarget thread session lookup", () => { const cfg: OpenClawConfig = {}; diff --git a/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts b/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts index edaee62daa6..d953185c369 100644 --- a/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts +++ b/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearFastTestEnv, loadRunCronIsolatedAgentTurn, @@ -8,8 +8,11 @@ import { runWithModelFallbackMock, } from "./run.test-harness.js"; -const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); -const { resolveSandboxConfigForAgent } = await import("../../agents/sandbox/config.js"); +type RunModule = typeof import("./run.js"); +type SandboxConfigModule = typeof import("../../agents/sandbox/config.js"); + +let runCronIsolatedAgentTurn: RunModule["runCronIsolatedAgentTurn"]; +let resolveSandboxConfigForAgent: SandboxConfigModule["resolveSandboxConfigForAgent"]; function makeJob(overrides?: Record) { return { @@ -82,7 +85,10 @@ function expectDefaultSandboxPreserved( describe("runCronIsolatedAgentTurn sandbox config preserved", () => { let previousFastTestEnv: string | undefined; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); + ({ resolveSandboxConfigForAgent } = await import("../../agents/sandbox/config.js")); previousFastTestEnv = clearFastTestEnv(); resetRunCronIsolatedAgentTurnHarness(); }); diff --git a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts index 75ffb262d4d..7b0e13e8cde 100644 --- a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts +++ b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts @@ -4,7 +4,6 @@ import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js"; import type { CronEvent, CronServiceDeps } from "./service.js"; import { CronService } from "./service.js"; import { createDeferred, createNoopLogger, installCronTestHooks } from "./service.test-harness.js"; -import { loadCronStore } from "./store.js"; const noopLogger = createNoopLogger(); installCronTestHooks({ logger: noopLogger }); @@ -60,10 +59,6 @@ async function makeStorePath() { return { storePath, cleanup: async () => {} }; } -function writeStoreFile(storePath: string, payload: unknown) { - setFile(storePath, JSON.stringify(payload, null, 2)); -} - vi.mock("node:fs", async (importOriginal) => { const actual = await importOriginal(); const pathMod = await import("node:path"); @@ -415,14 +410,6 @@ async function createMainOneShotJobHarness(params: { name: string; deleteAfterRu return { ...harness, atMs, job }; } -async function loadLegacyDeliveryMigrationByPayload(params: { - id: string; - payload: { provider?: string; channel?: string }; -}) { - const rawJob = createLegacyDeliveryMigrationJob(params); - return loadLegacyDeliveryMigration(rawJob); -} - async function expectNoMainSummaryForIsolatedRun(params: { runIsolatedAgentJob: CronServiceDeps["runIsolatedAgentJob"]; name: string; @@ -439,43 +426,6 @@ async function expectNoMainSummaryForIsolatedRun(params: { await stopCronAndCleanup(cron, store); } -function createLegacyDeliveryMigrationJob(options: { - id: string; - payload: { provider?: string; channel?: string }; -}) { - return { - id: options.id, - name: "legacy", - enabled: true, - createdAtMs: Date.now(), - updatedAtMs: Date.now(), - schedule: { kind: "cron", expr: "* * * * *" }, - sessionTarget: "isolated", - wakeMode: "now", - payload: { - kind: "agentTurn", - message: "hi", - deliver: true, - ...options.payload, - to: "7200373102", - }, - state: {}, - }; -} - -async function loadLegacyDeliveryMigration(rawJob: Record) { - ensureDir(fixturesRoot); - const store = await makeStorePath(); - writeStoreFile(store.storePath, { version: 1, jobs: [rawJob] }); - - const cron = createStartedCronService(store.storePath); - await cron.start(); - cron.stop(); - const loaded = await loadCronStore(store.storePath); - const job = loaded.jobs.find((j) => j.id === rawJob.id); - return { store, cron, job }; -} - describe("CronService", () => { it("runs a one-shot main job and disables it after success when requested", async () => { const { store, cron, enqueueSystemEvent, requestHeartbeatNow, events, atMs, job } = @@ -658,33 +608,6 @@ describe("CronService", () => { expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1); }); - it("migrates legacy payload.provider to payload.channel on load", async () => { - const { store, cron, job } = await loadLegacyDeliveryMigrationByPayload({ - id: "legacy-1", - payload: { provider: " TeLeGrAm " }, - }); - // Legacy delivery fields are migrated to the top-level delivery object - const delivery = job?.delivery as unknown as Record; - expect(delivery?.channel).toBe("telegram"); - const payload = job?.payload as unknown as Record; - expect("provider" in payload).toBe(false); - expect("channel" in payload).toBe(false); - - await stopCronAndCleanup(cron, store); - }); - - it("canonicalizes payload.channel casing on load", async () => { - const { store, cron, job } = await loadLegacyDeliveryMigrationByPayload({ - id: "legacy-2", - payload: { channel: "Telegram" }, - }); - // Legacy delivery fields are migrated to the top-level delivery object - const delivery = job?.delivery as unknown as Record; - expect(delivery?.channel).toBe("telegram"); - - await stopCronAndCleanup(cron, store); - }); - it("does not post a fallback main summary when an isolated job errors", async () => { const runIsolatedAgentJob = vi.fn(async () => ({ status: "error" as const, @@ -764,60 +687,4 @@ describe("CronService", () => { cron.stop(); await store.cleanup(); }); - - it("skips invalid main jobs with agentTurn payloads from disk", async () => { - ensureDir(fixturesRoot); - const store = await makeStorePath(); - const enqueueSystemEvent = vi.fn(); - const requestHeartbeatNow = vi.fn(); - const events = createCronEventHarness(); - - const atMs = Date.parse("2025-12-13T00:00:01.000Z"); - writeStoreFile(store.storePath, { - version: 1, - jobs: [ - { - id: "job-1", - enabled: true, - createdAtMs: Date.parse("2025-12-13T00:00:00.000Z"), - updatedAtMs: Date.parse("2025-12-13T00:00:00.000Z"), - schedule: { kind: "at", at: new Date(atMs).toISOString() }, - sessionTarget: "main", - wakeMode: "now", - payload: { kind: "agentTurn", message: "bad" }, - state: {}, - }, - ], - }); - - const cron = new CronService({ - storePath: store.storePath, - cronEnabled: true, - log: noopLogger, - enqueueSystemEvent, - requestHeartbeatNow, - runIsolatedAgentJob: vi.fn(async (_params: { job: unknown; message: string }) => ({ - status: "ok", - })) as unknown as CronServiceDeps["runIsolatedAgentJob"], - onEvent: events.onEvent, - }); - - await cron.start(); - - vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z")); - await vi.runOnlyPendingTimersAsync(); - await events.waitFor( - (evt) => evt.jobId === "job-1" && evt.action === "finished" && evt.status === "skipped", - ); - - expect(enqueueSystemEvent).not.toHaveBeenCalled(); - expect(requestHeartbeatNow).not.toHaveBeenCalled(); - - const jobs = await cron.list({ includeDisabled: true }); - expect(jobs[0]?.state.lastStatus).toBe("skipped"); - expect(jobs[0]?.state.lastError).toMatch(/main job requires/i); - - cron.stop(); - await store.cleanup(); - }); }); diff --git a/src/infra/boundary-file-read.test.ts b/src/infra/boundary-file-read.test.ts index 2dceb0cb06a..8829fec80b8 100644 --- a/src/infra/boundary-file-read.test.ts +++ b/src/infra/boundary-file-read.test.ts @@ -14,11 +14,15 @@ vi.mock("./safe-open-sync.js", () => ({ openVerifiedFileSync: (...args: unknown[]) => openVerifiedFileSyncMock(...args), })); -const { canUseBoundaryFileOpen, openBoundaryFile, openBoundaryFileSync } = - await import("./boundary-file-read.js"); +let canUseBoundaryFileOpen: typeof import("./boundary-file-read.js").canUseBoundaryFileOpen; +let openBoundaryFile: typeof import("./boundary-file-read.js").openBoundaryFile; +let openBoundaryFileSync: typeof import("./boundary-file-read.js").openBoundaryFileSync; describe("boundary-file-read", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ canUseBoundaryFileOpen, openBoundaryFile, openBoundaryFileSync } = + await import("./boundary-file-read.js")); resolveBoundaryPathSyncMock.mockReset(); resolveBoundaryPathMock.mockReset(); openVerifiedFileSyncMock.mockReset(); diff --git a/src/infra/channel-summary.test.ts b/src/infra/channel-summary.test.ts index c0fc17ba255..12cfa8bbbae 100644 --- a/src/infra/channel-summary.test.ts +++ b/src/infra/channel-summary.test.ts @@ -1,12 +1,18 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPlugin } from "../channels/plugins/types.js"; vi.mock("../channels/plugins/index.js", () => ({ listChannelPlugins: vi.fn(), })); -const { buildChannelSummary } = await import("./channel-summary.js"); -const { listChannelPlugins } = await import("../channels/plugins/index.js"); +let buildChannelSummary: typeof import("./channel-summary.js").buildChannelSummary; +let listChannelPlugins: typeof import("../channels/plugins/index.js").listChannelPlugins; + +beforeEach(async () => { + vi.resetModules(); + ({ buildChannelSummary } = await import("./channel-summary.js")); + ({ listChannelPlugins } = await import("../channels/plugins/index.js")); +}); function makeSlackHttpSummaryPlugin(): ChannelPlugin { return { diff --git a/src/infra/env.test.ts b/src/infra/env.test.ts index 5ee0af072fb..7cfac44bb86 100644 --- a/src/infra/env.test.ts +++ b/src/infra/env.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { withEnv } from "../test-utils/env.js"; const loggerMocks = vi.hoisted(() => ({ @@ -11,7 +11,18 @@ vi.mock("../logging/subsystem.js", () => ({ }), })); -import { isTruthyEnvValue, logAcceptedEnvOption, normalizeEnv, normalizeZaiEnv } from "./env.js"; +type EnvModule = typeof import("./env.js"); + +let isTruthyEnvValue: EnvModule["isTruthyEnvValue"]; +let logAcceptedEnvOption: EnvModule["logAcceptedEnvOption"]; +let normalizeEnv: EnvModule["normalizeEnv"]; +let normalizeZaiEnv: EnvModule["normalizeZaiEnv"]; + +beforeEach(async () => { + vi.resetModules(); + ({ isTruthyEnvValue, logAcceptedEnvOption, normalizeEnv, normalizeZaiEnv } = + await import("./env.js")); +}); describe("normalizeZaiEnv", () => { it("copies Z_AI_API_KEY to ZAI_API_KEY when missing", () => { diff --git a/src/infra/exec-approval-surface.test.ts b/src/infra/exec-approval-surface.test.ts index c4b959c5042..3e59d968670 100644 --- a/src/infra/exec-approval-surface.test.ts +++ b/src/infra/exec-approval-surface.test.ts @@ -5,51 +5,14 @@ const getChannelPluginMock = vi.hoisted(() => vi.fn()); const listChannelPluginsMock = vi.hoisted(() => vi.fn()); const normalizeMessageChannelMock = vi.hoisted(() => vi.fn()); -vi.mock("../config/config.js", () => ({ - loadConfig: (...args: unknown[]) => loadConfigMock(...args), -})); +type ExecApprovalSurfaceModule = typeof import("./exec-approval-surface.js"); -vi.mock("../channels/plugins/index.js", () => ({ - getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), - listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args), -})); - -vi.mock("../../extensions/discord/src/channel.js", () => ({ - discordPlugin: {}, -})); - -vi.mock("../../extensions/telegram/src/channel.js", () => ({ - telegramPlugin: {}, -})); - -vi.mock("../../extensions/slack/src/channel.js", () => ({ - slackPlugin: {}, -})); - -vi.mock("../../extensions/whatsapp/src/channel.js", () => ({ - whatsappPlugin: {}, -})); - -vi.mock("../../extensions/signal/src/channel.js", () => ({ - signalPlugin: {}, -})); - -vi.mock("../../extensions/imessage/src/channel.js", () => ({ - imessagePlugin: {}, -})); - -vi.mock("../utils/message-channel.js", () => ({ - INTERNAL_MESSAGE_CHANNEL: "web", - normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), -})); - -import { - hasConfiguredExecApprovalDmRoute, - resolveExecApprovalInitiatingSurfaceState, -} from "./exec-approval-surface.js"; +let hasConfiguredExecApprovalDmRoute: ExecApprovalSurfaceModule["hasConfiguredExecApprovalDmRoute"]; +let resolveExecApprovalInitiatingSurfaceState: ExecApprovalSurfaceModule["resolveExecApprovalInitiatingSurfaceState"]; describe("resolveExecApprovalInitiatingSurfaceState", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); loadConfigMock.mockReset(); getChannelPluginMock.mockReset(); listChannelPluginsMock.mockReset(); @@ -57,6 +20,37 @@ describe("resolveExecApprovalInitiatingSurfaceState", () => { normalizeMessageChannelMock.mockImplementation((value?: string | null) => typeof value === "string" ? value.trim().toLowerCase() : undefined, ); + vi.doMock("../config/config.js", () => ({ + loadConfig: (...args: unknown[]) => loadConfigMock(...args), + })); + vi.doMock("../channels/plugins/index.js", () => ({ + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args), + })); + vi.doMock("../../extensions/discord/src/channel.js", () => ({ + discordPlugin: {}, + })); + vi.doMock("../../extensions/telegram/src/channel.js", () => ({ + telegramPlugin: {}, + })); + vi.doMock("../../extensions/slack/src/channel.js", () => ({ + slackPlugin: {}, + })); + vi.doMock("../../extensions/whatsapp/src/channel.js", () => ({ + whatsappPlugin: {}, + })); + vi.doMock("../../extensions/signal/src/channel.js", () => ({ + signalPlugin: {}, + })); + vi.doMock("../../extensions/imessage/src/channel.js", () => ({ + imessagePlugin: {}, + })); + vi.doMock("../utils/message-channel.js", () => ({ + INTERNAL_MESSAGE_CHANNEL: "web", + normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), + })); + ({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } = + await import("./exec-approval-surface.js")); }); it("treats web UI, terminal UI, and missing channels as enabled", () => { @@ -154,8 +148,46 @@ describe("resolveExecApprovalInitiatingSurfaceState", () => { }); describe("hasConfiguredExecApprovalDmRoute", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + loadConfigMock.mockReset(); + getChannelPluginMock.mockReset(); listChannelPluginsMock.mockReset(); + normalizeMessageChannelMock.mockReset(); + normalizeMessageChannelMock.mockImplementation((value?: string | null) => + typeof value === "string" ? value.trim().toLowerCase() : undefined, + ); + vi.doMock("../config/config.js", () => ({ + loadConfig: (...args: unknown[]) => loadConfigMock(...args), + })); + vi.doMock("../channels/plugins/index.js", () => ({ + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args), + })); + vi.doMock("../../extensions/discord/src/channel.js", () => ({ + discordPlugin: {}, + })); + vi.doMock("../../extensions/telegram/src/channel.js", () => ({ + telegramPlugin: {}, + })); + vi.doMock("../../extensions/slack/src/channel.js", () => ({ + slackPlugin: {}, + })); + vi.doMock("../../extensions/whatsapp/src/channel.js", () => ({ + whatsappPlugin: {}, + })); + vi.doMock("../../extensions/signal/src/channel.js", () => ({ + signalPlugin: {}, + })); + vi.doMock("../../extensions/imessage/src/channel.js", () => ({ + imessagePlugin: {}, + })); + vi.doMock("../utils/message-channel.js", () => ({ + INTERNAL_MESSAGE_CHANNEL: "web", + normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), + })); + ({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } = + await import("./exec-approval-surface.js")); }); it("returns true when any enabled account routes approvals to DM or both", () => { diff --git a/src/infra/exec-approvals-store.test.ts b/src/infra/exec-approvals-store.test.ts index 4dc6ab71c7e..365e40b1f1d 100644 --- a/src/infra/exec-approvals-store.test.ts +++ b/src/infra/exec-approvals-store.test.ts @@ -9,23 +9,36 @@ vi.mock("./jsonl-socket.js", () => ({ requestJsonlSocket: (...args: unknown[]) => requestJsonlSocketMock(...args), })); -import { - addAllowlistEntry, - ensureExecApprovals, - mergeExecApprovalsSocketDefaults, - normalizeExecApprovals, - readExecApprovalsSnapshot, - recordAllowlistUse, - requestExecApprovalViaSocket, - resolveExecApprovalsPath, - resolveExecApprovalsSocketPath, - type ExecApprovalsFile, -} from "./exec-approvals.js"; +import type { ExecApprovalsFile } from "./exec-approvals.js"; + +type ExecApprovalsModule = typeof import("./exec-approvals.js"); + +let addAllowlistEntry: ExecApprovalsModule["addAllowlistEntry"]; +let ensureExecApprovals: ExecApprovalsModule["ensureExecApprovals"]; +let mergeExecApprovalsSocketDefaults: ExecApprovalsModule["mergeExecApprovalsSocketDefaults"]; +let normalizeExecApprovals: ExecApprovalsModule["normalizeExecApprovals"]; +let readExecApprovalsSnapshot: ExecApprovalsModule["readExecApprovalsSnapshot"]; +let recordAllowlistUse: ExecApprovalsModule["recordAllowlistUse"]; +let requestExecApprovalViaSocket: ExecApprovalsModule["requestExecApprovalViaSocket"]; +let resolveExecApprovalsPath: ExecApprovalsModule["resolveExecApprovalsPath"]; +let resolveExecApprovalsSocketPath: ExecApprovalsModule["resolveExecApprovalsSocketPath"]; const tempDirs: string[] = []; const originalOpenClawHome = process.env.OPENCLAW_HOME; -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + ({ + addAllowlistEntry, + ensureExecApprovals, + mergeExecApprovalsSocketDefaults, + normalizeExecApprovals, + readExecApprovalsSnapshot, + recordAllowlistUse, + requestExecApprovalViaSocket, + resolveExecApprovalsPath, + resolveExecApprovalsSocketPath, + } = await import("./exec-approvals.js")); requestJsonlSocketMock.mockReset(); }); diff --git a/src/infra/net/proxy-fetch.test.ts b/src/infra/net/proxy-fetch.test.ts index 8f9c17fa499..fb8dbf7f8d3 100644 --- a/src/infra/net/proxy-fetch.test.ts +++ b/src/infra/net/proxy-fetch.test.ts @@ -51,12 +51,10 @@ vi.mock("undici", () => ({ fetch: undiciFetch, })); -import { - getProxyUrlFromFetch, - makeProxyFetch, - PROXY_FETCH_PROXY_URL, - resolveProxyFetchFromEnv, -} from "./proxy-fetch.js"; +let getProxyUrlFromFetch: typeof import("./proxy-fetch.js").getProxyUrlFromFetch; +let makeProxyFetch: typeof import("./proxy-fetch.js").makeProxyFetch; +let PROXY_FETCH_PROXY_URL: typeof import("./proxy-fetch.js").PROXY_FETCH_PROXY_URL; +let resolveProxyFetchFromEnv: typeof import("./proxy-fetch.js").resolveProxyFetchFromEnv; function clearProxyEnv(): void { for (const key of PROXY_ENV_KEYS) { @@ -75,7 +73,12 @@ function restoreProxyEnv(): void { } describe("makeProxyFetch", () => { - beforeEach(() => vi.clearAllMocks()); + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + ({ getProxyUrlFromFetch, makeProxyFetch, PROXY_FETCH_PROXY_URL, resolveProxyFetchFromEnv } = + await import("./proxy-fetch.js")); + }); it("uses undici fetch with ProxyAgent dispatcher", async () => { const proxyUrl = "http://proxy.test:8080"; diff --git a/src/infra/net/ssrf.dispatcher.test.ts b/src/infra/net/ssrf.dispatcher.test.ts index 07b80b40465..838feb291ac 100644 --- a/src/infra/net/ssrf.dispatcher.test.ts +++ b/src/infra/net/ssrf.dispatcher.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const { agentCtor, envHttpProxyAgentCtor, proxyAgentCtor } = vi.hoisted(() => ({ agentCtor: vi.fn(function MockAgent(this: { options: unknown }, options: unknown) { @@ -21,7 +21,14 @@ vi.mock("undici", () => ({ ProxyAgent: proxyAgentCtor, })); -import { createPinnedDispatcher, type PinnedHostname } from "./ssrf.js"; +import type { PinnedHostname } from "./ssrf.js"; + +let createPinnedDispatcher: typeof import("./ssrf.js").createPinnedDispatcher; + +beforeEach(async () => { + vi.resetModules(); + ({ createPinnedDispatcher } = await import("./ssrf.js")); +}); describe("createPinnedDispatcher", () => { it("uses pinned lookup without overriding global family policy", () => { diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index 8b14c4084fc..47a97dd6fb6 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -62,15 +62,20 @@ vi.mock("./proxy-env.js", () => ({ })); import { hasEnvHttpProxyConfigured } from "./proxy-env.js"; -import { - DEFAULT_UNDICI_STREAM_TIMEOUT_MS, - ensureGlobalUndiciEnvProxyDispatcher, - ensureGlobalUndiciStreamTimeouts, - resetGlobalUndiciStreamTimeoutsForTests, -} from "./undici-global-dispatcher.js"; +let DEFAULT_UNDICI_STREAM_TIMEOUT_MS: typeof import("./undici-global-dispatcher.js").DEFAULT_UNDICI_STREAM_TIMEOUT_MS; +let ensureGlobalUndiciEnvProxyDispatcher: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciEnvProxyDispatcher; +let ensureGlobalUndiciStreamTimeouts: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciStreamTimeouts; +let resetGlobalUndiciStreamTimeoutsForTests: typeof import("./undici-global-dispatcher.js").resetGlobalUndiciStreamTimeoutsForTests; describe("ensureGlobalUndiciStreamTimeouts", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ + DEFAULT_UNDICI_STREAM_TIMEOUT_MS, + ensureGlobalUndiciEnvProxyDispatcher, + ensureGlobalUndiciStreamTimeouts, + resetGlobalUndiciStreamTimeoutsForTests, + } = await import("./undici-global-dispatcher.js")); vi.clearAllMocks(); resetGlobalUndiciStreamTimeoutsForTests(); setCurrentDispatcher(new Agent()); diff --git a/src/infra/openclaw-root.test.ts b/src/infra/openclaw-root.test.ts index e12b2d77f64..291280318bb 100644 --- a/src/infra/openclaw-root.test.ts +++ b/src/infra/openclaw-root.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" }; @@ -93,12 +93,10 @@ describe("resolveOpenClawPackageRoot", () => { let resolveOpenClawPackageRoot: typeof import("./openclaw-root.js").resolveOpenClawPackageRoot; let resolveOpenClawPackageRootSync: typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync; - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } = await import("./openclaw-root.js")); - }); - - beforeEach(() => { state.entries.clear(); state.realpaths.clear(); state.realpathErrors.clear(); diff --git a/src/infra/outbound/agent-delivery.test.ts b/src/infra/outbound/agent-delivery.test.ts index b137ce2a73f..88b6776105e 100644 --- a/src/infra/outbound/agent-delivery.test.ts +++ b/src/infra/outbound/agent-delivery.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ resolveOutboundTarget: vi.fn(() => ({ ok: true as const, to: "+1999" })), @@ -13,7 +13,15 @@ vi.mock("./targets.js", async () => { }); import type { OpenClawConfig } from "../../config/config.js"; -import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget } from "./agent-delivery.js"; +type AgentDeliveryModule = typeof import("./agent-delivery.js"); + +let resolveAgentDeliveryPlan: AgentDeliveryModule["resolveAgentDeliveryPlan"]; +let resolveAgentOutboundTarget: AgentDeliveryModule["resolveAgentOutboundTarget"]; + +beforeEach(async () => { + vi.resetModules(); + ({ resolveAgentDeliveryPlan, resolveAgentOutboundTarget } = await import("./agent-delivery.js")); +}); describe("agent delivery helpers", () => { it("builds a delivery plan from session delivery context", () => { diff --git a/src/infra/outbound/channel-selection.test.ts b/src/infra/outbound/channel-selection.test.ts index 5f3ac319628..fdb4ecd4b6f 100644 --- a/src/infra/outbound/channel-selection.test.ts +++ b/src/infra/outbound/channel-selection.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { defaultRuntime } from "../../runtime.js"; const mocks = vi.hoisted(() => ({ listChannelPlugins: vi.fn(), @@ -14,11 +13,20 @@ vi.mock("./channel-resolution.js", () => ({ resolveOutboundChannelPlugin: mocks.resolveOutboundChannelPlugin, })); -import { - __testing, - listConfiguredMessageChannels, - resolveMessageChannelSelection, -} from "./channel-selection.js"; +type ChannelSelectionModule = typeof import("./channel-selection.js"); +type RuntimeModule = typeof import("../../runtime.js"); + +let __testing: ChannelSelectionModule["__testing"]; +let listConfiguredMessageChannels: ChannelSelectionModule["listConfiguredMessageChannels"]; +let resolveMessageChannelSelection: ChannelSelectionModule["resolveMessageChannelSelection"]; +let runtimeModule: RuntimeModule; + +beforeEach(async () => { + vi.resetModules(); + runtimeModule = await import("../../runtime.js"); + ({ __testing, listConfiguredMessageChannels, resolveMessageChannelSelection } = + await import("./channel-selection.js")); +}); function makePlugin(params: { id: string; @@ -40,9 +48,10 @@ function makePlugin(params: { } describe("listConfiguredMessageChannels", () => { - const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => undefined); + let errorSpy: ReturnType; beforeEach(() => { + errorSpy = vi.spyOn(runtimeModule.defaultRuntime, "error").mockImplementation(() => undefined); mocks.listChannelPlugins.mockReset(); mocks.listChannelPlugins.mockReturnValue([]); mocks.resolveOutboundChannelPlugin.mockReset(); diff --git a/src/infra/outbound/deliver.lifecycle.test.ts b/src/infra/outbound/deliver.lifecycle.test.ts index 22fa829812e..c8ce22b826b 100644 --- a/src/infra/outbound/deliver.lifecycle.test.ts +++ b/src/infra/outbound/deliver.lifecycle.test.ts @@ -15,10 +15,12 @@ import { whatsappChunkConfig, } from "./deliver.test-helpers.js"; -const { deliverOutboundPayloads } = await import("./deliver.js"); +type DeliverModule = typeof import("./deliver.js"); + +let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"]; async function runChunkedWhatsAppDelivery(params?: { - mirror?: Parameters[0]["mirror"]; + mirror?: Parameters[0]["mirror"]; }) { return await runChunkedWhatsAppDeliveryHelper({ deliverOutboundPayloads, @@ -75,7 +77,9 @@ function expectSuccessfulWhatsAppInternalHookPayload( } describe("deliverOutboundPayloads lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ deliverOutboundPayloads } = await import("./deliver.js")); resetDeliverTestState(); resetDeliverTestMocks({ includeSessionMocks: true }); }); diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 5323dd83e27..e72cbaa0bee 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -80,7 +80,10 @@ vi.mock("../../logging/subsystem.js", () => ({ }, })); -const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js"); +type DeliverModule = typeof import("./deliver.js"); + +let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"]; +let normalizeOutboundPayloads: DeliverModule["normalizeOutboundPayloads"]; const telegramChunkConfig: OpenClawConfig = { channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, @@ -90,13 +93,13 @@ const whatsappChunkConfig: OpenClawConfig = { channels: { whatsapp: { textChunkLimit: 4000 } }, }; -type DeliverOutboundArgs = Parameters[0]; +type DeliverOutboundArgs = Parameters[0]; type DeliverOutboundPayload = DeliverOutboundArgs["payloads"][number]; type DeliverSession = DeliverOutboundArgs["session"]; async function deliverWhatsAppPayload(params: { sendWhatsApp: NonNullable< - NonNullable[0]["deps"]>["sendWhatsApp"] + NonNullable[0]["deps"]>["sendWhatsApp"] >; payload: DeliverOutboundPayload; cfg?: OpenClawConfig; @@ -198,7 +201,9 @@ function expectSuccessfulWhatsAppInternalHookPayload( } describe("deliverOutboundPayloads", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js")); setActivePluginRegistry(defaultRegistry); mocks.appendAssistantMessageToSessionTranscript.mockClear(); hookMocks.runner.hasHooks.mockClear(); diff --git a/src/infra/outbound/identity.test.ts b/src/infra/outbound/identity.test.ts index d31d8a6dd06..e5a8ea6a808 100644 --- a/src/infra/outbound/identity.test.ts +++ b/src/infra/outbound/identity.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const resolveAgentIdentityMock = vi.hoisted(() => vi.fn()); const resolveAgentAvatarMock = vi.hoisted(() => vi.fn()); @@ -11,7 +11,15 @@ vi.mock("../../agents/identity-avatar.js", () => ({ resolveAgentAvatar: (...args: unknown[]) => resolveAgentAvatarMock(...args), })); -import { normalizeOutboundIdentity, resolveAgentOutboundIdentity } from "./identity.js"; +type IdentityModule = typeof import("./identity.js"); + +let normalizeOutboundIdentity: IdentityModule["normalizeOutboundIdentity"]; +let resolveAgentOutboundIdentity: IdentityModule["resolveAgentOutboundIdentity"]; + +beforeEach(async () => { + vi.resetModules(); + ({ normalizeOutboundIdentity, resolveAgentOutboundIdentity } = await import("./identity.js")); +}); describe("normalizeOutboundIdentity", () => { it("trims fields and drops empty identities", () => { diff --git a/src/infra/outbound/message-action-runner.media.test.ts b/src/infra/outbound/message-action-runner.media.test.ts index 1715ea090f2..ba24bdb15df 100644 --- a/src/infra/outbound/message-action-runner.media.test.ts +++ b/src/infra/outbound/message-action-runner.media.test.ts @@ -1,16 +1,13 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { slackPlugin } from "../../../extensions/slack/src/channel.js"; -import { loadWebMedia } from "../../../extensions/whatsapp/src/media.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { jsonResult } from "../../agents/tools/common.js"; import type { ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js"; -import { runMessageAction } from "./message-action-runner.js"; vi.mock("../../../extensions/whatsapp/src/media.js", async () => { const actual = await vi.importActual( @@ -79,8 +76,17 @@ async function expectSandboxMediaRewrite(params: { ); } -let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime; -let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime; +type MessageActionRunnerModule = typeof import("./message-action-runner.js"); +type WhatsAppMediaModule = typeof import("../../../extensions/whatsapp/src/media.js"); +type SlackChannelModule = typeof import("../../../extensions/slack/src/channel.js"); +type RuntimeIndexModule = typeof import("../../plugins/runtime/index.js"); +type SlackRuntimeModule = typeof import("../../../extensions/slack/src/runtime.js"); + +let runMessageAction: MessageActionRunnerModule["runMessageAction"]; +let loadWebMedia: WhatsAppMediaModule["loadWebMedia"]; +let slackPlugin: SlackChannelModule["slackPlugin"]; +let createPluginRuntime: RuntimeIndexModule["createPluginRuntime"]; +let setSlackRuntime: SlackRuntimeModule["setSlackRuntime"]; function installSlackRuntime() { const runtime = createPluginRuntime(); @@ -88,7 +94,11 @@ function installSlackRuntime() { } describe("runMessageAction media behavior", () => { - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); + ({ runMessageAction } = await import("./message-action-runner.js")); + ({ loadWebMedia } = await import("../../../extensions/whatsapp/src/media.js")); + ({ slackPlugin } = await import("../../../extensions/slack/src/channel.js")); ({ createPluginRuntime } = await import("../../plugins/runtime/index.js")); ({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js")); }); diff --git a/src/infra/outbound/message-action-runner.poll.test.ts b/src/infra/outbound/message-action-runner.poll.test.ts index 895e47605ce..ed1beb91f5d 100644 --- a/src/infra/outbound/message-action-runner.poll.test.ts +++ b/src/infra/outbound/message-action-runner.poll.test.ts @@ -1,11 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - installMessageActionRunnerTestRegistry, - resetMessageActionRunnerTestRegistry, - slackConfig, - telegramConfig, -} from "./message-action-runner.test-helpers.js"; - const mocks = vi.hoisted(() => ({ executePollAction: vi.fn(), })); @@ -20,10 +13,18 @@ vi.mock("./outbound-send-service.js", async () => { }; }); -import { runMessageAction } from "./message-action-runner.js"; +type MessageActionRunnerModule = typeof import("./message-action-runner.js"); +type MessageActionRunnerTestHelpersModule = + typeof import("./message-action-runner.test-helpers.js"); + +let runMessageAction: MessageActionRunnerModule["runMessageAction"]; +let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"]; +let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"]; +let slackConfig: MessageActionRunnerTestHelpersModule["slackConfig"]; +let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"]; async function runPollAction(params: { - cfg: typeof slackConfig; + cfg: MessageActionRunnerTestHelpersModule["slackConfig"]; actionParams: Record; toolContext?: Record; }) { @@ -44,7 +45,15 @@ async function runPollAction(params: { | undefined; } describe("runMessageAction poll handling", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ runMessageAction } = await import("./message-action-runner.js")); + ({ + installMessageActionRunnerTestRegistry, + resetMessageActionRunnerTestRegistry, + slackConfig, + telegramConfig, + } = await import("./message-action-runner.test-helpers.js")); installMessageActionRunnerTestRegistry(); mocks.executePollAction.mockResolvedValue({ handledBy: "core", @@ -54,14 +63,14 @@ describe("runMessageAction poll handling", () => { }); afterEach(() => { - resetMessageActionRunnerTestRegistry(); + resetMessageActionRunnerTestRegistry?.(); mocks.executePollAction.mockReset(); }); it.each([ { name: "requires at least two poll options", - cfg: telegramConfig, + getCfg: () => telegramConfig, actionParams: { channel: "telegram", target: "telegram:123", @@ -72,7 +81,7 @@ describe("runMessageAction poll handling", () => { }, { name: "rejects durationSeconds outside telegram", - cfg: slackConfig, + getCfg: () => slackConfig, actionParams: { channel: "slack", target: "#C12345678", @@ -84,7 +93,7 @@ describe("runMessageAction poll handling", () => { }, { name: "rejects poll visibility outside telegram", - cfg: slackConfig, + getCfg: () => slackConfig, actionParams: { channel: "slack", target: "#C12345678", @@ -94,8 +103,8 @@ describe("runMessageAction poll handling", () => { }, message: /pollAnonymous\/pollPublic are only supported for Telegram polls/i, }, - ])("$name", async ({ cfg, actionParams, message }) => { - await expect(runPollAction({ cfg, actionParams })).rejects.toThrow(message); + ])("$name", async ({ getCfg, actionParams, message }) => { + await expect(runPollAction({ cfg: getCfg(), actionParams })).rejects.toThrow(message); expect(mocks.executePollAction).not.toHaveBeenCalled(); }); diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index 42d898b145a..7401127251a 100644 --- a/src/infra/outbound/message-action-runner.threading.test.ts +++ b/src/infra/outbound/message-action-runner.threading.test.ts @@ -1,11 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - installMessageActionRunnerTestRegistry, - resetMessageActionRunnerTestRegistry, - slackConfig, - telegramConfig, -} from "./message-action-runner.test-helpers.js"; - const mocks = vi.hoisted(() => ({ executeSendAction: vi.fn(), recordSessionMetaFromInbound: vi.fn(async () => ({ ok: true })), @@ -31,10 +24,18 @@ vi.mock("../../config/sessions.js", async () => { }; }); -import { runMessageAction } from "./message-action-runner.js"; +type MessageActionRunnerModule = typeof import("./message-action-runner.js"); +type MessageActionRunnerTestHelpersModule = + typeof import("./message-action-runner.test-helpers.js"); + +let runMessageAction: MessageActionRunnerModule["runMessageAction"]; +let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"]; +let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"]; +let slackConfig: MessageActionRunnerTestHelpersModule["slackConfig"]; +let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"]; async function runThreadingAction(params: { - cfg: typeof slackConfig; + cfg: MessageActionRunnerTestHelpersModule["slackConfig"]; actionParams: Record; toolContext?: Record; }) { @@ -65,12 +66,20 @@ const defaultTelegramToolContext = { } as const; describe("runMessageAction threading auto-injection", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ runMessageAction } = await import("./message-action-runner.js")); + ({ + installMessageActionRunnerTestRegistry, + resetMessageActionRunnerTestRegistry, + slackConfig, + telegramConfig, + } = await import("./message-action-runner.test-helpers.js")); installMessageActionRunnerTestRegistry(); }); afterEach(() => { - resetMessageActionRunnerTestRegistry(); + resetMessageActionRunnerTestRegistry?.(); mocks.executeSendAction.mockClear(); mocks.recordSessionMetaFromInbound.mockClear(); }); diff --git a/src/infra/outbound/message.channels.test.ts b/src/infra/outbound/message.channels.test.ts index 6d89ac5ab91..6167c3c250c 100644 --- a/src/infra/outbound/message.channels.test.ts +++ b/src/infra/outbound/message.channels.test.ts @@ -4,7 +4,6 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createMSTeamsTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; -import { sendMessage, sendPoll } from "./message.js"; const setRegistry = (registry: ReturnType) => { setActivePluginRegistry(registry); @@ -17,7 +16,12 @@ vi.mock("../../gateway/call.js", () => ({ randomIdempotencyKey: () => "idem-1", })); -beforeEach(() => { +let sendMessage: typeof import("./message.js").sendMessage; +let sendPoll: typeof import("./message.js").sendPoll; + +beforeEach(async () => { + vi.resetModules(); + ({ sendMessage, sendPoll } = await import("./message.js")); callGatewayMock.mockClear(); setRegistry(emptyRegistry); }); diff --git a/src/infra/outbound/message.test.ts b/src/infra/outbound/message.test.ts index 200d4d587e1..47a43eb8437 100644 --- a/src/infra/outbound/message.test.ts +++ b/src/infra/outbound/message.test.ts @@ -46,10 +46,13 @@ vi.mock("./deliver.js", () => ({ import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; -import { sendMessage } from "./message.js"; + +let sendMessage: typeof import("./message.js").sendMessage; describe("sendMessage", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ sendMessage } = await import("./message.js")); setActivePluginRegistry(createTestRegistry([])); mocks.getChannelPlugin.mockClear(); mocks.resolveOutboundTarget.mockClear(); diff --git a/src/infra/outbound/outbound-send-service.test.ts b/src/infra/outbound/outbound-send-service.test.ts index d4a481a8693..f5d1f2b9b28 100644 --- a/src/infra/outbound/outbound-send-service.test.ts +++ b/src/infra/outbound/outbound-send-service.test.ts @@ -32,7 +32,10 @@ vi.mock("../../config/sessions.js", () => ({ appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript, })); -import { executePollAction, executeSendAction } from "./outbound-send-service.js"; +type OutboundSendServiceModule = typeof import("./outbound-send-service.js"); + +let executePollAction: OutboundSendServiceModule["executePollAction"]; +let executeSendAction: OutboundSendServiceModule["executeSendAction"]; describe("executeSendAction", () => { function pluginActionResult(messageId: string) { @@ -88,7 +91,9 @@ describe("executeSendAction", () => { }); } - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ executePollAction, executeSendAction } = await import("./outbound-send-service.js")); mocks.dispatchChannelMessageAction.mockClear(); mocks.sendMessage.mockClear(); mocks.sendPoll.mockClear(); diff --git a/src/infra/outbound/session-context.test.ts b/src/infra/outbound/session-context.test.ts index a62c47fb998..1446d665f35 100644 --- a/src/infra/outbound/session-context.test.ts +++ b/src/infra/outbound/session-context.test.ts @@ -1,12 +1,19 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn()); -vi.mock("../../agents/agent-scope.js", () => ({ - resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args), -})); +type SessionContextModule = typeof import("./session-context.js"); -import { buildOutboundSessionContext } from "./session-context.js"; +let buildOutboundSessionContext: SessionContextModule["buildOutboundSessionContext"]; + +beforeEach(async () => { + vi.resetModules(); + resolveSessionAgentIdMock.mockReset(); + vi.doMock("../../agents/agent-scope.js", () => ({ + resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args), + })); + ({ buildOutboundSessionContext } = await import("./session-context.js")); +}); describe("buildOutboundSessionContext", () => { it("returns undefined when both session key and agent id are blank", () => { diff --git a/src/infra/outbound/target-normalization.test.ts b/src/infra/outbound/target-normalization.test.ts index c8e6ea7e124..33b4fd8f08c 100644 --- a/src/infra/outbound/target-normalization.test.ts +++ b/src/infra/outbound/target-normalization.test.ts @@ -4,33 +4,51 @@ const normalizeChannelIdMock = vi.hoisted(() => vi.fn()); const getChannelPluginMock = vi.hoisted(() => vi.fn()); const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn()); -vi.mock("../../channels/plugins/index.js", () => ({ - normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), - getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), -})); +type TargetNormalizationModule = typeof import("./target-normalization.js"); -vi.mock("../../plugins/runtime.js", () => ({ - getActivePluginRegistryVersion: (...args: unknown[]) => - getActivePluginRegistryVersionMock(...args), -})); - -import { - buildTargetResolverSignature, - normalizeChannelTargetInput, - normalizeTargetForProvider, -} from "./target-normalization.js"; +let buildTargetResolverSignature: TargetNormalizationModule["buildTargetResolverSignature"]; +let normalizeChannelTargetInput: TargetNormalizationModule["normalizeChannelTargetInput"]; +let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForProvider"]; describe("normalizeChannelTargetInput", () => { + beforeEach(async () => { + vi.resetModules(); + normalizeChannelIdMock.mockReset(); + getChannelPluginMock.mockReset(); + getActivePluginRegistryVersionMock.mockReset(); + vi.doMock("../../channels/plugins/index.js", () => ({ + normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + })); + vi.doMock("../../plugins/runtime.js", () => ({ + getActivePluginRegistryVersion: (...args: unknown[]) => + getActivePluginRegistryVersionMock(...args), + })); + ({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } = + await import("./target-normalization.js")); + }); + it("trims raw target input", () => { expect(normalizeChannelTargetInput(" channel:C1 ")).toBe("channel:C1"); }); }); describe("normalizeTargetForProvider", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); normalizeChannelIdMock.mockReset(); getChannelPluginMock.mockReset(); getActivePluginRegistryVersionMock.mockReset(); + vi.doMock("../../channels/plugins/index.js", () => ({ + normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + })); + vi.doMock("../../plugins/runtime.js", () => ({ + getActivePluginRegistryVersion: (...args: unknown[]) => + getActivePluginRegistryVersionMock(...args), + })); + ({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } = + await import("./target-normalization.js")); }); it("returns undefined for missing or blank raw input", () => { @@ -87,8 +105,21 @@ describe("normalizeTargetForProvider", () => { }); describe("buildTargetResolverSignature", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + normalizeChannelIdMock.mockReset(); getChannelPluginMock.mockReset(); + getActivePluginRegistryVersionMock.mockReset(); + vi.doMock("../../channels/plugins/index.js", () => ({ + normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + })); + vi.doMock("../../plugins/runtime.js", () => ({ + getActivePluginRegistryVersion: (...args: unknown[]) => + getActivePluginRegistryVersionMock(...args), + })); + ({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } = + await import("./target-normalization.js")); }); it("builds stable signatures from resolver hint and looksLikeId source", () => { diff --git a/src/infra/outbound/target-resolver.test.ts b/src/infra/outbound/target-resolver.test.ts index 643a5c3ed25..0e877a60c6a 100644 --- a/src/infra/outbound/target-resolver.test.ts +++ b/src/infra/outbound/target-resolver.test.ts @@ -1,28 +1,41 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelDirectoryEntry } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { resetDirectoryCache, resolveMessagingTarget } from "./target-resolver.js"; +type TargetResolverModule = typeof import("./target-resolver.js"); + +let resetDirectoryCache: TargetResolverModule["resetDirectoryCache"]; +let resolveMessagingTarget: TargetResolverModule["resolveMessagingTarget"]; const mocks = vi.hoisted(() => ({ listGroups: vi.fn(), listGroupsLive: vi.fn(), resolveTarget: vi.fn(), getChannelPlugin: vi.fn(), + getActivePluginRegistryVersion: vi.fn(() => 1), })); -vi.mock("../../channels/plugins/index.js", () => ({ - getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args), - normalizeChannelId: (value: string) => value, -})); +beforeEach(async () => { + vi.resetModules(); + mocks.listGroups.mockReset(); + mocks.listGroupsLive.mockReset(); + mocks.resolveTarget.mockReset(); + mocks.getChannelPlugin.mockReset(); + mocks.getActivePluginRegistryVersion.mockReset(); + mocks.getActivePluginRegistryVersion.mockReturnValue(1); + vi.doMock("../../channels/plugins/index.js", () => ({ + getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args), + normalizeChannelId: (value: string) => value, + })); + vi.doMock("../../plugins/runtime.js", () => ({ + getActivePluginRegistryVersion: () => mocks.getActivePluginRegistryVersion(), + })); + ({ resetDirectoryCache, resolveMessagingTarget } = await import("./target-resolver.js")); +}); describe("resolveMessagingTarget (directory fallback)", () => { const cfg = {} as OpenClawConfig; beforeEach(() => { - mocks.listGroups.mockClear(); - mocks.listGroupsLive.mockClear(); - mocks.resolveTarget.mockClear(); - mocks.getChannelPlugin.mockClear(); resetDirectoryCache(); mocks.getChannelPlugin.mockReturnValue({ directory: { diff --git a/src/infra/outbound/targets.channel-resolution.test.ts b/src/infra/outbound/targets.channel-resolution.test.ts index e676a425bba..f7e38e0bfef 100644 --- a/src/infra/outbound/targets.channel-resolution.test.ts +++ b/src/infra/outbound/targets.channel-resolution.test.ts @@ -48,7 +48,8 @@ vi.mock("../../config/plugin-auto-enable.js", () => ({ import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; -import { resolveOutboundTarget } from "./targets.js"; + +let resolveOutboundTarget: typeof import("./targets.js").resolveOutboundTarget; describe("resolveOutboundTarget channel resolution", () => { let registrySeq = 0; @@ -60,7 +61,9 @@ describe("resolveOutboundTarget channel resolution", () => { mode: "explicit", }); - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ resolveOutboundTarget } = await import("./targets.js")); registrySeq += 1; setActivePluginRegistry(createTestRegistry([]), `targets-test-${registrySeq}`); mocks.getChannelPlugin.mockReset(); diff --git a/src/infra/pairing-token.test.ts b/src/infra/pairing-token.test.ts index 1ef0c8e20d7..9788e448e49 100644 --- a/src/infra/pairing-token.test.ts +++ b/src/infra/pairing-token.test.ts @@ -1,5 +1,5 @@ import { Buffer } from "node:buffer"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const randomBytesMock = vi.hoisted(() => vi.fn()); @@ -11,7 +11,17 @@ vi.mock("node:crypto", async () => { }; }); -import { generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } from "./pairing-token.js"; +type PairingTokenModule = typeof import("./pairing-token.js"); + +let generatePairingToken: PairingTokenModule["generatePairingToken"]; +let PAIRING_TOKEN_BYTES: PairingTokenModule["PAIRING_TOKEN_BYTES"]; +let verifyPairingToken: PairingTokenModule["verifyPairingToken"]; + +beforeEach(async () => { + vi.resetModules(); + ({ generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } = + await import("./pairing-token.js")); +}); describe("generatePairingToken", () => { it("uses the configured byte count and returns a base64url token", () => { diff --git a/src/infra/ports.test.ts b/src/infra/ports.test.ts index 090ccb128b9..4c3d3597f40 100644 --- a/src/infra/ports.test.ts +++ b/src/infra/ports.test.ts @@ -7,11 +7,20 @@ const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn()); vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), })); -import { inspectPortUsage } from "./ports-inspect.js"; -import { ensurePortAvailable, handlePortError, PortInUseError } from "./ports.js"; + +let inspectPortUsage: typeof import("./ports-inspect.js").inspectPortUsage; +let ensurePortAvailable: typeof import("./ports.js").ensurePortAvailable; +let handlePortError: typeof import("./ports.js").handlePortError; +let PortInUseError: typeof import("./ports.js").PortInUseError; const describeUnix = process.platform === "win32" ? describe.skip : describe; +beforeEach(async () => { + vi.resetModules(); + ({ inspectPortUsage } = await import("./ports-inspect.js")); + ({ ensurePortAvailable, handlePortError, PortInUseError } = await import("./ports.js")); +}); + describe("ports helpers", () => { it("ensurePortAvailable rejects when port busy", async () => { const server = net.createServer(); diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index 6782e89489b..64339a919d2 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -7,12 +7,14 @@ vi.mock("../plugins/provider-runtime.js", () => ({ resolveProviderUsageAuthWithPluginMock(...args), })); -import { resolveProviderAuths } from "./provider-usage.auth.js"; +let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; describe("resolveProviderAuths plugin seam", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); resolveProviderUsageAuthWithPluginMock.mockReset(); resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null); + ({ resolveProviderAuths } = await import("./provider-usage.auth.js")); }); it("prefers plugin-owned usage auth when available", async () => { diff --git a/src/infra/provider-usage.load.plugin.test.ts b/src/infra/provider-usage.load.plugin.test.ts index 55cff6cad72..6d4d7d7b602 100644 --- a/src/infra/provider-usage.load.plugin.test.ts +++ b/src/infra/provider-usage.load.plugin.test.ts @@ -12,14 +12,16 @@ vi.mock("../plugins/provider-runtime.js", () => ({ resolveProviderUsageSnapshotWithPluginMock(...args), })); -import { loadProviderUsageSummary } from "./provider-usage.load.js"; +let loadProviderUsageSummary: typeof import("./provider-usage.load.js").loadProviderUsageSummary; const usageNow = Date.UTC(2026, 0, 7, 0, 0, 0); describe("provider-usage.load plugin seam", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); resolveProviderUsageSnapshotWithPluginMock.mockReset(); resolveProviderUsageSnapshotWithPluginMock.mockResolvedValue(null); + ({ loadProviderUsageSummary } = await import("./provider-usage.load.js")); }); it("prefers plugin-owned usage snapshots", async () => { diff --git a/src/infra/restart-stale-pids.test.ts b/src/infra/restart-stale-pids.test.ts index b7589d26e15..4ff0823e4c3 100644 --- a/src/infra/restart-stale-pids.test.ts +++ b/src/infra/restart-stale-pids.test.ts @@ -32,11 +32,9 @@ vi.mock("../logging/subsystem.js", () => ({ })); import { resolveLsofCommandSync } from "./ports-lsof.js"; -import { - __testing, - cleanStaleGatewayProcessesSync, - findGatewayPidsOnPortSync, -} from "./restart-stale-pids.js"; +let __testing: typeof import("./restart-stale-pids.js").__testing; +let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync; +let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync; function lsofOutput(entries: Array<{ pid: number; cmd: string }>): string { return entries.map(({ pid, cmd }) => `p${pid}\nc${cmd}`).join("\n") + "\n"; @@ -89,6 +87,12 @@ function installInitialBusyPoll( describe.skipIf(isWindows)("restart-stale-pids", () => { beforeEach(() => { + vi.resetModules(); + }); + + beforeEach(async () => { + ({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } = + await import("./restart-stale-pids.js")); mockSpawnSync.mockReset(); mockResolveGatewayPort.mockReset(); mockRestartWarn.mockReset(); diff --git a/src/infra/restart.test.ts b/src/infra/restart.test.ts index e21225be37b..fe6e760041b 100644 --- a/src/infra/restart.test.ts +++ b/src/infra/restart.test.ts @@ -16,13 +16,14 @@ vi.mock("../config/paths.js", () => ({ resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), })); -import { - __testing, - cleanStaleGatewayProcessesSync, - findGatewayPidsOnPortSync, -} from "./restart-stale-pids.js"; +let __testing: typeof import("./restart-stale-pids.js").__testing; +let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync; +let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync; -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + ({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } = + await import("./restart-stale-pids.js")); spawnSyncMock.mockReset(); resolveLsofCommandSyncMock.mockReset(); resolveGatewayPortMock.mockReset(); diff --git a/src/infra/secure-random.test.ts b/src/infra/secure-random.test.ts index 2a595900c7b..1c9f8d949bc 100644 --- a/src/infra/secure-random.test.ts +++ b/src/infra/secure-random.test.ts @@ -1,5 +1,5 @@ import { Buffer } from "node:buffer"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const cryptoMocks = vi.hoisted(() => ({ randomBytes: vi.fn((bytes: number) => Buffer.alloc(bytes, 0xab)), @@ -11,7 +11,13 @@ vi.mock("node:crypto", () => ({ randomUUID: cryptoMocks.randomUUID, })); -import { generateSecureToken, generateSecureUuid } from "./secure-random.js"; +let generateSecureToken: typeof import("./secure-random.js").generateSecureToken; +let generateSecureUuid: typeof import("./secure-random.js").generateSecureUuid; + +beforeEach(async () => { + vi.resetModules(); + ({ generateSecureToken, generateSecureUuid } = await import("./secure-random.js")); +}); describe("secure-random", () => { it("delegates UUID generation to crypto.randomUUID", () => { diff --git a/src/infra/session-maintenance-warning.test.ts b/src/infra/session-maintenance-warning.test.ts index f4c2e0757a1..4395a46df89 100644 --- a/src/infra/session-maintenance-warning.test.ts +++ b/src/infra/session-maintenance-warning.test.ts @@ -15,28 +15,9 @@ const mocks = vi.hoisted(() => ({ enqueueSystemEvent: vi.fn(), })); -vi.mock("../agents/agent-scope.js", () => ({ - resolveSessionAgentId: mocks.resolveSessionAgentId, -})); +type SessionMaintenanceWarningModule = typeof import("./session-maintenance-warning.js"); -vi.mock("../utils/message-channel.js", () => ({ - normalizeMessageChannel: mocks.normalizeMessageChannel, - isDeliverableMessageChannel: mocks.isDeliverableMessageChannel, -})); - -vi.mock("./outbound/targets.js", () => ({ - resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget, -})); - -vi.mock("./outbound/deliver.js", () => ({ - deliverOutboundPayloads: mocks.deliverOutboundPayloads, -})); - -vi.mock("./system-events.js", () => ({ - enqueueSystemEvent: mocks.enqueueSystemEvent, -})); - -const { deliverSessionMaintenanceWarning } = await import("./session-maintenance-warning.js"); +let deliverSessionMaintenanceWarning: SessionMaintenanceWarningModule["deliverSessionMaintenanceWarning"]; function createParams( overrides: Partial[0]> = {}, @@ -62,17 +43,35 @@ describe("deliverSessionMaintenanceWarning", () => { let prevVitest: string | undefined; let prevNodeEnv: string | undefined; - beforeEach(() => { + beforeEach(async () => { prevVitest = process.env.VITEST; prevNodeEnv = process.env.NODE_ENV; delete process.env.VITEST; process.env.NODE_ENV = "development"; + vi.resetModules(); mocks.resolveSessionAgentId.mockClear(); mocks.resolveSessionDeliveryTarget.mockClear(); mocks.normalizeMessageChannel.mockClear(); mocks.isDeliverableMessageChannel.mockClear(); mocks.deliverOutboundPayloads.mockClear(); mocks.enqueueSystemEvent.mockClear(); + vi.doMock("../agents/agent-scope.js", () => ({ + resolveSessionAgentId: mocks.resolveSessionAgentId, + })); + vi.doMock("../utils/message-channel.js", () => ({ + normalizeMessageChannel: mocks.normalizeMessageChannel, + isDeliverableMessageChannel: mocks.isDeliverableMessageChannel, + })); + vi.doMock("./outbound/targets.js", () => ({ + resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget, + })); + vi.doMock("./outbound/deliver.js", () => ({ + deliverOutboundPayloads: mocks.deliverOutboundPayloads, + })); + vi.doMock("./system-events.js", () => ({ + enqueueSystemEvent: mocks.enqueueSystemEvent, + })); + ({ deliverSessionMaintenanceWarning } = await import("./session-maintenance-warning.js")); }); afterEach(() => { diff --git a/src/infra/transport-ready.test.ts b/src/infra/transport-ready.test.ts index a4703ba512c..e55dcb7dd7b 100644 --- a/src/infra/transport-ready.test.ts +++ b/src/infra/transport-ready.test.ts @@ -1,43 +1,45 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { waitForTransportReady } from "./transport-ready.js"; let injectedSleepError: Error | null = null; - -// Perf: `sleepWithAbort` uses `node:timers/promises` which isn't controlled by fake timers. -// Route sleeps through global `setTimeout` so tests can advance time deterministically. -vi.mock("./backoff.js", () => ({ - sleepWithAbort: async (ms: number, signal?: AbortSignal) => { - if (injectedSleepError) { - throw injectedSleepError; - } - if (signal?.aborted) { - throw new Error("aborted"); - } - if (ms <= 0) { - return; - } - await new Promise((resolve, reject) => { - const timer = setTimeout(() => { - signal?.removeEventListener("abort", onAbort); - resolve(); - }, ms); - const onAbort = () => { - clearTimeout(timer); - signal?.removeEventListener("abort", onAbort); - reject(new Error("aborted")); - }; - signal?.addEventListener("abort", onAbort, { once: true }); - }); - }, -})); +type TransportReadyModule = typeof import("./transport-ready.js"); +let waitForTransportReady: TransportReadyModule["waitForTransportReady"]; function createRuntime() { return { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; } describe("waitForTransportReady", () => { - beforeEach(() => { + beforeEach(async () => { vi.useFakeTimers(); + vi.resetModules(); + // Perf: `sleepWithAbort` uses `node:timers/promises` which isn't controlled by fake timers. + // Route sleeps through global `setTimeout` so tests can advance time deterministically. + vi.doMock("./backoff.js", () => ({ + sleepWithAbort: async (ms: number, signal?: AbortSignal) => { + if (injectedSleepError) { + throw injectedSleepError; + } + if (signal?.aborted) { + throw new Error("aborted"); + } + if (ms <= 0) { + return; + } + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + reject(new Error("aborted")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + }); + }, + })); + ({ waitForTransportReady } = await import("./transport-ready.js")); }); afterEach(() => { diff --git a/src/infra/windows-task-restart.test.ts b/src/infra/windows-task-restart.test.ts index 1a25a7a7415..5da5625f9b8 100644 --- a/src/infra/windows-task-restart.test.ts +++ b/src/infra/windows-task-restart.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { captureFullEnv } from "../test-utils/env.js"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -14,7 +14,9 @@ vi.mock("./tmp-openclaw-dir.js", () => ({ resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(), })); -import { relaunchGatewayScheduledTask } from "./windows-task-restart.js"; +type WindowsTaskRestartModule = typeof import("./windows-task-restart.js"); + +let relaunchGatewayScheduledTask: WindowsTaskRestartModule["relaunchGatewayScheduledTask"]; const envSnapshot = captureFullEnv(); const createdScriptPaths = new Set(); @@ -51,6 +53,11 @@ afterEach(() => { }); describe("relaunchGatewayScheduledTask", () => { + beforeEach(async () => { + vi.resetModules(); + ({ relaunchGatewayScheduledTask } = await import("./windows-task-restart.js")); + }); + it("writes a detached schtasks relaunch helper", () => { const unref = vi.fn(); let seenCommandArg = ""; diff --git a/src/infra/wsl.test.ts b/src/infra/wsl.test.ts index d026cf4bbb1..bc1aa23dad0 100644 --- a/src/infra/wsl.test.ts +++ b/src/infra/wsl.test.ts @@ -14,7 +14,11 @@ vi.mock("node:fs/promises", () => ({ }, })); -const { isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js"); +let isWSLEnv: typeof import("./wsl.js").isWSLEnv; +let isWSLSync: typeof import("./wsl.js").isWSLSync; +let isWSL2Sync: typeof import("./wsl.js").isWSL2Sync; +let isWSL: typeof import("./wsl.js").isWSL; +let resetWSLStateForTests: typeof import("./wsl.js").resetWSLStateForTests; const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); @@ -29,13 +33,18 @@ describe("wsl detection", () => { let envSnapshot: ReturnType; beforeEach(() => { + vi.resetModules(); envSnapshot = captureEnv(["WSL_INTEROP", "WSL_DISTRO_NAME", "WSLENV"]); readFileSyncMock.mockReset(); readFileMock.mockReset(); - resetWSLStateForTests(); setPlatform("linux"); }); + beforeEach(async () => { + ({ isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js")); + resetWSLStateForTests(); + }); + afterEach(() => { envSnapshot.restore(); resetWSLStateForTests(); diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.test.ts index 7058cef6bb1..b9fb809f2a0 100644 --- a/src/media-understanding/apply.test.ts +++ b/src/media-understanding/apply.test.ts @@ -2,51 +2,35 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import type { MsgContext } from "../auto-reply/templating.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; -import { fetchRemoteMedia } from "../media/fetch.js"; -import { runExec } from "../process/exec.js"; import { withEnvAsync } from "../test-utils/env.js"; -import { clearMediaUnderstandingBinaryCacheForTests } from "./runner.js"; import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js"; +type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider; + const resolveApiKeyForProviderMock = vi.hoisted(() => - vi.fn(async () => ({ + vi.fn(async () => ({ apiKey: "test-key", // pragma: allowlist secret source: "test", mode: "api-key", })), ); const hasAvailableAuthForProviderMock = vi.hoisted(() => - vi.fn(async (...args: Parameters) => { + vi.fn(async (...args: Parameters) => { const resolved = await resolveApiKeyForProviderMock(...args); return Boolean(resolved?.apiKey); }), ); - -vi.mock("../agents/model-auth.js", () => ({ - resolveApiKeyForProvider: resolveApiKeyForProviderMock, - hasAvailableAuthForProvider: hasAvailableAuthForProviderMock, - requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => { - if (auth?.apiKey) { - return auth.apiKey; - } - throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`); - }, -})); - -vi.mock("../media/fetch.js", () => ({ - fetchRemoteMedia: vi.fn(), -})); - -vi.mock("../process/exec.js", () => ({ - runExec: vi.fn(), -})); +const fetchRemoteMediaMock = vi.hoisted(() => vi.fn()); +const runExecMock = vi.hoisted(() => vi.fn()); let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding; -const mockedRunExec = vi.mocked(runExec); +let clearMediaUnderstandingBinaryCacheForTests: typeof import("./runner.js").clearMediaUnderstandingBinaryCacheForTests; +const mockedResolveApiKey = resolveApiKeyForProviderMock; +const mockedFetchRemoteMedia = fetchRemoteMediaMock; +const mockedRunExec = runExecMock; const TEMP_MEDIA_PREFIX = "openclaw-media-"; let suiteTempMediaRootDir = ""; @@ -241,14 +225,32 @@ function expectFileNotApplied(params: { } describe("applyMediaUnderstanding", () => { - const mockedResolveApiKey = vi.mocked(resolveApiKeyForProvider); - const mockedFetchRemoteMedia = vi.mocked(fetchRemoteMedia); - beforeAll(async () => { + vi.resetModules(); + vi.doMock("../agents/model-auth.js", () => ({ + resolveApiKeyForProvider: resolveApiKeyForProviderMock, + hasAvailableAuthForProvider: hasAvailableAuthForProviderMock, + requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => { + if (auth?.apiKey) { + return auth.apiKey; + } + throw new Error( + `No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`, + ); + }, + })); + vi.doMock("../media/fetch.js", () => ({ + fetchRemoteMedia: fetchRemoteMediaMock, + })); + vi.doMock("../process/exec.js", () => ({ + runExec: runExecMock, + })); + ({ applyMediaUnderstanding } = await import("./apply.js")); + ({ clearMediaUnderstandingBinaryCacheForTests } = await import("./runner.js")); + const baseDir = resolvePreferredOpenClawTmpDir(); await fs.mkdir(baseDir, { recursive: true }); suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX)); - ({ applyMediaUnderstanding } = await import("./apply.js")); }); beforeEach(() => { diff --git a/src/media-understanding/providers/image.test.ts b/src/media-understanding/providers/image.test.ts index d52c6590eef..9044d8ba83d 100644 --- a/src/media-understanding/providers/image.test.ts +++ b/src/media-understanding/providers/image.test.ts @@ -16,49 +16,43 @@ const resolveApiKeyForProviderMock = vi.fn(async () => ({ const requireApiKeyMock = vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? ""); const setRuntimeApiKeyMock = vi.fn(); const discoverModelsMock = vi.fn(); -let imageImportSeq = 0; +type ImageModule = typeof import("./image.js"); -vi.mock("@mariozechner/pi-ai", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - complete: completeMock, - }; -}); - -vi.mock("../../agents/minimax-vlm.js", () => ({ - isMinimaxVlmProvider: (provider: string) => - provider === "minimax" || provider === "minimax-portal", - isMinimaxVlmModel: (provider: string, modelId: string) => - (provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01", - minimaxUnderstandImage: minimaxUnderstandImageMock, -})); - -vi.mock("../../agents/models-config.js", () => ({ - ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock, -})); - -vi.mock("../../agents/model-auth.js", () => ({ - getApiKeyForModel: getApiKeyForModelMock, - resolveApiKeyForProvider: resolveApiKeyForProviderMock, - requireApiKey: requireApiKeyMock, -})); - -vi.mock("../../agents/pi-model-discovery-runtime.js", () => ({ - discoverAuthStorage: () => ({ - setRuntimeApiKey: setRuntimeApiKeyMock, - }), - discoverModels: discoverModelsMock, -})); - -async function importImageModule() { - imageImportSeq += 1; - return await import(/* @vite-ignore */ `./image.js?case=${imageImportSeq}`); -} +let describeImageWithModel: ImageModule["describeImageWithModel"]; describe("describeImageWithModel", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); + vi.doMock("@mariozechner/pi-ai", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + complete: completeMock, + }; + }); + vi.doMock("../../agents/minimax-vlm.js", () => ({ + isMinimaxVlmProvider: (provider: string) => + provider === "minimax" || provider === "minimax-portal", + isMinimaxVlmModel: (provider: string, modelId: string) => + (provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01", + minimaxUnderstandImage: minimaxUnderstandImageMock, + })); + vi.doMock("../../agents/models-config.js", () => ({ + ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock, + })); + vi.doMock("../../agents/model-auth.js", () => ({ + getApiKeyForModel: getApiKeyForModelMock, + resolveApiKeyForProvider: resolveApiKeyForProviderMock, + requireApiKey: requireApiKeyMock, + })); + vi.doMock("../../agents/pi-model-discovery-runtime.js", () => ({ + discoverAuthStorage: () => ({ + setRuntimeApiKey: setRuntimeApiKeyMock, + }), + discoverModels: discoverModelsMock, + })); + ({ describeImageWithModel } = await import("./image.js")); minimaxUnderstandImageMock.mockResolvedValue("portal ok"); discoverModelsMock.mockReturnValue({ find: vi.fn(() => ({ @@ -71,8 +65,6 @@ describe("describeImageWithModel", () => { }); it("routes minimax-portal image models through the MiniMax VLM endpoint", async () => { - const { describeImageWithModel } = await importImageModule(); - const result = await describeImageWithModel({ cfg: {}, agentDir: "/tmp/openclaw-agent", @@ -121,8 +113,6 @@ describe("describeImageWithModel", () => { content: [{ type: "text", text: "generic ok" }], }); - const { describeImageWithModel } = await importImageModule(); - const result = await describeImageWithModel({ cfg: {}, agentDir: "/tmp/openclaw-agent", @@ -165,8 +155,6 @@ describe("describeImageWithModel", () => { content: [{ type: "text", text: "flash ok" }], }); - const { describeImageWithModel } = await importImageModule(); - const result = await describeImageWithModel({ cfg: {}, agentDir: "/tmp/openclaw-agent", @@ -215,8 +203,6 @@ describe("describeImageWithModel", () => { content: [{ type: "text", text: "flash lite ok" }], }); - const { describeImageWithModel } = await importImageModule(); - const result = await describeImageWithModel({ cfg: {}, agentDir: "/tmp/openclaw-agent", diff --git a/src/media/fetch.telegram-network.test.ts b/src/media/fetch.telegram-network.test.ts index d7a4d8e217d..60e60f1c48c 100644 --- a/src/media/fetch.telegram-network.test.ts +++ b/src/media/fetch.telegram-network.test.ts @@ -1,9 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - resolveTelegramTransport, - shouldRetryTelegramIpv4Fallback, -} from "../../extensions/telegram/src/fetch.js"; -import { fetchRemoteMedia } from "./fetch.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const undiciMocks = vi.hoisted(() => { const createDispatcherCtor = | string>() => @@ -26,9 +21,20 @@ vi.mock("undici", () => ({ fetch: undiciMocks.fetch, })); +let resolveTelegramTransport: typeof import("../../extensions/telegram/src/fetch.js").resolveTelegramTransport; +let shouldRetryTelegramIpv4Fallback: typeof import("../../extensions/telegram/src/fetch.js").shouldRetryTelegramIpv4Fallback; +let fetchRemoteMedia: typeof import("./fetch.js").fetchRemoteMedia; + describe("fetchRemoteMedia telegram network policy", () => { type LookupFn = NonNullable[0]["lookupFn"]>; + beforeEach(async () => { + vi.resetModules(); + ({ resolveTelegramTransport, shouldRetryTelegramIpv4Fallback } = + await import("../../extensions/telegram/src/fetch.js")); + ({ fetchRemoteMedia } = await import("./fetch.js")); + }); + function createTelegramFetchFailedError(code: string): Error { return Object.assign(new TypeError("fetch failed"), { cause: { code }, diff --git a/src/media/input-files.fetch-guard.test.ts b/src/media/input-files.fetch-guard.test.ts index 377bbf78fa9..6bd9fbb4b81 100644 --- a/src/media/input-files.fetch-guard.test.ts +++ b/src/media/input-files.fetch-guard.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const fetchWithSsrFGuardMock = vi.fn(); const convertHeicToJpegMock = vi.fn(); @@ -24,15 +24,13 @@ let fetchWithGuard: typeof import("./input-files.js").fetchWithGuard; let extractImageContentFromSource: typeof import("./input-files.js").extractImageContentFromSource; let extractFileContentFromSource: typeof import("./input-files.js").extractFileContentFromSource; -beforeAll(async () => { +beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); ({ fetchWithGuard, extractImageContentFromSource, extractFileContentFromSource } = await import("./input-files.js")); }); -beforeEach(() => { - vi.clearAllMocks(); -}); - describe("HEIC input image normalization", () => { it("converts base64 HEIC images to JPEG before returning them", async () => { const normalized = Buffer.from("jpeg-normalized"); diff --git a/src/media/store.outside-workspace.test.ts b/src/media/store.outside-workspace.test.ts index 6483a856cd9..97c8c9df52b 100644 --- a/src/media/store.outside-workspace.test.ts +++ b/src/media/store.outside-workspace.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js"; const mocks = vi.hoisted(() => ({ @@ -15,13 +15,22 @@ vi.mock("../infra/fs-safe.js", async (importOriginal) => { }; }); -const { saveMediaSource } = await import("./store.js"); -const { SafeOpenError } = await import("../infra/fs-safe.js"); +type StoreModule = typeof import("./store.js"); +type FsSafeModule = typeof import("../infra/fs-safe.js"); + +let saveMediaSource: StoreModule["saveMediaSource"]; +let SafeOpenError: FsSafeModule["SafeOpenError"]; describe("media store outside-workspace mapping", () => { let tempHome: TempHomeEnv; let home = ""; + beforeEach(async () => { + vi.resetModules(); + ({ saveMediaSource } = await import("./store.js")); + ({ SafeOpenError } = await import("../infra/fs-safe.js")); + }); + beforeAll(async () => { tempHome = await createTempHomeEnv("openclaw-media-store-test-home-"); home = tempHome.home; diff --git a/src/memory/batch-http.test.ts b/src/memory/batch-http.test.ts index d70cdf292a2..275e3725eb9 100644 --- a/src/memory/batch-http.test.ts +++ b/src/memory/batch-http.test.ts @@ -1,7 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { retryAsync } from "../infra/retry.js"; -import { postJsonWithRetry } from "./batch-http.js"; -import { postJson } from "./post-json.js"; vi.mock("../infra/retry.js", () => ({ retryAsync: vi.fn(async (run: () => Promise) => await run()), @@ -12,11 +9,18 @@ vi.mock("./post-json.js", () => ({ })); describe("postJsonWithRetry", () => { - const retryAsyncMock = vi.mocked(retryAsync); - const postJsonMock = vi.mocked(postJson); + let retryAsyncMock: ReturnType>; + let postJsonMock: ReturnType>; + let postJsonWithRetry: typeof import("./batch-http.js").postJsonWithRetry; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); + ({ postJsonWithRetry } = await import("./batch-http.js")); + const retryModule = await import("../infra/retry.js"); + const postJsonModule = await import("./post-json.js"); + retryAsyncMock = vi.mocked(retryModule.retryAsync); + postJsonMock = vi.mocked(postJsonModule.postJson); }); it("posts JSON and returns parsed response payload", async () => { diff --git a/src/memory/embedding-manager.test-harness.ts b/src/memory/embedding-manager.test-harness.ts index 6835c9cce27..c0e973fade1 100644 --- a/src/memory/embedding-manager.test-harness.ts +++ b/src/memory/embedding-manager.test-harness.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, beforeAll, beforeEach, expect } from "vitest"; +import { afterAll, beforeAll, beforeEach, expect, vi, type Mock } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getEmbedBatchMock, resetEmbeddingMocks } from "./embedding.test-mocks.js"; -import { - getMemorySearchManager, - type MemoryIndexManager, - type MemorySearchManager, -} from "./index.js"; +import type { MemoryIndexManager, MemorySearchManager } from "./index.js"; + +type EmbeddingTestMocksModule = typeof import("./embedding.test-mocks.js"); +type MemoryIndexModule = typeof import("./index.js"); export function installEmbeddingManagerFixture(opts: { fixturePrefix: string; @@ -21,7 +19,6 @@ export function installEmbeddingManagerFixture(opts: { }) => OpenClawConfig; resetIndexEachTest?: boolean; }) { - const embedBatch = getEmbedBatchMock(); const resetIndexEachTest = opts.resetIndexEachTest ?? true; let fixtureRoot: string | undefined; @@ -29,6 +26,9 @@ export function installEmbeddingManagerFixture(opts: { let memoryDir: string | undefined; let managerLarge: MemoryIndexManager | undefined; let managerSmall: MemoryIndexManager | undefined; + let embedBatch: Mock<(texts: string[]) => Promise> | undefined; + let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; + let resetEmbeddingMocks: EmbeddingTestMocksModule["resetEmbeddingMocks"]; const resetManager = (manager: MemoryIndexManager) => { (manager as unknown as { resetIndex: () => void }).resetIndex(); @@ -56,6 +56,12 @@ export function installEmbeddingManagerFixture(opts: { }; beforeAll(async () => { + vi.resetModules(); + await import("./embedding.test-mocks.js"); + const embeddingMocks = await import("./embedding.test-mocks.js"); + embedBatch = embeddingMocks.getEmbedBatchMock(); + resetEmbeddingMocks = embeddingMocks.resetEmbeddingMocks; + ({ getMemorySearchManager } = await import("./index.js")); fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), opts.fixturePrefix)); workspaceDir = path.join(fixtureRoot, "workspace"); memoryDir = path.join(workspaceDir, "memory"); @@ -116,7 +122,9 @@ export function installEmbeddingManagerFixture(opts: { }); return { - embedBatch, + get embedBatch() { + return requireValue(embedBatch, "embedBatch"); + }, getFixtureRoot: () => requireValue(fixtureRoot, "fixtureRoot"), getWorkspaceDir: () => requireValue(workspaceDir, "workspaceDir"), getMemoryDir: () => requireValue(memoryDir, "memoryDir"), diff --git a/src/memory/embeddings-remote-fetch.test.ts b/src/memory/embeddings-remote-fetch.test.ts index bcef98fafda..eeaa39e9277 100644 --- a/src/memory/embeddings-remote-fetch.test.ts +++ b/src/memory/embeddings-remote-fetch.test.ts @@ -1,15 +1,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js"; import { postJson } from "./post-json.js"; vi.mock("./post-json.js", () => ({ postJson: vi.fn(), })); +type EmbeddingsRemoteFetchModule = typeof import("./embeddings-remote-fetch.js"); + +let fetchRemoteEmbeddingVectors: EmbeddingsRemoteFetchModule["fetchRemoteEmbeddingVectors"]; + describe("fetchRemoteEmbeddingVectors", () => { const postJsonMock = vi.mocked(postJson); - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ fetchRemoteEmbeddingVectors } = await import("./embeddings-remote-fetch.js")); vi.clearAllMocks(); }); diff --git a/src/memory/embeddings-voyage.test.ts b/src/memory/embeddings-voyage.test.ts index ccc164bd064..9dac8c04d75 100644 --- a/src/memory/embeddings-voyage.test.ts +++ b/src/memory/embeddings-voyage.test.ts @@ -1,7 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import * as authModule from "../agents/model-auth.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type FetchMock, withFetchPreconnect } from "../test-utils/fetch-mock.js"; -import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js"; import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js"; vi.mock("../agents/model-auth.js", async () => { @@ -20,6 +18,17 @@ const createFetchMock = () => { return withFetchPreconnect(fetchMock); }; +let authModule: typeof import("../agents/model-auth.js"); +let createVoyageEmbeddingProvider: typeof import("./embeddings-voyage.js").createVoyageEmbeddingProvider; +let normalizeVoyageModel: typeof import("./embeddings-voyage.js").normalizeVoyageModel; + +beforeEach(async () => { + vi.resetModules(); + authModule = await import("../agents/model-auth.js"); + ({ createVoyageEmbeddingProvider, normalizeVoyageModel } = + await import("./embeddings-voyage.js")); +}); + function mockVoyageApiKey() { vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({ apiKey: "voyage-key-123", diff --git a/src/memory/embeddings.test.ts b/src/memory/embeddings.test.ts index f15624ee1cb..e9a533f4f9d 100644 --- a/src/memory/embeddings.test.ts +++ b/src/memory/embeddings.test.ts @@ -1,7 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import * as authModule from "../agents/model-auth.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js"; -import { createEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js"; import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js"; vi.mock("../agents/model-auth.js", async () => { @@ -33,12 +31,25 @@ function readFirstFetchRequest(fetchMock: { mock: { calls: unknown[][] } }) { return { url, init: init as RequestInit | undefined }; } +type EmbeddingsModule = typeof import("./embeddings.js"); +type AuthModule = typeof import("../agents/model-auth.js"); + +let authModule: AuthModule; +let createEmbeddingProvider: EmbeddingsModule["createEmbeddingProvider"]; +let DEFAULT_LOCAL_MODEL: EmbeddingsModule["DEFAULT_LOCAL_MODEL"]; + +beforeEach(async () => { + vi.resetModules(); + authModule = await import("../agents/model-auth.js"); + ({ createEmbeddingProvider, DEFAULT_LOCAL_MODEL } = await import("./embeddings.js")); +}); + afterEach(() => { vi.resetAllMocks(); vi.unstubAllGlobals(); }); -function requireProvider(result: Awaited>) { +function requireProvider(result: Awaited>) { if (!result.provider) { throw new Error("Expected embedding provider"); } @@ -71,7 +82,7 @@ function createLocalProvider(options?: { fallback?: "none" | "openai" }) { } function expectAutoSelectedProvider( - result: Awaited>, + result: Awaited>, expectedId: "openai" | "gemini" | "mistral", ) { expect(result.requestedProvider).toBe("auto"); diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index dcb0b061073..1072eab2cc4 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -3,8 +3,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; import "./test-runtime-mocks.js"; +import type { MemoryIndexManager } from "./index.js"; + +type MemoryIndexModule = typeof import("./index.js"); + +let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; let embedBatchCalls = 0; let embedBatchInputCalls = 0; @@ -151,6 +155,9 @@ describe("memory index", () => { }); beforeEach(async () => { + vi.resetModules(); + await import("./test-runtime-mocks.js"); + ({ getMemorySearchManager } = await import("./index.js")); // Perf: most suites don't need atomic swap behavior for full reindexes. // Keep atomic reindex tests on the safe path. vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "1"); diff --git a/src/memory/manager.atomic-reindex.test.ts b/src/memory/manager.atomic-reindex.test.ts index d7d610312f5..b4dd35f9f37 100644 --- a/src/memory/manager.atomic-reindex.test.ts +++ b/src/memory/manager.atomic-reindex.test.ts @@ -3,25 +3,33 @@ import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getEmbedBatchMock, resetEmbeddingMocks } from "./embedding.test-mocks.js"; import type { MemoryIndexManager } from "./index.js"; -import { getRequiredMemoryIndexManager } from "./test-manager-helpers.js"; let shouldFail = false; +type EmbeddingTestMocksModule = typeof import("./embedding.test-mocks.js"); +type TestManagerHelpersModule = typeof import("./test-manager-helpers.js"); + describe("memory manager atomic reindex", () => { let fixtureRoot = ""; let caseId = 0; let workspaceDir: string; let indexPath: string; let manager: MemoryIndexManager | null = null; - const embedBatch = getEmbedBatchMock(); + let embedBatch: ReturnType; + let resetEmbeddingMocks: EmbeddingTestMocksModule["resetEmbeddingMocks"]; + let getRequiredMemoryIndexManager: TestManagerHelpersModule["getRequiredMemoryIndexManager"]; beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-atomic-")); }); beforeEach(async () => { + vi.resetModules(); + const embeddingMocks = await import("./embedding.test-mocks.js"); + embedBatch = embeddingMocks.getEmbedBatchMock(); + resetEmbeddingMocks = embeddingMocks.resetEmbeddingMocks; + ({ getRequiredMemoryIndexManager } = await import("./test-manager-helpers.js")); vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "0"); resetEmbeddingMocks(); shouldFail = false; diff --git a/src/memory/manager.batch.test.ts b/src/memory/manager.batch.test.ts index 453f1a6c815..38be2020f35 100644 --- a/src/memory/manager.batch.test.ts +++ b/src/memory/manager.batch.test.ts @@ -4,21 +4,15 @@ import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { useFastShortTimeouts } from "../../test/helpers/fast-short-timeouts.js"; import type { OpenClawConfig } from "../config/config.js"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; import { createOpenAIEmbeddingProviderMock } from "./test-embeddings-mock.js"; import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js"; -import "./test-runtime-mocks.js"; + +type MemoryIndexManager = import("./index.js").MemoryIndexManager; +type MemoryIndexModule = typeof import("./index.js"); const embedBatch = vi.fn(async (_texts: string[]) => [] as number[][]); const embedQuery = vi.fn(async () => [0.5, 0.5, 0.5]); - -vi.mock("./embeddings.js", () => ({ - createEmbeddingProvider: async () => - createOpenAIEmbeddingProviderMock({ - embedQuery, - embedBatch, - }), -})); +let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; describe("memory indexing with OpenAI batches", () => { let fixtureRoot: string; @@ -118,6 +112,17 @@ describe("memory indexing with OpenAI batches", () => { } beforeAll(async () => { + vi.resetModules(); + vi.doMock("./embeddings.js", () => ({ + createEmbeddingProvider: async () => + createOpenAIEmbeddingProviderMock({ + embedQuery, + embedBatch, + }), + })); + await import("./test-runtime-mocks.js"); + ({ getMemorySearchManager } = await import("./index.js")); + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-batch-")); workspaceDir = path.join(fixtureRoot, "workspace"); memoryDir = path.join(workspaceDir, "memory"); diff --git a/src/memory/manager.embedding-batches.test.ts b/src/memory/manager.embedding-batches.test.ts index e2af1ed97f2..d7b1071deed 100644 --- a/src/memory/manager.embedding-batches.test.ts +++ b/src/memory/manager.embedding-batches.test.ts @@ -25,7 +25,6 @@ const fx = installEmbeddingManagerFixture({ }, }), }); -const { embedBatch } = fx; describe("memory embedding batches", () => { async function expectSyncWithFastTimeouts(manager: { @@ -55,13 +54,13 @@ describe("memory embedding batches", () => { }); const status = managerLarge.status(); - const totalTexts = embedBatch.mock.calls.reduce( + const totalTexts = fx.embedBatch.mock.calls.reduce( (sum: number, call: unknown[]) => sum + ((call[0] as string[] | undefined)?.length ?? 0), 0, ); expect(totalTexts).toBe(status.chunks); - expect(embedBatch.mock.calls.length).toBeGreaterThan(1); - const inputs: string[] = embedBatch.mock.calls.flatMap( + expect(fx.embedBatch.mock.calls.length).toBeGreaterThan(1); + const inputs: string[] = fx.embedBatch.mock.calls.flatMap( (call: unknown[]) => (call[0] as string[] | undefined) ?? [], ); expect(inputs.every((text) => Buffer.byteLength(text, "utf8") <= 8000)).toBe(true); @@ -80,7 +79,7 @@ describe("memory embedding batches", () => { await fs.writeFile(path.join(memoryDir, "2026-01-04.md"), content); await managerSmall.sync({ reason: "test" }); - expect(embedBatch.mock.calls.length).toBe(1); + expect(fx.embedBatch.mock.calls.length).toBe(1); }); it("retries embeddings on transient rate limit and 5xx errors", async () => { @@ -95,7 +94,7 @@ describe("memory embedding batches", () => { "openai embeddings failed: 502 Bad Gateway (cloudflare)", ]; let calls = 0; - embedBatch.mockImplementation(async (texts: string[]) => { + fx.embedBatch.mockImplementation(async (texts: string[]) => { calls += 1; const transient = transientErrors[calls - 1]; if (transient) { @@ -117,7 +116,7 @@ describe("memory embedding batches", () => { await fs.writeFile(path.join(memoryDir, "2026-01-08.md"), content); let calls = 0; - embedBatch.mockImplementation(async (texts: string[]) => { + fx.embedBatch.mockImplementation(async (texts: string[]) => { calls += 1; if (calls === 1) { throw new Error("AWS Bedrock embeddings failed: Too many tokens per day"); @@ -136,7 +135,9 @@ describe("memory embedding batches", () => { await fs.writeFile(path.join(memoryDir, "2026-01-07.md"), "\n\n\n"); await managerSmall.sync({ reason: "test" }); - const inputs = embedBatch.mock.calls.flatMap((call: unknown[]) => (call[0] as string[]) ?? []); + const inputs = fx.embedBatch.mock.calls.flatMap( + (call: unknown[]) => (call[0] as string[]) ?? [], + ); expect(inputs).not.toContain(""); }); }); diff --git a/src/memory/manager.get-concurrency.test.ts b/src/memory/manager.get-concurrency.test.ts index 515a9d8226d..236f6780b84 100644 --- a/src/memory/manager.get-concurrency.test.ts +++ b/src/memory/manager.get-concurrency.test.ts @@ -3,12 +3,11 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; -import { - closeAllMemoryIndexManagers, - MemoryIndexManager as RawMemoryIndexManager, -} from "./manager.js"; import "./test-runtime-mocks.js"; +import type { MemoryIndexManager } from "./index.js"; + +type MemoryIndexModule = typeof import("./index.js"); +type ManagerModule = typeof import("./manager.js"); const hoisted = vi.hoisted(() => ({ providerCreateCalls: 0, @@ -34,10 +33,19 @@ vi.mock("./embeddings.js", () => ({ }, })); +let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; +let closeAllMemoryIndexManagers: ManagerModule["closeAllMemoryIndexManagers"]; +let RawMemoryIndexManager: ManagerModule["MemoryIndexManager"]; + describe("memory manager cache hydration", () => { let workspaceDir = ""; beforeEach(async () => { + vi.resetModules(); + await import("./test-runtime-mocks.js"); + ({ getMemorySearchManager } = await import("./index.js")); + ({ closeAllMemoryIndexManagers, MemoryIndexManager: RawMemoryIndexManager } = + await import("./manager.js")); workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-concurrent-")); await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true }); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello memory."); diff --git a/src/memory/manager.mistral-provider.test.ts b/src/memory/manager.mistral-provider.test.ts index 3345b01933c..be10e3c232b 100644 --- a/src/memory/manager.mistral-provider.test.ts +++ b/src/memory/manager.mistral-provider.test.ts @@ -11,7 +11,7 @@ import type { OllamaEmbeddingClient, OpenAiEmbeddingClient, } from "./embeddings.js"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; +import type { MemoryIndexManager } from "./index.js"; const { createEmbeddingProviderMock } = vi.hoisted(() => ({ createEmbeddingProviderMock: vi.fn(), @@ -25,6 +25,10 @@ vi.mock("./sqlite-vec.js", () => ({ loadSqliteVecExtension: async () => ({ ok: false, error: "sqlite-vec disabled in tests" }), })); +type MemoryIndexModule = typeof import("./index.js"); + +let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; + function createProvider(id: string): EmbeddingProvider { return { id, @@ -64,6 +68,8 @@ describe("memory manager mistral provider wiring", () => { let manager: MemoryIndexManager | null = null; beforeEach(async () => { + vi.resetModules(); + ({ getMemorySearchManager } = await import("./index.js")); createEmbeddingProviderMock.mockReset(); workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-mistral-")); indexPath = path.join(workspaceDir, "index.sqlite"); diff --git a/src/memory/manager.vector-dedupe.test.ts b/src/memory/manager.vector-dedupe.test.ts index fcd21a88431..64242ec3f0e 100644 --- a/src/memory/manager.vector-dedupe.test.ts +++ b/src/memory/manager.vector-dedupe.test.ts @@ -4,8 +4,6 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { MemoryIndexManager } from "./index.js"; -import { buildFileEntry } from "./internal.js"; -import { createMemoryManagerOrThrow } from "./test-manager.js"; vi.mock("./embeddings.js", () => { return { @@ -21,6 +19,12 @@ vi.mock("./embeddings.js", () => { }; }); +type MemoryInternalModule = typeof import("./internal.js"); +type TestManagerModule = typeof import("./test-manager.js"); + +let buildFileEntry: MemoryInternalModule["buildFileEntry"]; +let createMemoryManagerOrThrow: TestManagerModule["createMemoryManagerOrThrow"]; + describe("memory vector dedupe", () => { let workspaceDir: string; let indexPath: string; @@ -40,6 +44,9 @@ describe("memory vector dedupe", () => { } beforeEach(async () => { + vi.resetModules(); + ({ buildFileEntry } = await import("./internal.js")); + ({ createMemoryManagerOrThrow } = await import("./test-manager.js")); workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); indexPath = path.join(workspaceDir, "index.sqlite"); await seedMemoryWorkspace(workspaceDir); diff --git a/src/memory/manager.watcher-config.test.ts b/src/memory/manager.watcher-config.test.ts index b10cf84c71f..36d1b830e4a 100644 --- a/src/memory/manager.watcher-config.test.ts +++ b/src/memory/manager.watcher-config.test.ts @@ -1,10 +1,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { MemorySearchConfig } from "../config/types.tools.js"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; +import type { MemoryIndexManager } from "./index.js"; const { watchMock } = vi.hoisted(() => ({ watchMock: vi.fn(() => ({ @@ -34,11 +34,20 @@ vi.mock("./embeddings.js", () => ({ }), })); +type MemoryIndexModule = typeof import("./index.js"); + +let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"]; + describe("memory watcher config", () => { let manager: MemoryIndexManager | null = null; let workspaceDir = ""; let extraDir = ""; + beforeEach(async () => { + vi.resetModules(); + ({ getMemorySearchManager } = await import("./index.js")); + }); + afterEach(async () => { watchMock.mockClear(); if (manager) { diff --git a/src/memory/post-json.test.ts b/src/memory/post-json.test.ts index 7e1aaf27cb6..1fd4210c111 100644 --- a/src/memory/post-json.test.ts +++ b/src/memory/post-json.test.ts @@ -1,16 +1,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { postJson } from "./post-json.js"; -import { withRemoteHttpResponse } from "./remote-http.js"; vi.mock("./remote-http.js", () => ({ withRemoteHttpResponse: vi.fn(), })); -describe("postJson", () => { - const remoteHttpMock = vi.mocked(withRemoteHttpResponse); +let postJson: typeof import("./post-json.js").postJson; +let withRemoteHttpResponse: typeof import("./remote-http.js").withRemoteHttpResponse; - beforeEach(() => { +describe("postJson", () => { + let remoteHttpMock: ReturnType>; + + beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); + ({ postJson } = await import("./post-json.js")); + ({ withRemoteHttpResponse } = await import("./remote-http.js")); + remoteHttpMock = vi.mocked(withRemoteHttpResponse); }); it("parses JSON payload on successful response", async () => { diff --git a/src/memory/test-manager-helpers.ts b/src/memory/test-manager-helpers.ts index 4bbcf2d608e..cfe3f09e49f 100644 --- a/src/memory/test-manager-helpers.ts +++ b/src/memory/test-manager-helpers.ts @@ -1,10 +1,12 @@ import type { OpenClawConfig } from "../config/config.js"; -import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; +import type { MemoryIndexManager } from "./index.js"; export async function getRequiredMemoryIndexManager(params: { cfg: OpenClawConfig; agentId?: string; }): Promise { + await import("./embedding.test-mocks.js"); + const { getMemorySearchManager } = await import("./index.js"); const result = await getMemorySearchManager({ cfg: params.cfg, agentId: params.agentId ?? "main", diff --git a/src/pairing/setup-code.test.ts b/src/pairing/setup-code.test.ts index e72a9399623..6622f6c010f 100644 --- a/src/pairing/setup-code.test.ts +++ b/src/pairing/setup-code.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SecretInput } from "../config/types.secrets.js"; -import { encodePairingSetupCode, resolvePairingSetupFromConfig } from "./setup-code.js"; vi.mock("../infra/device-bootstrap.js", () => ({ issueDeviceBootstrapToken: vi.fn(async () => ({ @@ -9,6 +8,9 @@ vi.mock("../infra/device-bootstrap.js", () => ({ })), })); +let encodePairingSetupCode: typeof import("./setup-code.js").encodePairingSetupCode; +let resolvePairingSetupFromConfig: typeof import("./setup-code.js").resolvePairingSetupFromConfig; + describe("pairing setup code", () => { type ResolvedSetup = Awaited>; const defaultEnvSecretProviderConfig = { @@ -68,10 +70,17 @@ describe("pairing setup code", () => { } beforeEach(() => { + vi.resetModules(); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", ""); vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", ""); vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", ""); vi.stubEnv("CLAWDBOT_GATEWAY_PASSWORD", ""); + vi.stubEnv("OPENCLAW_GATEWAY_PORT", ""); + vi.stubEnv("CLAWDBOT_GATEWAY_PORT", ""); + }); + + beforeEach(async () => { + ({ encodePairingSetupCode, resolvePairingSetupFromConfig } = await import("./setup-code.js")); }); afterEach(() => { diff --git a/src/plugin-sdk/outbound-media.test.ts b/src/plugin-sdk/outbound-media.test.ts index bc56f2e6ea4..84b0db6def9 100644 --- a/src/plugin-sdk/outbound-media.test.ts +++ b/src/plugin-sdk/outbound-media.test.ts @@ -1,5 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; -import { loadOutboundMediaFromUrl } from "./outbound-media.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const loadWebMediaMock = vi.hoisted(() => vi.fn()); @@ -7,7 +6,17 @@ vi.mock("../../extensions/whatsapp/src/media.js", () => ({ loadWebMedia: loadWebMediaMock, })); +type OutboundMediaModule = typeof import("./outbound-media.js"); + +let loadOutboundMediaFromUrl: OutboundMediaModule["loadOutboundMediaFromUrl"]; + describe("loadOutboundMediaFromUrl", () => { + beforeEach(async () => { + vi.resetModules(); + ({ loadOutboundMediaFromUrl } = await import("./outbound-media.js")); + loadWebMediaMock.mockReset(); + }); + it("forwards maxBytes and mediaLocalRoots to loadWebMedia", async () => { loadWebMediaMock.mockResolvedValueOnce({ buffer: Buffer.from("x"), diff --git a/src/plugins/contracts/auth.contract.test.ts b/src/plugins/contracts/auth.contract.test.ts index 4842bef5e76..40a82482edd 100644 --- a/src/plugins/contracts/auth.contract.test.ts +++ b/src/plugins/contracts/auth.contract.test.ts @@ -4,43 +4,61 @@ import { replaceRuntimeAuthProfileStoreSnapshots, } from "../../agents/auth-profiles/store.js"; import { createNonExitingRuntime } from "../../runtime.js"; +import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js"; import type { WizardMultiSelectParams, WizardPrompter, WizardProgress, WizardSelectParams, } from "../../wizard/prompts.js"; -import { registerProviders, requireProvider } from "./testkit.js"; +import type { OpenClawPluginApi, ProviderPlugin } from "../types.js"; type LoginOpenAICodexOAuth = - (typeof import("../../plugins/provider-openai-codex-oauth.js"))["loginOpenAICodexOAuth"]; + (typeof import("openclaw/plugin-sdk/provider-auth"))["loginOpenAICodexOAuth"]; type LoginQwenPortalOAuth = (typeof import("../../../extensions/qwen-portal-auth/oauth.js"))["loginQwenPortalOAuth"]; type GithubCopilotLoginCommand = - (typeof import("../../providers/github-copilot-auth.js"))["githubCopilotLoginCommand"]; + (typeof import("openclaw/plugin-sdk/provider-auth"))["githubCopilotLoginCommand"]; type CreateVpsAwareHandlers = - (typeof import("../../plugins/provider-oauth-flow.js"))["createVpsAwareOAuthHandlers"]; + (typeof import("../../commands/oauth-flow.js"))["createVpsAwareOAuthHandlers"]; const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn()); const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn()); const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn()); -vi.mock("../../plugins/provider-openai-codex-oauth.js", () => ({ - loginOpenAICodexOAuth: loginOpenAICodexOAuthMock, -})); +vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loginOpenAICodexOAuth: loginOpenAICodexOAuthMock, + githubCopilotLoginCommand: githubCopilotLoginCommandMock, + }; +}); vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({ loginQwenPortalOAuth: loginQwenPortalOAuthMock, })); -vi.mock("../../providers/github-copilot-auth.js", () => ({ - githubCopilotLoginCommand: githubCopilotLoginCommandMock, -})); - const openAIPlugin = (await import("../../../extensions/openai/index.js")).default; const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default; const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default; +function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) { + const captured = createCapturedPluginRegistration(); + for (const plugin of plugins) { + plugin.register(captured.api); + } + return captured.providers; +} + +function requireProvider(providers: ProviderPlugin[], providerId: string) { + const provider = providers.find((entry) => entry.id === providerId); + if (!provider) { + throw new Error(`provider ${providerId} missing`); + } + return provider; +} + function buildPrompter(): WizardPrompter { const progress: WizardProgress = { update() {}, diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 07ee1794562..c6cb64db8eb 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -23,30 +23,28 @@ vi.mock("./providers.js", () => ({ resolveOwningPluginIdsForProviderMock(params as never), })); -import { - augmentModelCatalogWithProviderPlugins, - buildProviderAuthDoctorHintWithPlugin, - buildProviderMissingAuthMessageWithPlugin, - formatProviderAuthProfileApiKeyWithPlugin, - prepareProviderExtraParams, - resolveProviderCacheTtlEligibility, - resolveProviderBinaryThinking, - resolveProviderBuiltInModelSuppression, - resolveProviderDefaultThinkingLevel, - resolveProviderModernModelRef, - resolveProviderUsageSnapshotWithPlugin, - resolveProviderCapabilitiesWithPlugin, - resolveProviderUsageAuthWithPlugin, - resolveProviderXHighThinking, - normalizeProviderResolvedModelWithPlugin, - prepareProviderDynamicModel, - prepareProviderRuntimeAuth, - resetProviderRuntimeHookCacheForTest, - refreshProviderOAuthCredentialWithPlugin, - resolveProviderRuntimePlugin, - runProviderDynamicModel, - wrapProviderStreamFn, -} from "./provider-runtime.js"; +let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins; +let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js").buildProviderAuthDoctorHintWithPlugin; +let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin; +let formatProviderAuthProfileApiKeyWithPlugin: typeof import("./provider-runtime.js").formatProviderAuthProfileApiKeyWithPlugin; +let prepareProviderExtraParams: typeof import("./provider-runtime.js").prepareProviderExtraParams; +let resolveProviderCacheTtlEligibility: typeof import("./provider-runtime.js").resolveProviderCacheTtlEligibility; +let resolveProviderBinaryThinking: typeof import("./provider-runtime.js").resolveProviderBinaryThinking; +let resolveProviderBuiltInModelSuppression: typeof import("./provider-runtime.js").resolveProviderBuiltInModelSuppression; +let resolveProviderDefaultThinkingLevel: typeof import("./provider-runtime.js").resolveProviderDefaultThinkingLevel; +let resolveProviderModernModelRef: typeof import("./provider-runtime.js").resolveProviderModernModelRef; +let resolveProviderUsageSnapshotWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageSnapshotWithPlugin; +let resolveProviderCapabilitiesWithPlugin: typeof import("./provider-runtime.js").resolveProviderCapabilitiesWithPlugin; +let resolveProviderUsageAuthWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageAuthWithPlugin; +let resolveProviderXHighThinking: typeof import("./provider-runtime.js").resolveProviderXHighThinking; +let normalizeProviderResolvedModelWithPlugin: typeof import("./provider-runtime.js").normalizeProviderResolvedModelWithPlugin; +let prepareProviderDynamicModel: typeof import("./provider-runtime.js").prepareProviderDynamicModel; +let prepareProviderRuntimeAuth: typeof import("./provider-runtime.js").prepareProviderRuntimeAuth; +let resetProviderRuntimeHookCacheForTest: typeof import("./provider-runtime.js").resetProviderRuntimeHookCacheForTest; +let refreshProviderOAuthCredentialWithPlugin: typeof import("./provider-runtime.js").refreshProviderOAuthCredentialWithPlugin; +let resolveProviderRuntimePlugin: typeof import("./provider-runtime.js").resolveProviderRuntimePlugin; +let runProviderDynamicModel: typeof import("./provider-runtime.js").runProviderDynamicModel; +let wrapProviderStreamFn: typeof import("./provider-runtime.js").wrapProviderStreamFn; const MODEL: ProviderRuntimeModel = { id: "demo-model", @@ -62,7 +60,32 @@ const MODEL: ProviderRuntimeModel = { }; describe("provider-runtime", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ + augmentModelCatalogWithProviderPlugins, + buildProviderAuthDoctorHintWithPlugin, + buildProviderMissingAuthMessageWithPlugin, + formatProviderAuthProfileApiKeyWithPlugin, + prepareProviderExtraParams, + resolveProviderCacheTtlEligibility, + resolveProviderBinaryThinking, + resolveProviderBuiltInModelSuppression, + resolveProviderDefaultThinkingLevel, + resolveProviderModernModelRef, + resolveProviderUsageSnapshotWithPlugin, + resolveProviderCapabilitiesWithPlugin, + resolveProviderUsageAuthWithPlugin, + resolveProviderXHighThinking, + normalizeProviderResolvedModelWithPlugin, + prepareProviderDynamicModel, + prepareProviderRuntimeAuth, + resetProviderRuntimeHookCacheForTest, + refreshProviderOAuthCredentialWithPlugin, + resolveProviderRuntimePlugin, + runProviderDynamicModel, + wrapProviderStreamFn, + } = await import("./provider-runtime.js")); resetProviderRuntimeHookCacheForTest(); resolvePluginProvidersMock.mockReset(); resolvePluginProvidersMock.mockReturnValue([]); diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index bfc976a7abf..b8da58c1921 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js"; const loadOpenClawPluginsMock = vi.fn(); const loadPluginManifestRegistryMock = vi.fn(); @@ -12,8 +11,12 @@ vi.mock("./manifest-registry.js", () => ({ loadPluginManifestRegistry: (...args: unknown[]) => loadPluginManifestRegistryMock(...args), })); +let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider; +let resolvePluginProviders: typeof import("./providers.js").resolvePluginProviders; + describe("resolvePluginProviders", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); loadOpenClawPluginsMock.mockReset(); loadOpenClawPluginsMock.mockReturnValue({ providers: [{ pluginId: "google", provider: { id: "demo-provider" } }], @@ -29,6 +32,8 @@ describe("resolvePluginProviders", () => { ], diagnostics: [], }); + ({ resolveOwningPluginIdsForProvider, resolvePluginProviders } = + await import("./providers.js")); }); it("forwards an explicit env to plugin loading", () => { diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 80c41858733..c18f5008c31 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolvePluginTools } from "./tools.js"; type MockRegistryToolEntry = { pluginId: string; @@ -14,6 +13,8 @@ vi.mock("./loader.js", () => ({ loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params), })); +let resolvePluginTools: typeof import("./tools.js").resolvePluginTools; + function makeTool(name: string) { return { name, @@ -90,8 +91,10 @@ function resolveOptionalDemoTools(toolAllowlist?: string[]) { } describe("resolvePluginTools optional tools", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); loadOpenClawPluginsMock.mockClear(); + ({ resolvePluginTools } = await import("./tools.js")); }); it("skips optional tools without explicit allowlist", () => { diff --git a/src/plugins/wired-hooks-compaction.test.ts b/src/plugins/wired-hooks-compaction.test.ts index 694f4a1f4b4..f8ce4d0a668 100644 --- a/src/plugins/wired-hooks-compaction.test.ts +++ b/src/plugins/wired-hooks-compaction.test.ts @@ -1,9 +1,8 @@ /** * Test: before_compaction & after_compaction hook wiring */ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { makeZeroUsageSnapshot } from "../agents/usage.js"; -import { emitAgentEvent } from "../infra/agent-events.js"; const hookMocks = vi.hoisted(() => ({ runner: { @@ -11,13 +10,6 @@ const hookMocks = vi.hoisted(() => ({ runBeforeCompaction: vi.fn(async () => {}), runAfterCompaction: vi.fn(async () => {}), }, -})); - -vi.mock("../plugins/hook-runner-global.js", () => ({ - getGlobalHookRunner: () => hookMocks.runner, -})); - -vi.mock("../infra/agent-events.js", () => ({ emitAgentEvent: vi.fn(), })); @@ -25,19 +17,23 @@ describe("compaction hook wiring", () => { let handleAutoCompactionStart: typeof import("../agents/pi-embedded-subscribe.handlers.compaction.js").handleAutoCompactionStart; let handleAutoCompactionEnd: typeof import("../agents/pi-embedded-subscribe.handlers.compaction.js").handleAutoCompactionEnd; - beforeAll(async () => { - ({ handleAutoCompactionStart, handleAutoCompactionEnd } = - await import("../agents/pi-embedded-subscribe.handlers.compaction.js")); - }); - - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); hookMocks.runner.hasHooks.mockClear(); hookMocks.runner.hasHooks.mockReturnValue(false); hookMocks.runner.runBeforeCompaction.mockClear(); hookMocks.runner.runBeforeCompaction.mockResolvedValue(undefined); hookMocks.runner.runAfterCompaction.mockClear(); hookMocks.runner.runAfterCompaction.mockResolvedValue(undefined); - vi.mocked(emitAgentEvent).mockClear(); + hookMocks.emitAgentEvent.mockClear(); + vi.doMock("../plugins/hook-runner-global.js", () => ({ + getGlobalHookRunner: () => hookMocks.runner, + })); + vi.doMock("../infra/agent-events.js", () => ({ + emitAgentEvent: hookMocks.emitAgentEvent, + })); + ({ handleAutoCompactionStart, handleAutoCompactionEnd } = + await import("../agents/pi-embedded-subscribe.handlers.compaction.js")); }); function createCompactionEndCtx(params: { @@ -94,7 +90,7 @@ describe("compaction hook wiring", () => { const hookCtx = beforeCalls[0]?.[1] as { sessionKey?: string } | undefined; expect(hookCtx?.sessionKey).toBe("agent:main:web-abc123"); expect(ctx.ensureCompactionPromise).toHaveBeenCalledTimes(1); - expect(emitAgentEvent).toHaveBeenCalledWith({ + expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({ runId: "r1", stream: "compaction", data: { phase: "start" }, @@ -135,7 +131,7 @@ describe("compaction hook wiring", () => { expect(event?.compactedCount).toBe(1); expect(ctx.incrementCompactionCount).toHaveBeenCalledTimes(1); expect(ctx.maybeResolveCompactionWait).toHaveBeenCalledTimes(1); - expect(emitAgentEvent).toHaveBeenCalledWith({ + expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({ runId: "r2", stream: "compaction", data: { phase: "end", willRetry: false, completed: true }, @@ -166,7 +162,7 @@ describe("compaction hook wiring", () => { expect(ctx.noteCompactionRetry).toHaveBeenCalledTimes(1); expect(ctx.resetForCompactionRetry).toHaveBeenCalledTimes(1); expect(ctx.maybeResolveCompactionWait).not.toHaveBeenCalled(); - expect(emitAgentEvent).toHaveBeenCalledWith({ + expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({ runId: "r3", stream: "compaction", data: { phase: "end", willRetry: true, completed: true }, diff --git a/src/process/command-queue.test.ts b/src/process/command-queue.test.ts index b6e6f17cd85..a35512d4f0d 100644 --- a/src/process/command-queue.test.ts +++ b/src/process/command-queue.test.ts @@ -17,19 +17,19 @@ vi.mock("../logging/diagnostic.js", () => ({ diagnosticLogger: diagnosticMocks.diag, })); -import { - clearCommandLane, - CommandLaneClearedError, - enqueueCommand, - enqueueCommandInLane, - GatewayDrainingError, - getActiveTaskCount, - getQueueSize, - markGatewayDraining, - resetAllLanes, - setCommandLaneConcurrency, - waitForActiveTasks, -} from "./command-queue.js"; +type CommandQueueModule = typeof import("./command-queue.js"); + +let clearCommandLane: CommandQueueModule["clearCommandLane"]; +let CommandLaneClearedError: CommandQueueModule["CommandLaneClearedError"]; +let enqueueCommand: CommandQueueModule["enqueueCommand"]; +let enqueueCommandInLane: CommandQueueModule["enqueueCommandInLane"]; +let GatewayDrainingError: CommandQueueModule["GatewayDrainingError"]; +let getActiveTaskCount: CommandQueueModule["getActiveTaskCount"]; +let getQueueSize: CommandQueueModule["getQueueSize"]; +let markGatewayDraining: CommandQueueModule["markGatewayDraining"]; +let resetAllLanes: CommandQueueModule["resetAllLanes"]; +let setCommandLaneConcurrency: CommandQueueModule["setCommandLaneConcurrency"]; +let waitForActiveTasks: CommandQueueModule["waitForActiveTasks"]; function createDeferred(): { promise: Promise; resolve: () => void } { let resolve!: () => void; @@ -54,7 +54,21 @@ function enqueueBlockedMainTask( } describe("command queue", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ + clearCommandLane, + CommandLaneClearedError, + enqueueCommand, + enqueueCommandInLane, + GatewayDrainingError, + getActiveTaskCount, + getQueueSize, + markGatewayDraining, + resetAllLanes, + setCommandLaneConcurrency, + waitForActiveTasks, + } = await import("./command-queue.js")); resetAllLanes(); diagnosticMocks.logLaneEnqueue.mockClear(); diagnosticMocks.logLaneDequeue.mockClear(); diff --git a/src/process/exec.no-output-timer.test.ts b/src/process/exec.no-output-timer.test.ts index 9c851f1e1a2..dfd7348877a 100644 --- a/src/process/exec.no-output-timer.test.ts +++ b/src/process/exec.no-output-timer.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -12,7 +12,9 @@ vi.mock("node:child_process", async () => { }; }); -import { runCommandWithTimeout } from "./exec.js"; +type ExecModule = typeof import("./exec.js"); + +let runCommandWithTimeout: ExecModule["runCommandWithTimeout"]; function createFakeSpawnedChild() { const child = new EventEmitter() as EventEmitter & ChildProcess; @@ -39,6 +41,11 @@ function createFakeSpawnedChild() { } describe("runCommandWithTimeout no-output timer", () => { + beforeEach(async () => { + vi.resetModules(); + ({ runCommandWithTimeout } = await import("./exec.js")); + }); + afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index 85600755dac..b2357858565 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "node:events"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); const execFileMock = vi.hoisted(() => vi.fn()); @@ -13,7 +13,8 @@ vi.mock("node:child_process", async (importOriginal) => { }; }); -import { runCommandWithTimeout, runExec } from "./exec.js"; +let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout; +let runExec: typeof import("./exec.js").runExec; type MockChild = EventEmitter & { stdout: EventEmitter; @@ -64,6 +65,11 @@ function expectCmdWrappedInvocation(params: { } describe("windows command wrapper behavior", () => { + beforeEach(async () => { + vi.resetModules(); + ({ runCommandWithTimeout, runExec } = await import("./exec.js")); + }); + afterEach(() => { spawnMock.mockReset(); execFileMock.mockReset(); diff --git a/src/process/kill-tree.test.ts b/src/process/kill-tree.test.ts index a506442aed4..7260938b438 100644 --- a/src/process/kill-tree.test.ts +++ b/src/process/kill-tree.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { killProcessTree } from "./kill-tree.js"; const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), @@ -9,6 +8,8 @@ vi.mock("node:child_process", () => ({ spawn: (...args: unknown[]) => spawnMock(...args), })); +let killProcessTree: typeof import("./kill-tree.js").killProcessTree; + async function withPlatform(platform: NodeJS.Platform, run: () => Promise | T): Promise { const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: platform, configurable: true }); @@ -24,7 +25,9 @@ async function withPlatform(platform: NodeJS.Platform, run: () => Promise describe("killProcessTree", () => { let killSpy: ReturnType; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + ({ killProcessTree } = await import("./kill-tree.js")); spawnMock.mockClear(); killSpy = vi.spyOn(process, "kill"); vi.useFakeTimers(); diff --git a/src/process/supervisor/adapters/child.test.ts b/src/process/supervisor/adapters/child.test.ts index 8494a701c7e..2d3040f8811 100644 --- a/src/process/supervisor/adapters/child.test.ts +++ b/src/process/supervisor/adapters/child.test.ts @@ -1,7 +1,7 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const { spawnWithFallbackMock, killProcessTreeMock } = vi.hoisted(() => ({ spawnWithFallbackMock: vi.fn(), @@ -51,11 +51,9 @@ async function createAdapterHarness(params?: { describe("createChildAdapter", () => { const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER; - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ createChildAdapter } = await import("./child.js")); - }); - - beforeEach(() => { spawnWithFallbackMock.mockClear(); killProcessTreeMock.mockClear(); delete process.env.OPENCLAW_SERVICE_MARKER; diff --git a/src/process/supervisor/adapters/pty.test.ts b/src/process/supervisor/adapters/pty.test.ts index 32ca418b533..83e650c073a 100644 --- a/src/process/supervisor/adapters/pty.test.ts +++ b/src/process/supervisor/adapters/pty.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { spawnMock, ptyKillMock, killProcessTreeMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), @@ -39,11 +39,9 @@ function expectSpawnEnv() { describe("createPtyAdapter", () => { let createPtyAdapter: typeof import("./pty.js").createPtyAdapter; - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ createPtyAdapter } = await import("./pty.js")); - }); - - beforeEach(() => { spawnMock.mockClear(); ptyKillMock.mockClear(); killProcessTreeMock.mockClear(); diff --git a/src/process/supervisor/supervisor.pty-command.test.ts b/src/process/supervisor/supervisor.pty-command.test.ts index daee348944d..eb3427d462f 100644 --- a/src/process/supervisor/supervisor.pty-command.test.ts +++ b/src/process/supervisor/supervisor.pty-command.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const { createPtyAdapterMock } = vi.hoisted(() => ({ createPtyAdapterMock: vi.fn(), @@ -35,11 +35,9 @@ function createStubPtyAdapter() { describe("process supervisor PTY command contract", () => { let createProcessSupervisor: typeof import("./supervisor.js").createProcessSupervisor; - beforeAll(async () => { + beforeEach(async () => { + vi.resetModules(); ({ createProcessSupervisor } = await import("./supervisor.js")); - }); - - beforeEach(() => { createPtyAdapterMock.mockClear(); }); diff --git a/src/security/windows-acl.test.ts b/src/security/windows-acl.test.ts index f9cb67fa4e5..6f073e34a10 100644 --- a/src/security/windows-acl.test.ts +++ b/src/security/windows-acl.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js"; const MOCK_USERNAME = "MockUser"; @@ -8,15 +8,26 @@ vi.mock("node:os", () => ({ userInfo: () => ({ username: MOCK_USERNAME }), })); -const { - createIcaclsResetCommand, - formatIcaclsResetCommand, - formatWindowsAclSummary, - inspectWindowsAcl, - parseIcaclsOutput, - resolveWindowsUserPrincipal, - summarizeWindowsAcl, -} = await import("./windows-acl.js"); +let createIcaclsResetCommand: typeof import("./windows-acl.js").createIcaclsResetCommand; +let formatIcaclsResetCommand: typeof import("./windows-acl.js").formatIcaclsResetCommand; +let formatWindowsAclSummary: typeof import("./windows-acl.js").formatWindowsAclSummary; +let inspectWindowsAcl: typeof import("./windows-acl.js").inspectWindowsAcl; +let parseIcaclsOutput: typeof import("./windows-acl.js").parseIcaclsOutput; +let resolveWindowsUserPrincipal: typeof import("./windows-acl.js").resolveWindowsUserPrincipal; +let summarizeWindowsAcl: typeof import("./windows-acl.js").summarizeWindowsAcl; + +beforeEach(async () => { + vi.resetModules(); + ({ + createIcaclsResetCommand, + formatIcaclsResetCommand, + formatWindowsAclSummary, + inspectWindowsAcl, + parseIcaclsOutput, + resolveWindowsUserPrincipal, + summarizeWindowsAcl, + } = await import("./windows-acl.js")); +}); function aclEntry(params: { principal: string; diff --git a/src/tts/edge-tts-validation.test.ts b/src/tts/edge-tts-validation.test.ts index 08697a2c9bd..51e4dbce39f 100644 --- a/src/tts/edge-tts-validation.test.ts +++ b/src/tts/edge-tts-validation.test.ts @@ -1,7 +1,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; let mockTtsPromise = vi.fn<(text: string, filePath: string) => Promise>(); @@ -13,7 +13,9 @@ vi.mock("node-edge-tts", () => ({ }, })); -const { edgeTTS } = await import("./tts-core.js"); +type TtsCoreModule = typeof import("./tts-core.js"); + +let edgeTTS: TtsCoreModule["edgeTTS"]; const baseEdgeConfig = { enabled: true, @@ -27,6 +29,11 @@ const baseEdgeConfig = { describe("edgeTTS – empty audio validation", () => { let tempDir: string; + beforeEach(async () => { + vi.resetModules(); + ({ edgeTTS } = await import("./tts-core.js")); + }); + afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); diff --git a/src/tts/tts.test.ts b/src/tts/tts.test.ts index 16b91b6f330..05e902ef20c 100644 --- a/src/tts/tts.test.ts +++ b/src/tts/tts.test.ts @@ -362,20 +362,43 @@ describe("tts", () => { }); describe("summarizeText", () => { + let summarizeTextForTest: typeof summarizeText; + let resolveTtsConfigForTest: typeof resolveTtsConfig; + let completeSimpleForTest: typeof completeSimple; + let getApiKeyForModelForTest: typeof getApiKeyForModel; + let resolveModelAsyncForTest: typeof resolveModelAsync; + let ensureCustomApiRegisteredForTest: typeof ensureCustomApiRegistered; + const baseCfg: OpenClawConfig = { agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } }, messages: { tts: {} }, }; - const baseConfig = resolveTtsConfig(baseCfg); + + beforeEach(async () => { + vi.resetModules(); + ({ completeSimple: completeSimpleForTest } = await import("@mariozechner/pi-ai")); + ({ getApiKeyForModel: getApiKeyForModelForTest } = await import("../agents/model-auth.js")); + ({ resolveModelAsync: resolveModelAsyncForTest } = + await import("../agents/pi-embedded-runner/model.js")); + ({ ensureCustomApiRegistered: ensureCustomApiRegisteredForTest } = + await import("../agents/custom-api-registry.js")); + const ttsModule = await import("./tts.js"); + summarizeTextForTest = ttsModule._test.summarizeText; + resolveTtsConfigForTest = ttsModule.resolveTtsConfig; + vi.mocked(completeSimpleForTest).mockResolvedValue( + mockAssistantMessage([{ type: "text", text: "Summary" }]), + ); + }); it("summarizes text and returns result with metrics", async () => { const mockSummary = "This is a summarized version of the text."; - vi.mocked(completeSimple).mockResolvedValue( + const baseConfig = resolveTtsConfigForTest(baseCfg); + vi.mocked(completeSimpleForTest).mockResolvedValue( mockAssistantMessage([{ type: "text", text: mockSummary }]), ); const longText = "A".repeat(2000); - const result = await summarizeText({ + const result = await summarizeTextForTest({ text: longText, targetLength: 1500, cfg: baseCfg, @@ -387,11 +410,12 @@ describe("tts", () => { expect(result.inputLength).toBe(2000); expect(result.outputLength).toBe(mockSummary.length); expect(result.latencyMs).toBeGreaterThanOrEqual(0); - expect(completeSimple).toHaveBeenCalledTimes(1); + expect(completeSimpleForTest).toHaveBeenCalledTimes(1); }); it("calls the summary model with the expected parameters", async () => { - await summarizeText({ + const baseConfig = resolveTtsConfigForTest(baseCfg); + await summarizeTextForTest({ text: "Long text to summarize", targetLength: 500, cfg: baseCfg, @@ -399,11 +423,11 @@ describe("tts", () => { timeoutMs: 30_000, }); - const callArgs = vi.mocked(completeSimple).mock.calls[0]; + const callArgs = vi.mocked(completeSimpleForTest).mock.calls[0]; expect(callArgs?.[1]?.messages?.[0]?.role).toBe("user"); expect(callArgs?.[2]?.maxTokens).toBe(250); expect(callArgs?.[2]?.temperature).toBe(0.3); - expect(getApiKeyForModel).toHaveBeenCalledTimes(1); + expect(getApiKeyForModelForTest).toHaveBeenCalledTimes(1); }); it("uses summaryModel override when configured", async () => { @@ -411,8 +435,8 @@ describe("tts", () => { agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } }, messages: { tts: { summaryModel: "openai/gpt-4.1-mini" } }, }; - const config = resolveTtsConfig(cfg); - await summarizeText({ + const config = resolveTtsConfigForTest(cfg); + await summarizeTextForTest({ text: "Long text to summarize", targetLength: 500, cfg, @@ -420,11 +444,17 @@ describe("tts", () => { timeoutMs: 30_000, }); - expect(resolveModelAsync).toHaveBeenCalledWith("openai", "gpt-4.1-mini", undefined, cfg); + expect(resolveModelAsyncForTest).toHaveBeenCalledWith( + "openai", + "gpt-4.1-mini", + undefined, + cfg, + ); }); it("registers the Ollama api before direct summarization", async () => { - vi.mocked(resolveModelAsync).mockResolvedValue({ + const baseConfig = resolveTtsConfigForTest(baseCfg); + vi.mocked(resolveModelAsyncForTest).mockResolvedValue({ ...createResolvedModel("ollama", "qwen3:8b", "ollama"), model: { ...createResolvedModel("ollama", "qwen3:8b", "ollama").model, @@ -432,7 +462,7 @@ describe("tts", () => { }, } as never); - await summarizeText({ + await summarizeTextForTest({ text: "Long text to summarize", targetLength: 500, cfg: baseCfg, @@ -440,10 +470,11 @@ describe("tts", () => { timeoutMs: 30_000, }); - expect(ensureCustomApiRegistered).toHaveBeenCalledWith("ollama", expect.any(Function)); + expect(ensureCustomApiRegisteredForTest).toHaveBeenCalledWith("ollama", expect.any(Function)); }); it("validates targetLength bounds", async () => { + const baseConfig = resolveTtsConfigForTest(baseCfg); const cases = [ { targetLength: 99, shouldThrow: true }, { targetLength: 100, shouldThrow: false }, @@ -451,7 +482,7 @@ describe("tts", () => { { targetLength: 10001, shouldThrow: true }, ] as const; for (const testCase of cases) { - const call = summarizeText({ + const call = summarizeTextForTest({ text: "text", targetLength: testCase.targetLength, cfg: baseCfg, @@ -469,6 +500,7 @@ describe("tts", () => { }); it("throws when summary output is missing or empty", async () => { + const baseConfig = resolveTtsConfigForTest(baseCfg); const cases = [ { name: "no summary blocks", message: mockAssistantMessage([]) }, { @@ -477,9 +509,9 @@ describe("tts", () => { }, ] as const; for (const testCase of cases) { - vi.mocked(completeSimple).mockResolvedValue(testCase.message); + vi.mocked(completeSimpleForTest).mockResolvedValue(testCase.message); await expect( - summarizeText({ + summarizeTextForTest({ text: "text", targetLength: 500, cfg: baseCfg, diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts index ed580960ad4..f80633e450d 100644 --- a/src/utils/message-channel.ts +++ b/src/utils/message-channel.ts @@ -12,10 +12,23 @@ import { normalizeGatewayClientMode, normalizeGatewayClientName, } from "../gateway/protocol/client-info.js"; -import { getActivePluginRegistry } from "../plugins/runtime.js"; export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const; export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL; +const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); + +type PluginRegistryStateLike = { + registry?: { + channels?: Array<{ + plugin: { + id: string; + meta: { + aliases?: string[]; + }; + }; + }>; + } | null; +}; const MARKDOWN_CAPABLE_CHANNELS = new Set([ "slack", @@ -64,8 +77,13 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined if (builtIn) { return builtIn; } - const registry = getActivePluginRegistry(); - const pluginMatch = registry?.channels.find((entry) => { + const channels = + ( + globalThis as typeof globalThis & { + [REGISTRY_STATE]?: PluginRegistryStateLike; + } + )[REGISTRY_STATE]?.registry?.channels ?? []; + const pluginMatch = channels.find((entry) => { if (entry.plugin.id.toLowerCase() === normalized) { return true; } @@ -77,19 +95,23 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined } const listPluginChannelIds = (): string[] => { - const registry = getActivePluginRegistry(); - if (!registry) { - return []; - } - return registry.channels.map((entry) => entry.plugin.id); + const channels = + ( + globalThis as typeof globalThis & { + [REGISTRY_STATE]?: PluginRegistryStateLike; + } + )[REGISTRY_STATE]?.registry?.channels ?? []; + return channels.map((entry) => entry.plugin.id); }; const listPluginChannelAliases = (): string[] => { - const registry = getActivePluginRegistry(); - if (!registry) { - return []; - } - return registry.channels.flatMap((entry) => entry.plugin.meta.aliases ?? []); + const channels = + ( + globalThis as typeof globalThis & { + [REGISTRY_STATE]?: PluginRegistryStateLike; + } + )[REGISTRY_STATE]?.registry?.channels ?? []; + return channels.flatMap((entry) => entry.plugin.meta.aliases ?? []); }; export const listDeliverableMessageChannels = (): ChannelId[] => diff --git a/src/whatsapp/resolve-outbound-target.test.ts b/src/whatsapp/resolve-outbound-target.test.ts index 5c4495053b2..4d7d16b4393 100644 --- a/src/whatsapp/resolve-outbound-target.test.ts +++ b/src/whatsapp/resolve-outbound-target.test.ts @@ -1,12 +1,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import * as normalize from "./normalize.js"; -import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js"; vi.mock("./normalize.js"); vi.mock("../infra/outbound/target-errors.js", () => ({ missingTargetError: (platform: string, format: string) => new Error(`${platform}: ${format}`), })); +let resolveWhatsAppOutboundTarget: typeof import("./resolve-outbound-target.js").resolveWhatsAppOutboundTarget; + type ResolveParams = Parameters[0]; const PRIMARY_TARGET = "+11234567890"; const SECONDARY_TARGET = "+19876543210"; @@ -62,8 +63,10 @@ function expectDeniedForTarget(params: { } describe("resolveWhatsAppOutboundTarget", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.resetAllMocks(); + ({ resolveWhatsAppOutboundTarget } = await import("./resolve-outbound-target.js")); }); describe("empty/missing to parameter", () => {