diff --git a/extensions/browser/src/browser/chrome.default-browser.test.ts b/extensions/browser/src/browser/chrome.default-browser.test.ts index a02f26986ef..33d92f7f124 100644 --- a/extensions/browser/src/browser/chrome.default-browser.test.ts +++ b/extensions/browser/src/browser/chrome.default-browser.test.ts @@ -1,15 +1,24 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("node:child_process", () => ({ - execFileSync: vi.fn(), -})); -vi.mock("node:fs", () => { +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFileSync: vi.fn(), + }; +}); +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); const existsSync = vi.fn(); const readFileSync = vi.fn(); const module = { existsSync, readFileSync }; return { + ...actual, ...module, - default: module, + default: { + ...actual, + ...module, + }, }; }); vi.mock("node:os", () => { diff --git a/extensions/voice-call/src/webhook/tailscale.test.ts b/extensions/voice-call/src/webhook/tailscale.test.ts index d4f396461d5..b1728226bde 100644 --- a/extensions/voice-call/src/webhook/tailscale.test.ts +++ b/extensions/voice-call/src/webhook/tailscale.test.ts @@ -5,9 +5,13 @@ const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), })); -vi.mock("node:child_process", () => ({ - spawn: spawnMock, -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: spawnMock, + }; +}); import { cleanupTailscaleExposure, diff --git a/src/auto-reply/reply/commands-core.test.ts b/src/auto-reply/reply/commands-core.test.ts index f08d03eb823..9c32bced113 100644 --- a/src/auto-reply/reply/commands-core.test.ts +++ b/src/auto-reply/reply/commands-core.test.ts @@ -12,12 +12,19 @@ const hookRunnerMocks = vi.hoisted(() => ({ runBeforeReset: vi.fn(), })); -vi.mock("node:fs/promises", () => ({ - default: { +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + readFile: fsMocks.readFile, + readdir: fsMocks.readdir, + }, readFile: fsMocks.readFile, readdir: fsMocks.readdir, - }, -})); + }; +}); vi.mock("../../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: () => diff --git a/src/cli/update-cli/restart-helper.test.ts b/src/cli/update-cli/restart-helper.test.ts index 847893e9f23..42e7c8b4e63 100644 --- a/src/cli/update-cli/restart-helper.test.ts +++ b/src/cli/update-cli/restart-helper.test.ts @@ -3,9 +3,13 @@ import fs from "node:fs/promises"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { prepareRestartScript, runRestartScript } from "./restart-helper.js"; -vi.mock("node:child_process", () => ({ - spawn: vi.fn(), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: vi.fn(), + }; +}); describe("restart-helper", () => { const originalPlatform = process.platform; diff --git a/src/daemon/launchd-restart-handoff.test.ts b/src/daemon/launchd-restart-handoff.test.ts index d685e64d851..1801ecabab2 100644 --- a/src/daemon/launchd-restart-handoff.test.ts +++ b/src/daemon/launchd-restart-handoff.test.ts @@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); const unrefMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - spawn: (...args: unknown[]) => spawnMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: unknown[]) => spawnMock(...args), + }; +}); import { scheduleDetachedLaunchdRestartHandoff } from "./launchd-restart-handoff.js"; diff --git a/src/daemon/program-args.test.ts b/src/daemon/program-args.test.ts index 920f4533297..3823f589be1 100644 --- a/src/daemon/program-args.test.ts +++ b/src/daemon/program-args.test.ts @@ -10,15 +10,27 @@ const fsMocks = vi.hoisted(() => ({ realpath: vi.fn(), })); -vi.mock("node:fs/promises", () => ({ - default: { access: fsMocks.access, realpath: fsMocks.realpath }, - access: fsMocks.access, - realpath: fsMocks.realpath, -})); +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + access: fsMocks.access, + realpath: fsMocks.realpath, + }, + access: fsMocks.access, + realpath: fsMocks.realpath, + }; +}); -vi.mock("node:child_process", () => ({ - execFileSync: childProcessMocks.execFileSync, -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFileSync: childProcessMocks.execFileSync, + }; +}); import { resolveGatewayProgramArguments } from "./program-args.js"; diff --git a/src/daemon/runtime-paths.test.ts b/src/daemon/runtime-paths.test.ts index 6acc816b737..57116799e85 100644 --- a/src/daemon/runtime-paths.test.ts +++ b/src/daemon/runtime-paths.test.ts @@ -4,10 +4,17 @@ const fsMocks = vi.hoisted(() => ({ access: vi.fn(), })); -vi.mock("node:fs/promises", () => ({ - default: { access: fsMocks.access }, - access: fsMocks.access, -})); +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + access: fsMocks.access, + }, + access: fsMocks.access, + }; +}); import { renderSystemNodeWarning, diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index dafa7b7e32e..1382e995060 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -4,9 +4,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const execFileMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - execFile: execFileMock, -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFile: execFileMock, + }; +}); import { splitArgsPreservingQuotes } from "./arg-split.js"; import { parseSystemdExecStart } from "./systemd-unit.js"; diff --git a/src/gateway/server-methods/config.test.ts b/src/gateway/server-methods/config.test.ts index 1a399fd288d..e6ef3d52d96 100644 --- a/src/gateway/server-methods/config.test.ts +++ b/src/gateway/server-methods/config.test.ts @@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { configHandlers, resolveConfigOpenCommand } from "./config.js"; import type { GatewayRequestHandlerOptions } from "./types.js"; -vi.mock("node:child_process", () => ({ - execFile: vi.fn(), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFile: vi.fn(), + }; +}); function invokeExecFileCallback(args: unknown[], error: Error | null) { const callback = args.at(-1); diff --git a/src/infra/gateway-processes.test.ts b/src/infra/gateway-processes.test.ts index 5eb2fbd1113..67964d7661d 100644 --- a/src/infra/gateway-processes.test.ts +++ b/src/infra/gateway-processes.test.ts @@ -7,15 +7,25 @@ const parseProcCmdlineMock = vi.hoisted(() => vi.fn()); const isGatewayArgvMock = vi.hoisted(() => vi.fn()); const findGatewayPidsOnPortSyncMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - spawnSync: (...args: unknown[]) => spawnSyncMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawnSync: (...args: unknown[]) => spawnSyncMock(...args), + }; +}); -vi.mock("node:fs", () => ({ - default: { +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + readFileSync: (...args: unknown[]) => readFileSyncMock(...args), + }, readFileSync: (...args: unknown[]) => readFileSyncMock(...args), - }, -})); + }; +}); vi.mock("../daemon/cmd-argv.js", () => ({ parseCmdScriptCommandLine: (...args: unknown[]) => parseCmdScriptCommandLineMock(...args), @@ -30,6 +40,26 @@ vi.mock("./restart-stale-pids.js", () => ({ findGatewayPidsOnPortSync: (...args: unknown[]) => findGatewayPidsOnPortSyncMock(...args), })); +vi.mock("../logging/subsystem.js", () => ({ + createSubsystemLogger: vi.fn(() => ({ + warn: vi.fn(), + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + fatal: vi.fn(), + raw: vi.fn(), + child: vi.fn(), + isEnabled: vi.fn(() => false), + subsystem: "test", + })), +})); + +vi.mock("../channels/chat-meta.js", () => ({ + listChatChannels: vi.fn(() => []), + getChatChannelMeta: vi.fn(() => null), +})); + const { findVerifiedGatewayListenerPidsOnPortSync, formatGatewayPidList, diff --git a/src/infra/heartbeat-runner.returns-default-unset.test.ts b/src/infra/heartbeat-runner.returns-default-unset.test.ts index c4c3e972033..c2f25b20a1b 100644 --- a/src/infra/heartbeat-runner.returns-default-unset.test.ts +++ b/src/infra/heartbeat-runner.returns-default-unset.test.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js"; -import type { ChannelOutboundAdapter } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentIdFromSessionKey, @@ -11,9 +10,9 @@ import { resolveMainSessionKey, resolveStorePath, } from "../config/sessions.js"; +import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../plugin-sdk/whatsapp-targets.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { buildAgentPeerSessionKey } from "../routing/session-key.js"; -import { loadBundledPluginTestApiSync } from "../test-utils/bundled-plugin-public-surface.js"; import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; import { typedCases } from "../test-utils/typed-cases.js"; import { @@ -23,6 +22,7 @@ import { resolveHeartbeatPrompt, runHeartbeatOnce, } from "./heartbeat-runner.js"; +import { whatsappOutbound } from "./outbound/deliver.test-outbounds.js"; import { resolveHeartbeatDeliveryTarget, resolveHeartbeatSenderContext, @@ -35,15 +35,40 @@ let testRegistry: ReturnType | null = null; let fixtureRoot = ""; let fixtureCount = 0; -let whatsappOutboundCache: ChannelOutboundAdapter | undefined; -function getWhatsAppOutbound(): ChannelOutboundAdapter { - if (!whatsappOutboundCache) { - ({ whatsappOutbound: whatsappOutboundCache } = loadBundledPluginTestApiSync<{ - whatsappOutbound: ChannelOutboundAdapter; - }>("whatsapp")); +function resolveWhatsAppTargetForTest(params: { + to: string | null | undefined; + allowFrom: Array | null | undefined; +}) { + const trimmed = params.to?.trim() ?? ""; + const allowListRaw = (params.allowFrom ?? []) + .map((entry) => String(entry).trim()) + .filter(Boolean); + const hasWildcard = allowListRaw.includes("*"); + const allowList = allowListRaw + .filter((entry) => entry !== "*") + .map((entry) => normalizeWhatsAppTarget(entry)) + .filter((entry): entry is string => Boolean(entry)); + const normalizedTarget = normalizeWhatsAppTarget(trimmed); + + if (!normalizedTarget) { + return { + ok: false as const, + error: new Error('Missing target for WhatsApp; expected "".'), + }; } - return whatsappOutboundCache; + if (isWhatsAppGroupJid(normalizedTarget)) { + return { ok: true as const, to: normalizedTarget }; + } + if (hasWildcard || allowList.length === 0 || allowList.includes(normalizedTarget)) { + return { ok: true as const, to: normalizedTarget }; + } + return { + ok: false as const, + error: new Error( + `Target "${normalizedTarget}" is not listed in the configured WhatsApp allowFrom policy.`, + ), + }; } const createCaseDir = async (prefix: string) => { @@ -57,7 +82,14 @@ beforeAll(async () => { const whatsappPlugin = createOutboundTestPlugin({ id: "whatsapp", - outbound: getWhatsAppOutbound(), + outbound: { + ...whatsappOutbound, + resolveTarget: ({ to, allowFrom }) => + resolveWhatsAppTargetForTest({ + to, + allowFrom, + }), + }, }); whatsappPlugin.config = { ...whatsappPlugin.config, diff --git a/src/infra/machine-name.test.ts b/src/infra/machine-name.test.ts index 61e03bcb68f..742279d2ba2 100644 --- a/src/infra/machine-name.test.ts +++ b/src/infra/machine-name.test.ts @@ -4,9 +4,13 @@ import { importFreshModule } from "../../test/helpers/import-fresh.js"; const execFileMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - execFile: (...args: unknown[]) => execFileMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFile: (...args: unknown[]) => execFileMock(...args), + }; +}); const originalVitest = process.env.VITEST; const originalNodeEnv = process.env.NODE_ENV; diff --git a/src/infra/os-summary.test.ts b/src/infra/os-summary.test.ts index 62616550697..8698cb07e4f 100644 --- a/src/infra/os-summary.test.ts +++ b/src/infra/os-summary.test.ts @@ -3,9 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const spawnSyncMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - spawnSync: (...args: unknown[]) => spawnSyncMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawnSync: (...args: unknown[]) => spawnSyncMock(...args), + }; +}); import { resolveOsSummary } from "./os-summary.js"; diff --git a/src/infra/process-respawn.test.ts b/src/infra/process-respawn.test.ts index 804dca82c22..cf96fccf1d4 100644 --- a/src/infra/process-respawn.test.ts +++ b/src/infra/process-respawn.test.ts @@ -6,9 +6,13 @@ const spawnMock = vi.hoisted(() => vi.fn()); const triggerOpenClawRestartMock = vi.hoisted(() => vi.fn()); const scheduleDetachedLaunchdRestartHandoffMock = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - spawn: (...args: unknown[]) => spawnMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: unknown[]) => spawnMock(...args), + }; +}); vi.mock("./restart.js", () => ({ triggerOpenClawRestart: (...args: unknown[]) => triggerOpenClawRestartMock(...args), })); diff --git a/src/infra/restart-stale-pids.test.ts b/src/infra/restart-stale-pids.test.ts index 036298b7a1e..d81839ac6a8 100644 --- a/src/infra/restart-stale-pids.test.ts +++ b/src/infra/restart-stale-pids.test.ts @@ -10,10 +10,14 @@ const mockSpawnSync = vi.hoisted(() => vi.fn()); const mockResolveGatewayPort = vi.hoisted(() => vi.fn(() => 18789)); const mockRestartWarn = vi.hoisted(() => vi.fn()); -vi.mock("node:child_process", () => ({ - spawnSync: (...args: unknown[]) => mockSpawnSync(...args), - execFileSync: vi.fn(), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawnSync: (...args: unknown[]) => mockSpawnSync(...args), + execFileSync: vi.fn(), + }; +}); vi.mock("../config/paths.js", () => ({ resolveGatewayPort: () => mockResolveGatewayPort(), diff --git a/src/infra/ssh-config.test.ts b/src/infra/ssh-config.test.ts index cd722f51203..01d40fa85f6 100644 --- a/src/infra/ssh-config.test.ts +++ b/src/infra/ssh-config.test.ts @@ -16,7 +16,8 @@ function createMockSpawnChild() { return { child, stdout }; } -vi.mock("node:child_process", () => { +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); const spawn = vi.fn(() => { const { child, stdout } = createMockSpawnChild(); process.nextTick(() => { @@ -35,7 +36,10 @@ vi.mock("node:child_process", () => { }); return child; }); - return { spawn }; + return { + ...actual, + spawn, + }; }); const spawnMock = vi.mocked(spawn); diff --git a/src/infra/windows-task-restart.test.ts b/src/infra/windows-task-restart.test.ts index a800b3ebeee..ce6f8cd8da9 100644 --- a/src/infra/windows-task-restart.test.ts +++ b/src/infra/windows-task-restart.test.ts @@ -7,9 +7,13 @@ import { captureFullEnv } from "../test-utils/env.js"; const spawnMock = vi.hoisted(() => vi.fn()); const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir())); -vi.mock("node:child_process", () => ({ - spawn: (...args: unknown[]) => spawnMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: unknown[]) => spawnMock(...args), + }; +}); vi.mock("./tmp-openclaw-dir.js", () => ({ resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(), })); diff --git a/src/infra/wsl.test.ts b/src/infra/wsl.test.ts index 018863d02cd..1b6b6a92d2b 100644 --- a/src/infra/wsl.test.ts +++ b/src/infra/wsl.test.ts @@ -4,15 +4,25 @@ import { captureEnv } from "../test-utils/env.js"; const readFileSyncMock = vi.hoisted(() => vi.fn()); const readFileMock = vi.hoisted(() => vi.fn()); -vi.mock("node:fs", () => ({ - readFileSync: readFileSyncMock, -})); +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readFileSync: readFileSyncMock, + }; +}); -vi.mock("node:fs/promises", () => ({ - default: { +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + readFile: readFileMock, + }, readFile: readFileMock, - }, -})); + }; +}); let isWSLEnv: typeof import("./wsl.js").isWSLEnv; let isWSLSync: typeof import("./wsl.js").isWSLSync; diff --git a/src/plugin-sdk/browser-maintenance.test.ts b/src/plugin-sdk/browser-maintenance.test.ts index 731f9f989cd..630301bc429 100644 --- a/src/plugin-sdk/browser-maintenance.test.ts +++ b/src/plugin-sdk/browser-maintenance.test.ts @@ -14,17 +14,33 @@ vi.mock("./facade-runtime.js", () => ({ tryLoadActivatedBundledPluginPublicSurfaceModuleSync, })); -vi.mock("node:fs/promises", () => { - const mocked = { mkdir, access, rename }; - return { ...mocked, default: mocked }; +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + mkdir, + access, + rename, + }, + mkdir, + access, + rename, + }; }); -vi.mock("node:os", () => ({ - default: { +vi.mock("node:os", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + homedir: () => "/home/test", + }, homedir: () => "/home/test", - }, - homedir: () => "/home/test", -})); + }; +}); describe("browser maintenance", () => { beforeEach(() => { diff --git a/src/process/command-queue.test.ts b/src/process/command-queue.test.ts index 3dc017d068c..f8e508c7e56 100644 --- a/src/process/command-queue.test.ts +++ b/src/process/command-queue.test.ts @@ -294,10 +294,13 @@ describe("command queue", () => { const blocker2 = new Promise((r) => { resolve2 = r; }); + const firstStarted = createDeferred(); const first = enqueueCommandInLane(lane, async () => { + firstStarted.resolve(); await blocker1; }); + await firstStarted.promise; const drainPromise = waitForActiveTasks(2000); // Starts after waitForActiveTasks snapshot and should not block drain completion. diff --git a/src/process/kill-tree.test.ts b/src/process/kill-tree.test.ts index 5178abf0faa..9f1a359a0f5 100644 --- a/src/process/kill-tree.test.ts +++ b/src/process/kill-tree.test.ts @@ -4,9 +4,13 @@ const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), })); -vi.mock("node:child_process", () => ({ - spawn: (...args: unknown[]) => spawnMock(...args), -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: unknown[]) => spawnMock(...args), + }; +}); let killProcessTree: typeof import("./kill-tree.js").killProcessTree; diff --git a/test/scripts/test-report-utils.test.ts b/test/scripts/test-report-utils.test.ts index a42817750dd..1d4cbd2102d 100644 --- a/test/scripts/test-report-utils.test.ts +++ b/test/scripts/test-report-utils.test.ts @@ -12,9 +12,13 @@ const { spawnSyncMock } = vi.hoisted(() => ({ spawnSyncMock: vi.fn(), })); -vi.mock("node:child_process", () => ({ - spawnSync: spawnSyncMock, -})); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawnSync: spawnSyncMock, + }; +}); describe("scripts/test-report-utils normalizeTrackedRepoPath", () => { it("normalizes repo-local absolute paths to repo-relative slash paths", () => { diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index 5f512f659c6..d9e3506427a 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -1,4 +1,5 @@ import { afterAll, afterEach, beforeAll } from "vitest"; +import { createTopLevelChannelReplyToModeResolver } from "../src/channels/plugins/threading-helpers.js"; import type { ChannelId, ChannelOutboundAdapter, @@ -8,25 +9,24 @@ import type { OpenClawConfig } from "../src/config/config.js"; import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js"; import type { PluginRegistry } from "../src/plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../src/plugins/runtime.js"; +import { createTestRegistry } from "../src/test-utils/channel-plugins.js"; import { installSharedTestSetup } from "./setup.shared.js"; const testEnv = installSharedTestSetup(); const WORKER_RUNTIME_STATE = Symbol.for("openclaw.testSetupRuntimeState"); -const WORKER_RUNTIME_DEPS = Symbol.for("openclaw.testSetupRuntimeDeps"); +const WORKER_CLEANUP_DEPS = Symbol.for("openclaw.testSetupCleanupDeps"); type WorkerRuntimeState = { defaultPluginRegistry: PluginRegistry | null; materializedDefaultPluginRegistry: PluginRegistry | null; }; -type WorkerRuntimeDeps = { +type WorkerCleanupDeps = { resetContextWindowCacheForTest: typeof import("../src/agents/context.js").resetContextWindowCacheForTest; resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config.js").resetModelsJsonReadyCacheForTest; drainSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").drainSessionWriteLockStateForTest; resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").resetSessionWriteLockStateForTest; - createTopLevelChannelReplyToModeResolver: typeof import("../src/channels/plugins/threading-helpers.js").createTopLevelChannelReplyToModeResolver; - createTestRegistry: typeof import("../src/test-utils/channel-plugins.js").createTestRegistry; cleanupSessionStateForTest: typeof import("../src/test-utils/session-state-cleanup.js").cleanupSessionStateForTest; }; @@ -43,20 +43,16 @@ const workerRuntimeState = (() => { return globalState[WORKER_RUNTIME_STATE]; })(); -async function loadWorkerRuntimeDeps(): Promise { +async function loadWorkerCleanupDeps(): Promise { const [ { resetContextWindowCacheForTest }, { resetModelsJsonReadyCacheForTest }, { drainSessionWriteLockStateForTest, resetSessionWriteLockStateForTest }, - { createTopLevelChannelReplyToModeResolver }, - { createTestRegistry }, { cleanupSessionStateForTest }, ] = await Promise.all([ import("../src/agents/context.js"), import("../src/agents/models-config.js"), import("../src/agents/session-write-lock.js"), - import("../src/channels/plugins/threading-helpers.js"), - import("../src/test-utils/channel-plugins.js"), import("../src/test-utils/session-state-cleanup.js"), ]); @@ -65,29 +61,22 @@ async function loadWorkerRuntimeDeps(): Promise { resetModelsJsonReadyCacheForTest, drainSessionWriteLockStateForTest, resetSessionWriteLockStateForTest, - createTopLevelChannelReplyToModeResolver, - createTestRegistry, cleanupSessionStateForTest, }; } -const workerRuntimeDeps = await (() => { +function getWorkerCleanupDeps(): Promise { const globalState = globalThis as typeof globalThis & { - [WORKER_RUNTIME_DEPS]?: Promise; + [WORKER_CLEANUP_DEPS]?: Promise; }; - globalState[WORKER_RUNTIME_DEPS] ??= loadWorkerRuntimeDeps(); - return globalState[WORKER_RUNTIME_DEPS]; -})(); + globalState[WORKER_CLEANUP_DEPS] ??= loadWorkerCleanupDeps(); + return globalState[WORKER_CLEANUP_DEPS]; +} -const { - resetContextWindowCacheForTest, - resetModelsJsonReadyCacheForTest, - drainSessionWriteLockStateForTest, - resetSessionWriteLockStateForTest, - createTopLevelChannelReplyToModeResolver, - createTestRegistry, - cleanupSessionStateForTest, -} = workerRuntimeDeps; +// Preload cleanup/runtime helpers before per-file vi.mock hoists run in +// non-isolated workers, otherwise test-scoped module mocks can leak into the +// shared cleanup dependency graph. +void getWorkerCleanupDeps(); const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => { return deps?.[id] as ((...args: unknown[]) => Promise) | undefined; @@ -321,6 +310,12 @@ beforeAll(() => { }); afterEach(async () => { + const { + cleanupSessionStateForTest, + resetContextWindowCacheForTest, + resetModelsJsonReadyCacheForTest, + resetSessionWriteLockStateForTest, + } = await getWorkerCleanupDeps(); await cleanupSessionStateForTest(); resetContextWindowCacheForTest(); resetModelsJsonReadyCacheForTest(); @@ -329,6 +324,8 @@ afterEach(async () => { }); afterAll(async () => { + const { cleanupSessionStateForTest, drainSessionWriteLockStateForTest } = + await getWorkerCleanupDeps(); await cleanupSessionStateForTest(); await drainSessionWriteLockStateForTest(); testEnv.cleanup();