From 6b6ddcd2a695209b943098a7bf31e574ff32e718 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 31 Mar 2026 02:12:14 +0100 Subject: [PATCH] test: speed up core runtime suites --- src/acp/runtime/session-meta.test.ts | 10 +- .../bash-tools.exec-host-shared.test.ts | 27 ++- src/agents/bash-tools.exec-runtime.test.ts | 6 +- .../minimax-vlm.normalizes-api-key.test.ts | 12 +- .../models-config.providers.policy.test.ts | 16 +- src/agents/openclaw-tools.web-runtime.test.ts | 5 +- ...session.subscribeembeddedpisession.test.ts | 15 +- src/agents/provider-capabilities.test.ts | 34 ++- src/agents/runtime-plugins.test.ts | 12 +- src/agents/session-write-lock.test.ts | 39 ++-- .../simple-completion-transport.test.ts | 33 +-- src/agents/skills/plugin-skills.test.ts | 18 +- src/agents/skills/refresh.test.ts | 19 +- src/agents/tools/gateway.test.ts | 21 +- src/agents/tools/nodes-utils.test.ts | 10 +- src/agents/tools/sessions-list-tool.test.ts | 129 +++++------ src/agents/tools/sessions-resolution.test.ts | 13 +- src/agents/tools/tts-tool.test.ts | 10 +- src/agents/transcript-policy.test.ts | 15 +- src/auto-reply/reply/commands-core.test.ts | 2 +- .../reply/session-hooks-context.test.ts | 6 +- src/channels/session.test.ts | 8 +- src/config/config.web-search-provider.test.ts | 5 +- src/config/logging.test.ts | 9 +- src/config/runtime-schema.test.ts | 12 +- src/config/sessions/delivery-info.test.ts | 10 +- .../validation.channel-metadata.test.ts | 12 +- src/cron/service.issue-regressions.test.ts | 28 ++- .../runtime-hints.windows-paths.test.ts | 29 +-- src/daemon/schtasks-exec.test.ts | 3 +- src/daemon/systemd.test.ts | 5 +- src/entry.version-fast-path.test.ts | 4 - src/gateway/server-startup.test.ts | 17 +- .../provider-registry.test.ts | 13 +- src/image-generation/runtime.test.ts | 14 +- src/infra/env.test.ts | 1 + src/infra/exec-approval-forwarder.test.ts | 10 +- src/infra/exec-approval-surface.test.ts | 87 ++++---- src/infra/exec-approvals-store.test.ts | 8 +- src/infra/net/proxy-fetch.test.ts | 11 +- src/infra/net/ssrf.dispatcher.test.ts | 10 +- .../net/undici-global-dispatcher.test.ts | 9 +- src/infra/outbound/agent-delivery.test.ts | 8 +- src/infra/outbound/channel-selection.test.ts | 5 +- src/infra/outbound/identity.test.ts | 10 +- src/infra/outbound/message.channels.test.ts | 8 +- src/infra/outbound/message.test.ts | 82 ++++--- src/infra/outbound/outbound-policy.test.ts | 33 +-- src/infra/outbound/session-context.test.ts | 17 +- .../outbound/target-normalization.test.ts | 33 +-- src/infra/outbound/target-normalization.ts | 8 + src/infra/outbound/target-resolver.test.ts | 29 ++- .../targets.channel-resolution.test.ts | 111 ---------- src/infra/pairing-token.test.ts | 9 +- src/infra/plugin-approval-forwarder.test.ts | 48 ++-- src/infra/ports.test.ts | 13 +- src/infra/provider-usage.auth.plugin.test.ts | 10 +- src/infra/provider-usage.load.plugin.test.ts | 10 +- src/infra/restart-stale-pids.test.ts | 11 +- src/infra/restart.test.ts | 25 +-- src/infra/secure-random.test.ts | 10 +- src/infra/transport-ready.test.ts | 72 +++--- src/infra/windows-task-restart.test.ts | 11 +- src/infra/wsl.test.ts | 11 +- src/logging/config.test.ts | 3 +- src/media-understanding/runtime.test.ts | 35 +-- src/media/server.outside-workspace.test.ts | 1 - src/media/server.test.ts | 1 - src/media/store.outside-workspace.test.ts | 8 +- src/pairing/setup-code.test.ts | 8 +- src/plugin-sdk/keyed-async-queue.test.ts | 8 +- src/plugin-sdk/outbound-media.test.ts | 8 +- src/plugins/bundled-web-search.test.ts | 23 +- .../capability-provider-runtime.test.ts | 10 +- src/plugins/marketplace.test.ts | 16 +- src/plugins/memory-runtime.test.ts | 18 +- src/plugins/providers.test.ts | 12 +- src/plugins/status.test.ts | 26 ++- src/process/command-queue.test.ts | 22 +- src/process/exec.no-output-timer.test.ts | 1 + src/process/kill-tree.test.ts | 8 +- src/process/supervisor/adapters/child.test.ts | 8 +- src/process/supervisor/adapters/pty.test.ts | 8 +- .../supervisor/supervisor.pty-command.test.ts | 8 +- src/secrets/configure.test.ts | 11 +- src/secrets/runtime-web-tools.test.ts | 13 +- src/secrets/runtime.coverage.test.ts | 14 +- src/secrets/runtime.test.ts | 15 +- src/security/windows-acl.test.ts | 1 - src/tasks/task-registry.test.ts | 207 ++++++++---------- src/tts/provider-registry.test.ts | 12 +- src/web-search/runtime.test.ts | 14 +- test/fixtures/test-timings.unit.json | 4 - 93 files changed, 874 insertions(+), 980 deletions(-) delete mode 100644 src/infra/outbound/targets.channel-resolution.test.ts diff --git a/src/acp/runtime/session-meta.test.ts b/src/acp/runtime/session-meta.test.ts index dc8c02b0c53..9de6ec46731 100644 --- a/src/acp/runtime/session-meta.test.ts +++ b/src/acp/runtime/session-meta.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; const hoisted = vi.hoisted(() => { @@ -20,12 +20,14 @@ vi.mock("../../config/sessions.js", () => ({ let listAcpSessionEntries: typeof import("./session-meta.js").listAcpSessionEntries; describe("listAcpSessionEntries", () => { - beforeEach(async () => { - vi.resetModules(); - vi.clearAllMocks(); + beforeAll(async () => { ({ listAcpSessionEntries } = await import("./session-meta.js")); }); + beforeEach(() => { + vi.clearAllMocks(); + }); + it("reads ACP sessions from resolved configured store targets", async () => { const cfg = { session: { diff --git a/src/agents/bash-tools.exec-host-shared.test.ts b/src/agents/bash-tools.exec-host-shared.test.ts index b393692f4c3..33c1069fb0a 100644 --- a/src/agents/bash-tools.exec-host-shared.test.ts +++ b/src/agents/bash-tools.exec-host-shared.test.ts @@ -1,4 +1,17 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + sendExecApprovalFollowup: vi.fn(), + logWarn: vi.fn(), +})); + +vi.mock("./bash-tools.exec-approval-followup.js", () => ({ + sendExecApprovalFollowup: mocks.sendExecApprovalFollowup, +})); + +vi.mock("../logger.js", () => ({ + logWarn: mocks.logWarn, +})); let sendExecApprovalFollowupResult: typeof import("./bash-tools.exec-host-shared.js").sendExecApprovalFollowupResult; let maxExecApprovalFollowupFailureLogKeys: typeof import("./bash-tools.exec-host-shared.js").MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS; @@ -6,20 +19,16 @@ let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup let logWarn: typeof import("../logger.js").logWarn; describe("sendExecApprovalFollowupResult", () => { - beforeEach(async () => { - vi.resetModules(); - vi.doMock("./bash-tools.exec-approval-followup.js", () => ({ - sendExecApprovalFollowup: vi.fn(), - })); - vi.doMock("../logger.js", () => ({ - logWarn: vi.fn(), - })); + beforeAll(async () => { ({ sendExecApprovalFollowupResult, MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS: maxExecApprovalFollowupFailureLogKeys, } = await import("./bash-tools.exec-host-shared.js")); ({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js")); ({ logWarn } = await import("../logger.js")); + }); + + beforeEach(() => { vi.mocked(sendExecApprovalFollowup).mockReset(); vi.mocked(logWarn).mockReset(); }); diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index 8973661ae3b..f6b9665dc9c 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; const requestHeartbeatNowMock = vi.hoisted(() => vi.fn()); @@ -11,7 +11,7 @@ let formatExecFailureReason: typeof import("./bash-tools.exec-runtime.js").forma let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget; describe("detectCursorKeyMode", () => { - beforeEach(async () => { + beforeAll(async () => { ({ detectCursorKeyMode } = await import("./bash-tools.exec-runtime.js")); }); @@ -43,7 +43,7 @@ describe("detectCursorKeyMode", () => { }); describe("resolveExecTarget", () => { - beforeEach(async () => { + beforeAll(async () => { ({ resolveExecTarget } = await import("./bash-tools.exec-runtime.js")); }); diff --git a/src/agents/minimax-vlm.normalizes-api-key.test.ts b/src/agents/minimax-vlm.normalizes-api-key.test.ts index 146f90bbb62..5156fc5817d 100644 --- a/src/agents/minimax-vlm.normalizes-api-key.test.ts +++ b/src/agents/minimax-vlm.normalizes-api-key.test.ts @@ -1,6 +1,13 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; +let isMinimaxVlmModel: typeof import("./minimax-vlm.js").isMinimaxVlmModel; +let minimaxUnderstandImage: typeof import("./minimax-vlm.js").minimaxUnderstandImage; + +beforeAll(async () => { + ({ isMinimaxVlmModel, minimaxUnderstandImage } = await import("./minimax-vlm.js")); +}); + describe("minimaxUnderstandImage apiKey normalization", () => { const priorFetch = global.fetch; const apiResponse = JSON.stringify({ @@ -25,7 +32,6 @@ describe("minimaxUnderstandImage apiKey normalization", () => { }); global.fetch = withFetchPreconnect(fetchSpy); - const { minimaxUnderstandImage } = await import("./minimax-vlm.js"); const text = await minimaxUnderstandImage({ apiKey, prompt: "hi", @@ -48,8 +54,6 @@ describe("minimaxUnderstandImage apiKey normalization", () => { describe("isMinimaxVlmModel", () => { it("only matches the canonical MiniMax VLM model id", async () => { - const { isMinimaxVlmModel } = await import("./minimax-vlm.js"); - expect(isMinimaxVlmModel("minimax", "MiniMax-VL-01")).toBe(true); expect(isMinimaxVlmModel("minimax-portal", "MiniMax-VL-01")).toBe(true); expect(isMinimaxVlmModel("minimax-portal", "custom-vision")).toBe(false); diff --git a/src/agents/models-config.providers.policy.test.ts b/src/agents/models-config.providers.policy.test.ts index bcebe8b163b..4777ba08ce7 100644 --- a/src/agents/models-config.providers.policy.test.ts +++ b/src/agents/models-config.providers.policy.test.ts @@ -1,9 +1,15 @@ -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; + +let normalizeProviderSpecificConfig: typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig; +let resolveProviderConfigApiKeyResolver: typeof import("./models-config.providers.policy.js").resolveProviderConfigApiKeyResolver; + +beforeAll(async () => { + ({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } = + await import("./models-config.providers.policy.js")); +}); describe("models-config.providers.policy", () => { it("resolves config apiKey markers through provider plugin hooks", async () => { - const { resolveProviderConfigApiKeyResolver } = - await import("./models-config.providers.policy.js"); const env = { AWS_PROFILE: "default", } as NodeJS.ProcessEnv; @@ -14,8 +20,6 @@ describe("models-config.providers.policy", () => { }); it("resolves anthropic-vertex ADC markers through provider plugin hooks", async () => { - const { resolveProviderConfigApiKeyResolver } = - await import("./models-config.providers.policy.js"); const resolver = resolveProviderConfigApiKeyResolver("anthropic-vertex"); expect(resolver).toBeTypeOf("function"); @@ -27,8 +31,6 @@ describe("models-config.providers.policy", () => { }); it("normalizes Google provider config through provider plugin hooks", async () => { - const { normalizeProviderSpecificConfig } = await import("./models-config.providers.policy.js"); - expect( normalizeProviderSpecificConfig("google", { api: "google-generative-ai", diff --git a/src/agents/openclaw-tools.web-runtime.test.ts b/src/agents/openclaw-tools.web-runtime.test.ts index 45f4649386f..7ca83f37b27 100644 --- a/src/agents/openclaw-tools.web-runtime.test.ts +++ b/src/agents/openclaw-tools.web-runtime.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { RuntimeWebFetchFirecrawlMetadata } from "../secrets/runtime-web-tools.types.js"; import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js"; @@ -138,8 +138,7 @@ async function prepareAndActivate(params: { config: OpenClawConfig; env?: NodeJS describe("openclaw tools runtime web metadata wiring", () => { const priorFetch = global.fetch; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { secretsRuntime = await import("../secrets/runtime.js"); ({ createWebFetchTool, createWebSearchTool } = await import("./tools/web-tools.js")); }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts index 1007c0d4b2f..a4ac130a514 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts @@ -12,6 +12,11 @@ import { import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; describe("subscribeEmbeddedPiSession", () => { + async function flushBlockReplyCallbacks(): Promise { + await Promise.resolve(); + await Promise.resolve(); + } + function createAgentEventHarness(options?: { runId?: string; sessionKey?: string }) { const { session, emit } = createStubSessionHarness(); const onAgentEvent = vi.fn(); @@ -132,10 +137,9 @@ describe("subscribeEmbeddedPiSession", () => { } as AssistantMessage; emit({ type: "message_end", message: assistantMessage }); + await flushBlockReplyCallbacks(); - await vi.waitFor(() => { - expect(onBlockReply).toHaveBeenCalledTimes(1); - }); + expect(onBlockReply).toHaveBeenCalledTimes(1); expect(onBlockReply.mock.calls[0][0].text).toBe("Final answer"); const streamTexts = onReasoningStream.mock.calls @@ -176,10 +180,9 @@ describe("subscribeEmbeddedPiSession", () => { message: { role: "assistant" }, assistantMessageEvent: { type: "text_end" }, }); + await flushBlockReplyCallbacks(); - await vi.waitFor(() => { - expect(onBlockReply.mock.calls.length).toBeGreaterThan(0); - }); + expect(onBlockReply.mock.calls.length).toBeGreaterThan(0); const payloadTexts = onBlockReply.mock.calls .map((call) => call[0]?.text) .filter((value): value is string => typeof value === "string"); diff --git a/src/agents/provider-capabilities.test.ts b/src/agents/provider-capabilities.test.ts index 90c4055862c..c39306231d5 100644 --- a/src/agents/provider-capabilities.test.ts +++ b/src/agents/provider-capabilities.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveProviderCapabilitiesWithPluginMock = vi.fn((params: { provider: string }) => { switch (params.provider) { @@ -65,24 +65,22 @@ let shouldSanitizeGeminiThoughtSignaturesForModel: typeof import("./provider-cap let supportsOpenAiCompatTurnValidation: typeof import("./provider-capabilities.js").supportsOpenAiCompatTurnValidation; let usesMoonshotThinkingPayloadCompat: typeof import("./provider-capabilities.js").usesMoonshotThinkingPayloadCompat; -async function loadFreshProviderCapabilitiesModuleForTest() { - vi.resetModules(); - ({ - isAnthropicProviderFamily, - isOpenAiProviderFamily, - requiresOpenAiCompatibleAnthropicToolPayload, - resolveProviderCapabilities, - resolveTranscriptToolCallIdMode, - shouldDropThinkingBlocksForModel, - shouldSanitizeGeminiThoughtSignaturesForModel, - supportsOpenAiCompatTurnValidation, - usesMoonshotThinkingPayloadCompat, - } = await import("./provider-capabilities.js")); -} - describe("resolveProviderCapabilities", () => { - beforeEach(async () => { - await loadFreshProviderCapabilitiesModuleForTest(); + beforeAll(async () => { + ({ + isAnthropicProviderFamily, + isOpenAiProviderFamily, + requiresOpenAiCompatibleAnthropicToolPayload, + resolveProviderCapabilities, + resolveTranscriptToolCallIdMode, + shouldDropThinkingBlocksForModel, + shouldSanitizeGeminiThoughtSignaturesForModel, + supportsOpenAiCompatTurnValidation, + usesMoonshotThinkingPayloadCompat, + } = await import("./provider-capabilities.js")); + }); + + beforeEach(() => { resolveProviderCapabilitiesWithPluginMock.mockClear(); }); diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index 7616ac88f8f..554b7b0fa34 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => ({ resolveRuntimePluginRegistry: vi.fn(), @@ -9,14 +9,18 @@ vi.mock("../plugins/loader.js", () => ({ })); describe("ensureRuntimePluginsLoaded", () => { + let ensureRuntimePluginsLoaded: typeof import("./runtime-plugins.js").ensureRuntimePluginsLoaded; + + beforeAll(async () => { + ({ ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js")); + }); + beforeEach(() => { hoisted.resolveRuntimePluginRegistry.mockReset(); hoisted.resolveRuntimePluginRegistry.mockReturnValue(undefined); - vi.resetModules(); }); it("does not reactivate plugins when a process already has an active registry", async () => { - const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js"); hoisted.resolveRuntimePluginRegistry.mockReturnValue({}); ensureRuntimePluginsLoaded({ @@ -29,8 +33,6 @@ describe("ensureRuntimePluginsLoaded", () => { }); it("resolves runtime plugins through the shared runtime helper", async () => { - const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js"); - ensureRuntimePluginsLoaded({ config: {} as never, workspaceDir: "/tmp/workspace", diff --git a/src/agents/session-write-lock.test.ts b/src/agents/session-write-lock.test.ts index fa8d35eb878..f067232244e 100644 --- a/src/agents/session-write-lock.test.ts +++ b/src/agents/session-write-lock.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; const FAKE_STARTTIME = 12345; let __testing: typeof import("./session-write-lock.js").__testing; @@ -10,25 +10,14 @@ let cleanStaleLockFiles: typeof import("./session-write-lock.js").cleanStaleLock let resetSessionWriteLockStateForTest: typeof import("./session-write-lock.js").resetSessionWriteLockStateForTest; let resolveSessionLockMaxHoldFromTimeout: typeof import("./session-write-lock.js").resolveSessionLockMaxHoldFromTimeout; -async function loadFreshSessionWriteLockModuleForTest() { - vi.resetModules(); - // Mock getProcessStartTime so PID-recycling detection works on non-Linux - // (macOS, CI runners). isPidAlive is left unmocked. - vi.doMock("../shared/pid-alive.js", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - getProcessStartTime: (pid: number) => (pid === process.pid ? FAKE_STARTTIME : null), - }; - }); - ({ - __testing, - acquireSessionWriteLock, - cleanStaleLockFiles, - resetSessionWriteLockStateForTest, - resolveSessionLockMaxHoldFromTimeout, - } = await import("./session-write-lock.js")); -} +vi.mock("../shared/pid-alive.js", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + // Keep liveness checks real; only pin process start time for PID recycle coverage. + getProcessStartTime: (pid: number) => (pid === process.pid ? FAKE_STARTTIME : null), + }; +}); async function expectLockRemovedOnlyAfterFinalRelease(params: { lockPath: string; @@ -104,8 +93,14 @@ async function expectActiveInProcessLockIsNotReclaimed(params?: { } describe("acquireSessionWriteLock", () => { - beforeEach(async () => { - await loadFreshSessionWriteLockModuleForTest(); + beforeAll(async () => { + ({ + __testing, + acquireSessionWriteLock, + cleanStaleLockFiles, + resetSessionWriteLockStateForTest, + resolveSessionLockMaxHoldFromTimeout, + } = await import("./session-write-lock.js")); }); afterEach(() => { diff --git a/src/agents/simple-completion-transport.test.ts b/src/agents/simple-completion-transport.test.ts index d4ca6c2e7bd..085c25780a2 100644 --- a/src/agents/simple-completion-transport.test.ts +++ b/src/agents/simple-completion-transport.test.ts @@ -1,33 +1,36 @@ import type { Model } from "@mariozechner/pi-ai"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; const createAnthropicVertexStreamFnForModel = vi.fn(); const ensureCustomApiRegistered = vi.fn(); const resolveProviderStreamFn = vi.fn(); +vi.mock("./anthropic-vertex-stream.js", () => ({ + createAnthropicVertexStreamFnForModel, +})); + +vi.mock("./custom-api-registry.js", () => ({ + ensureCustomApiRegistered, +})); + +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveProviderStreamFn, +})); + let prepareModelForSimpleCompletion: typeof import("./simple-completion-transport.js").prepareModelForSimpleCompletion; describe("prepareModelForSimpleCompletion", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ prepareModelForSimpleCompletion } = await import("./simple-completion-transport.js")); + }); + + beforeEach(() => { createAnthropicVertexStreamFnForModel.mockReset(); ensureCustomApiRegistered.mockReset(); resolveProviderStreamFn.mockReset(); createAnthropicVertexStreamFnForModel.mockReturnValue("vertex-stream"); resolveProviderStreamFn.mockReturnValue("ollama-stream"); - - vi.doMock("./anthropic-vertex-stream.js", () => ({ - createAnthropicVertexStreamFnForModel, - })); - vi.doMock("./custom-api-registry.js", () => ({ - ensureCustomApiRegistered, - })); - vi.doMock("../plugins/provider-runtime.js", () => ({ - resolveProviderStreamFn, - })); - - ({ prepareModelForSimpleCompletion } = await import("./simple-completion-transport.js")); }); it("registers the configured Ollama transport and keeps the original api", () => { diff --git a/src/agents/skills/plugin-skills.test.ts b/src/agents/skills/plugin-skills.test.ts index 9d8541c72bb..2eee4b980ed 100644 --- a/src/agents/skills/plugin-skills.test.ts +++ b/src/agents/skills/plugin-skills.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import type { PluginManifestRegistry } from "../../plugins/manifest-registry.js"; import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js"; @@ -15,14 +15,6 @@ vi.mock("../../plugins/manifest-registry.js", () => ({ let resolvePluginSkillDirs: typeof import("./plugin-skills.js").resolvePluginSkillDirs; -async function loadFreshPluginSkillsModuleForTest() { - vi.resetModules(); - vi.doMock("../../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: (...args: unknown[]) => hoisted.loadPluginManifestRegistry(...args), - })); - ({ resolvePluginSkillDirs } = await import("./plugin-skills.js")); -} - const tempDirs = createTrackedTempDirs(); function buildRegistry(params: { acpxRoot: string; helperRoot: string }): PluginManifestRegistry { @@ -109,8 +101,12 @@ afterEach(async () => { }); describe("resolvePluginSkillDirs", () => { - beforeEach(async () => { - await loadFreshPluginSkillsModuleForTest(); + beforeAll(async () => { + ({ resolvePluginSkillDirs } = await import("./plugin-skills.js")); + }); + + beforeEach(() => { + hoisted.loadPluginManifestRegistry.mockReset(); }); it.each([ diff --git a/src/agents/skills/refresh.test.ts b/src/agents/skills/refresh.test.ts index cf40c795bdd..227b3412634 100644 --- a/src/agents/skills/refresh.test.ts +++ b/src/agents/skills/refresh.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const watchMock = vi.fn(() => ({ on: vi.fn(), @@ -9,18 +9,17 @@ const watchMock = vi.fn(() => ({ let refreshModule: typeof import("./refresh.js"); -async function loadFreshRefreshModuleForTest() { - vi.resetModules(); - vi.doMock("chokidar", () => ({ - default: { watch: watchMock }, - })); - refreshModule = await import("./refresh.js"); -} +vi.mock("chokidar", () => ({ + default: { watch: watchMock }, +})); describe("ensureSkillsWatcher", () => { - beforeEach(async () => { + beforeAll(async () => { + refreshModule = await import("./refresh.js"); + }); + + beforeEach(() => { watchMock.mockClear(); - await loadFreshRefreshModuleForTest(); }); afterEach(async () => { diff --git a/src/agents/tools/gateway.test.ts b/src/agents/tools/gateway.test.ts index a8bc3683fa0..1326f25445f 100644 --- a/src/agents/tools/gateway.test.ts +++ b/src/agents/tools/gateway.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const callGatewayMock = vi.fn(); const configState = vi.hoisted(() => ({ @@ -15,28 +15,19 @@ vi.mock("../../gateway/call.js", () => ({ let callGatewayTool: typeof import("./gateway.js").callGatewayTool; let resolveGatewayOptions: typeof import("./gateway.js").resolveGatewayOptions; -async function loadFreshGatewayToolModuleForTest() { - vi.resetModules(); - vi.doMock("../../config/config.js", () => ({ - loadConfig: () => configState.value, - resolveGatewayPort: () => 18789, - })); - vi.doMock("../../gateway/call.js", () => ({ - callGateway: (...args: unknown[]) => callGatewayMock(...args), - })); - ({ callGatewayTool, resolveGatewayOptions } = await import("./gateway.js")); -} - describe("gateway tool defaults", () => { const envSnapshot = { openclaw: process.env.OPENCLAW_GATEWAY_TOKEN, }; - beforeEach(async () => { + beforeAll(async () => { + ({ callGatewayTool, resolveGatewayOptions } = await import("./gateway.js")); + }); + + beforeEach(() => { callGatewayMock.mockClear(); configState.value = {}; delete process.env.OPENCLAW_GATEWAY_TOKEN; - await loadFreshGatewayToolModuleForTest(); }); afterAll(() => { diff --git a/src/agents/tools/nodes-utils.test.ts b/src/agents/tools/nodes-utils.test.ts index 6fa264a1015..de7a72c95ca 100644 --- a/src/agents/tools/nodes-utils.test.ts +++ b/src/agents/tools/nodes-utils.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const gatewayMocks = vi.hoisted(() => ({ callGatewayTool: vi.fn(), @@ -21,12 +21,14 @@ function node({ nodeId, ...overrides }: Partial & { nodeId: string }; } -beforeEach(async () => { - vi.resetModules(); - gatewayMocks.callGatewayTool.mockReset(); +beforeAll(async () => { ({ listNodes, resolveNodeIdFromList } = await import("./nodes-utils.js")); }); +beforeEach(() => { + gatewayMocks.callGatewayTool.mockReset(); +}); + describe("resolveNodeIdFromList defaults", () => { it("falls back to most recently connected node when multiple non-Mac candidates exist", () => { const nodes: NodeListNode[] = [ diff --git a/src/agents/tools/sessions-list-tool.test.ts b/src/agents/tools/sessions-list-tool.test.ts index f5d5db3cc24..85900ae309e 100644 --- a/src/agents/tools/sessions-list-tool.test.ts +++ b/src/agents/tools/sessions-list-tool.test.ts @@ -1,12 +1,59 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + gatewayCall: vi.fn(), + createAgentToAgentPolicy: vi.fn(() => ({})), + createSessionVisibilityGuard: vi.fn(async () => ({ + check: () => ({ allowed: true }), + })), + resolveEffectiveSessionToolsVisibility: vi.fn(() => "all"), + resolveSandboxedSessionToolContext: vi.fn(() => ({ + mainKey: "main", + alias: "main", + requesterInternalKey: undefined, + restrictToSpawned: false, + })), +})); + +vi.mock("../../gateway/call.js", () => ({ + callGateway: (opts: unknown) => mocks.gatewayCall(opts), +})); + +vi.mock("./sessions-helpers.js", async (importActual) => { + const actual = await importActual(); + return { + ...actual, + createAgentToAgentPolicy: () => mocks.createAgentToAgentPolicy(), + createSessionVisibilityGuard: async () => await mocks.createSessionVisibilityGuard(), + resolveEffectiveSessionToolsVisibility: () => mocks.resolveEffectiveSessionToolsVisibility(), + resolveSandboxedSessionToolContext: () => mocks.resolveSandboxedSessionToolContext(), + }; +}); describe("sessions-list-tool", () => { + let createSessionsListTool: typeof import("./sessions-list-tool.js").createSessionsListTool; + + beforeAll(async () => { + ({ createSessionsListTool } = await import("./sessions-list-tool.js")); + }); + beforeEach(() => { - vi.resetModules(); + vi.clearAllMocks(); + mocks.createAgentToAgentPolicy.mockReturnValue({}); + mocks.createSessionVisibilityGuard.mockResolvedValue({ + check: () => ({ allowed: true }), + }); + mocks.resolveEffectiveSessionToolsVisibility.mockReturnValue("all"); + mocks.resolveSandboxedSessionToolContext.mockReturnValue({ + mainKey: "main", + alias: "main", + requesterInternalKey: undefined, + restrictToSpawned: false, + }); }); it("keeps deliveryContext.threadId in sessions_list results", async () => { - const gatewayCallMock = vi.fn(async (opts: unknown) => { + mocks.gatewayCall.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "sessions.list") { return { @@ -39,30 +86,6 @@ describe("sessions-list-tool", () => { } return {}; }); - - vi.doMock("../../gateway/call.js", () => ({ - callGateway: gatewayCallMock, - })); - vi.doMock("./sessions-helpers.js", async () => { - const actual = - await vi.importActual("./sessions-helpers.js"); - return { - ...actual, - createAgentToAgentPolicy: () => ({}), - createSessionVisibilityGuard: async () => ({ - check: () => ({ allowed: true }), - }), - resolveEffectiveSessionToolsVisibility: () => "all", - resolveSandboxedSessionToolContext: () => ({ - mainKey: "main", - alias: "main", - requesterInternalKey: undefined, - restrictToSpawned: false, - }), - }; - }); - - const { createSessionsListTool } = await import("./sessions-list-tool.js"); const tool = createSessionsListTool({ config: {} as never }); const result = await tool.execute("call-1", {}); @@ -92,7 +115,7 @@ describe("sessions-list-tool", () => { }); it("keeps numeric deliveryContext.threadId in sessions_list results", async () => { - const gatewayCallMock = vi.fn(async (opts: unknown) => { + mocks.gatewayCall.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "sessions.list") { return { @@ -114,30 +137,6 @@ describe("sessions-list-tool", () => { } return {}; }); - - vi.doMock("../../gateway/call.js", () => ({ - callGateway: gatewayCallMock, - })); - vi.doMock("./sessions-helpers.js", async () => { - const actual = - await vi.importActual("./sessions-helpers.js"); - return { - ...actual, - createAgentToAgentPolicy: () => ({}), - createSessionVisibilityGuard: async () => ({ - check: () => ({ allowed: true }), - }), - resolveEffectiveSessionToolsVisibility: () => "all", - resolveSandboxedSessionToolContext: () => ({ - mainKey: "main", - alias: "main", - requesterInternalKey: undefined, - restrictToSpawned: false, - }), - }; - }); - - const { createSessionsListTool } = await import("./sessions-list-tool.js"); const tool = createSessionsListTool({ config: {} as never }); const result = await tool.execute("call-2", {}); @@ -161,7 +160,7 @@ describe("sessions-list-tool", () => { }); it("keeps live session setting metadata in sessions_list results", async () => { - const gatewayCallMock = vi.fn(async (opts: unknown) => { + mocks.gatewayCall.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "sessions.list") { return { @@ -183,30 +182,6 @@ describe("sessions-list-tool", () => { } return {}; }); - - vi.doMock("../../gateway/call.js", () => ({ - callGateway: gatewayCallMock, - })); - vi.doMock("./sessions-helpers.js", async () => { - const actual = - await vi.importActual("./sessions-helpers.js"); - return { - ...actual, - createAgentToAgentPolicy: () => ({}), - createSessionVisibilityGuard: async () => ({ - check: () => ({ allowed: true }), - }), - resolveEffectiveSessionToolsVisibility: () => "all", - resolveSandboxedSessionToolContext: () => ({ - mainKey: "main", - alias: "main", - requesterInternalKey: undefined, - restrictToSpawned: false, - }), - }; - }); - - const { createSessionsListTool } = await import("./sessions-list-tool.js"); const tool = createSessionsListTool({ config: {} as never }); const result = await tool.execute("call-3", {}); diff --git a/src/agents/tools/sessions-resolution.test.ts b/src/agents/tools/sessions-resolution.test.ts index 47e4615410e..f6faca642bc 100644 --- a/src/agents/tools/sessions-resolution.test.ts +++ b/src/agents/tools/sessions-resolution.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; const callGatewayMock = vi.fn(); vi.mock("../../gateway/call.js", () => ({ @@ -14,11 +14,7 @@ let resolveSessionReference: typeof import("./sessions-resolution.js").resolveSe let shouldVerifyRequesterSpawnedSessionVisibility: typeof import("./sessions-resolution.js").shouldVerifyRequesterSpawnedSessionVisibility; let shouldResolveSessionIdInput: typeof import("./sessions-resolution.js").shouldResolveSessionIdInput; -async function loadFreshSessionsResolutionModuleForTest() { - vi.resetModules(); - vi.doMock("../../gateway/call.js", () => ({ - callGateway: (opts: unknown) => callGatewayMock(opts), - })); +beforeAll(async () => { ({ isResolvedSessionVisibleToRequester, looksLikeSessionId, @@ -30,11 +26,10 @@ async function loadFreshSessionsResolutionModuleForTest() { shouldVerifyRequesterSpawnedSessionVisibility, shouldResolveSessionIdInput, } = await import("./sessions-resolution.js")); -} +}); -beforeEach(async () => { +beforeEach(() => { callGatewayMock.mockReset(); - await loadFreshSessionsResolutionModuleForTest(); }); describe("resolveMainSessionAlias", () => { diff --git a/src/agents/tools/tts-tool.test.ts b/src/agents/tools/tts-tool.test.ts index 52bc1c138c6..f559166d7bb 100644 --- a/src/agents/tools/tts-tool.test.ts +++ b/src/agents/tools/tts-tool.test.ts @@ -1,18 +1,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; +import * as ttsRuntime from "../../tts/tts.js"; +import { createTtsTool } from "./tts-tool.js"; let textToSpeechSpy: ReturnType; describe("createTtsTool", () => { - beforeEach(async () => { + beforeEach(() => { vi.restoreAllMocks(); - vi.resetModules(); - const ttsRuntime = await import("../../tts/tts.js"); textToSpeechSpy = vi.spyOn(ttsRuntime, "textToSpeech"); }); - it("uses SILENT_REPLY_TOKEN in guidance text", async () => { - const { createTtsTool } = await import("./tts-tool.js"); + it("uses SILENT_REPLY_TOKEN in guidance text", () => { const tool = createTtsTool(); expect(tool.description).toContain(SILENT_REPLY_TOKEN); @@ -26,7 +25,6 @@ describe("createTtsTool", () => { voiceCompatible: true, }); - const { createTtsTool } = await import("./tts-tool.js"); const tool = createTtsTool(); const result = await tool.execute("call-1", { text: "hello" }); diff --git a/src/agents/transcript-policy.test.ts b/src/agents/transcript-policy.test.ts index 33c45f330e6..8dcd3acd14f 100644 --- a/src/agents/transcript-policy.test.ts +++ b/src/agents/transcript-policy.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../plugins/provider-runtime.js", () => ({ resolveProviderCapabilitiesWithPlugin: vi.fn(({ provider }: { provider?: string }) => { @@ -29,14 +29,13 @@ vi.mock("../plugins/provider-runtime.js", () => ({ let resolveTranscriptPolicy: typeof import("./transcript-policy.js").resolveTranscriptPolicy; -async function loadFreshTranscriptPolicyModuleForTest() { - vi.resetModules(); - ({ resolveTranscriptPolicy } = await import("./transcript-policy.js")); -} - describe("resolveTranscriptPolicy", () => { - beforeEach(async () => { - await loadFreshTranscriptPolicyModuleForTest(); + beforeAll(async () => { + ({ resolveTranscriptPolicy } = await import("./transcript-policy.js")); + }); + + beforeEach(() => { + vi.clearAllMocks(); }); it("enables sanitizeToolCallIds for Anthropic provider", () => { diff --git a/src/auto-reply/reply/commands-core.test.ts b/src/auto-reply/reply/commands-core.test.ts index 226037f957a..c58556ad038 100644 --- a/src/auto-reply/reply/commands-core.test.ts +++ b/src/auto-reply/reply/commands-core.test.ts @@ -40,7 +40,7 @@ describe("emitResetCommandHooks", () => { workspaceDir: "/tmp/openclaw-workspace", }); - await vi.waitFor(() => expect(hookRunnerMocks.runBeforeReset).toHaveBeenCalledTimes(1)); + expect(hookRunnerMocks.runBeforeReset).toHaveBeenCalledTimes(1); const [, ctx] = hookRunnerMocks.runBeforeReset.mock.calls[0] ?? []; return ctx; } diff --git a/src/auto-reply/reply/session-hooks-context.test.ts b/src/auto-reply/reply/session-hooks-context.test.ts index ee9c20420ee..140a4442d5b 100644 --- a/src/auto-reply/reply/session-hooks-context.test.ts +++ b/src/auto-reply/reply/session-hooks-context.test.ts @@ -65,7 +65,7 @@ describe("session hook context wiring", () => { commandAuthorized: true, }); - await vi.waitFor(() => expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1)); + expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1); const [event, context] = hookRunnerMocks.runSessionStart.mock.calls[0] ?? []; expect(event).toMatchObject({ sessionKey }); expect(context).toMatchObject({ sessionKey, agentId: "main" }); @@ -89,8 +89,8 @@ describe("session hook context wiring", () => { commandAuthorized: true, }); - await vi.waitFor(() => expect(hookRunnerMocks.runSessionEnd).toHaveBeenCalledTimes(1)); - await vi.waitFor(() => expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1)); + expect(hookRunnerMocks.runSessionEnd).toHaveBeenCalledTimes(1); + expect(hookRunnerMocks.runSessionStart).toHaveBeenCalledTimes(1); const [event, context] = hookRunnerMocks.runSessionEnd.mock.calls[0] ?? []; expect(event).toMatchObject({ sessionKey }); expect(context).toMatchObject({ sessionKey, agentId: "main" }); diff --git a/src/channels/session.test.ts b/src/channels/session.test.ts index 143e0fa5564..2627a0acfe4 100644 --- a/src/channels/session.test.ts +++ b/src/channels/session.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { MsgContext } from "../auto-reply/templating.js"; const recordSessionMetaFromInboundMock = vi.fn((_args?: unknown) => Promise.resolve(undefined)); @@ -21,9 +21,11 @@ describe("recordInboundSession", () => { OriginatingTo: "demo-channel:1234", }; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ recordInboundSession } = await import("./session.js")); + }); + + beforeEach(() => { recordSessionMetaFromInboundMock.mockClear(); updateLastRouteMock.mockClear(); }); diff --git a/src/config/config.web-search-provider.test.ts b/src/config/config.web-search-provider.test.ts index 98b682a6598..e0d2f1862f9 100644 --- a/src/config/config.web-search-provider.test.ts +++ b/src/config/config.web-search-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { buildWebSearchProviderConfig } from "./test-helpers.js"; vi.mock("../runtime.js", () => ({ @@ -77,8 +77,7 @@ vi.mock("../plugins/web-search-providers.js", () => { let validateConfigObjectWithPlugins: typeof import("./config.js").validateConfigObjectWithPlugins; let resolveSearchProvider: typeof import("../agents/tools/web-search.js").__testing.resolveSearchProvider; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ validateConfigObjectWithPlugins } = await import("./config.js")); ({ __testing: { resolveSearchProvider }, diff --git a/src/config/logging.test.ts b/src/config/logging.test.ts index e410c3f81ba..6f6246a17fd 100644 --- a/src/config/logging.test.ts +++ b/src/config/logging.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ createConfigIO: vi.fn().mockReturnValue({ @@ -13,11 +13,14 @@ vi.mock("./io.js", () => ({ let formatConfigPath: typeof import("./logging.js").formatConfigPath; let logConfigUpdated: typeof import("./logging.js").logConfigUpdated; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ formatConfigPath, logConfigUpdated } = await import("./logging.js")); }); +beforeEach(() => { + mocks.createConfigIO.mockClear(); +}); + describe("config logging", () => { it("formats the live config path when no explicit path is provided", () => { expect(formatConfigPath()).toBe("/tmp/openclaw-dev/openclaw.json"); diff --git a/src/config/runtime-schema.test.ts b/src/config/runtime-schema.test.ts index 8704106977c..1b488918fec 100644 --- a/src/config/runtime-schema.test.ts +++ b/src/config/runtime-schema.test.ts @@ -1,10 +1,13 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js"; const mockLoadConfig = vi.hoisted(() => vi.fn<() => OpenClawConfig>()); const mockReadConfigFileSnapshot = vi.hoisted(() => vi.fn<() => Promise>()); const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn()); +let readBestEffortRuntimeConfigSchema: typeof import("./runtime-schema.js").readBestEffortRuntimeConfigSchema; +let loadGatewayRuntimeConfigSchema: typeof import("./runtime-schema.js").loadGatewayRuntimeConfigSchema; + vi.mock("./config.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -127,7 +130,6 @@ function makeManifestRegistry() { } async function readSchemaNodes() { - const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js"); const result = await readBestEffortRuntimeConfigSchema(); const schema = result.schema as { properties?: Record }; const channelsNode = schema.properties?.channels as Record | undefined; @@ -139,6 +141,11 @@ async function readSchemaNodes() { return { channelProps, entryProps }; } +beforeAll(async () => { + ({ readBestEffortRuntimeConfigSchema, loadGatewayRuntimeConfigSchema } = + await import("./runtime-schema.js")); +}); + describe("readBestEffortRuntimeConfigSchema", () => { beforeEach(() => { vi.clearAllMocks(); @@ -192,7 +199,6 @@ describe("loadGatewayRuntimeConfigSchema", () => { }); it("uses manifest metadata instead of booting plugin runtime", async () => { - const { loadGatewayRuntimeConfigSchema } = await import("./runtime-schema.js"); const result = loadGatewayRuntimeConfigSchema(); const schema = result.schema as { properties?: Record }; const channelsNode = schema.properties?.channels as Record | undefined; diff --git a/src/config/sessions/delivery-info.test.ts b/src/config/sessions/delivery-info.test.ts index 2f315fd807e..9a435c38a7a 100644 --- a/src/config/sessions/delivery-info.test.ts +++ b/src/config/sessions/delivery-info.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "./types.js"; const storeState = vi.hoisted(() => ({ @@ -26,12 +26,14 @@ const buildEntry = (deliveryContext: SessionEntry["deliveryContext"]): SessionEn deliveryContext, }); -beforeEach(async () => { - vi.resetModules(); - storeState.store = {}; +beforeAll(async () => { ({ extractDeliveryInfo, parseSessionThreadInfo } = await import("./delivery-info.js")); }); +beforeEach(() => { + storeState.store = {}; +}); + describe("extractDeliveryInfo", () => { it("parses base session and thread/topic ids", () => { expect(parseSessionThreadInfo("agent:main:telegram:group:1:topic:55")).toEqual({ diff --git a/src/config/validation.channel-metadata.test.ts b/src/config/validation.channel-metadata.test.ts index 3961a35e4a7..7d688813f37 100644 --- a/src/config/validation.channel-metadata.test.ts +++ b/src/config/validation.channel-metadata.test.ts @@ -1,11 +1,19 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn()); +let validateConfigObjectWithPlugins: typeof import("./validation.js").validateConfigObjectWithPlugins; +let validateConfigObjectRawWithPlugins: typeof import("./validation.js").validateConfigObjectRawWithPlugins; + vi.mock("../plugins/manifest-registry.js", () => ({ loadPluginManifestRegistry: (...args: unknown[]) => mockLoadPluginManifestRegistry(...args), })); +beforeAll(async () => { + ({ validateConfigObjectWithPlugins, validateConfigObjectRawWithPlugins } = + await import("./validation.js")); +}); + function setupTelegramSchemaWithDefault() { mockLoadPluginManifestRegistry.mockReturnValue({ diagnostics: [], @@ -44,7 +52,6 @@ describe("validateConfigObjectWithPlugins channel metadata (applyDefaults: true) it("applies bundled channel defaults from plugin-owned schema metadata", async () => { setupTelegramSchemaWithDefault(); - const { validateConfigObjectWithPlugins } = await import("./validation.js"); const result = validateConfigObjectWithPlugins({ channels: { telegram: {}, @@ -71,7 +78,6 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => { // merge-patched value) instead of validated.config. setupTelegramSchemaWithDefault(); - const { validateConfigObjectRawWithPlugins } = await import("./validation.js"); const result = validateConfigObjectRawWithPlugins({ channels: { telegram: {}, diff --git a/src/cron/service.issue-regressions.test.ts b/src/cron/service.issue-regressions.test.ts index ccd2080c36c..fe0c87b4de9 100644 --- a/src/cron/service.issue-regressions.test.ts +++ b/src/cron/service.issue-regressions.test.ts @@ -1556,12 +1556,17 @@ describe("Cron issue regressions", () => { let now = dueAt; let activeRuns = 0; let peakActiveRuns = 0; + const firstStarted = createDeferred(); const firstRun = createDeferred<{ status: "ok"; summary: string }>(); const secondRun = createDeferred<{ status: "ok"; summary: string }>(); const secondStarted = createDeferred(); + const bothFinished = createDeferred(); const runIsolatedAgentJob = vi.fn(async (params: { job: { id: string } }) => { activeRuns += 1; peakActiveRuns = Math.max(peakActiveRuns, activeRuns); + if (params.job.id === first.id) { + firstStarted.resolve(); + } if (params.job.id === second.id) { secondStarted.resolve(); } @@ -1583,6 +1588,11 @@ describe("Cron issue regressions", () => { enqueueSystemEvent: vi.fn(), requestHeartbeatNow: vi.fn(), runIsolatedAgentJob, + onEvent: (evt) => { + if (evt.action === "finished" && evt.jobId === second.id && evt.status === "ok") { + bothFinished.resolve(); + } + }, }); const firstAck = await enqueueRun(state, first.id, "force"); @@ -1590,7 +1600,7 @@ describe("Cron issue regressions", () => { expect(firstAck).toEqual({ ok: true, enqueued: true, runId: expect.any(String) }); expect(secondAck).toEqual({ ok: true, enqueued: true, runId: expect.any(String) }); - await vi.waitFor(() => expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1)); + await firstStarted.promise; expect(runIsolatedAgentJob.mock.calls[0]?.[0]).toMatchObject({ job: { id: first.id } }); expect(peakActiveRuns).toBe(1); @@ -1601,11 +1611,10 @@ describe("Cron issue regressions", () => { expect(peakActiveRuns).toBe(1); secondRun.resolve({ status: "ok", summary: "second queued run" }); - await vi.waitFor(() => { - const jobs = state.store?.jobs ?? []; - expect(jobs.find((job) => job.id === first.id)?.state.lastStatus).toBe("ok"); - expect(jobs.find((job) => job.id === second.id)?.state.lastStatus).toBe("ok"); - }); + await bothFinished.promise; + const jobs = state.store?.jobs ?? []; + expect(jobs.find((job) => job.id === first.id)?.state.lastStatus).toBe("ok"); + expect(jobs.find((job) => job.id === second.id)?.state.lastStatus).toBe("ok"); clearCommandLane(CommandLane.Cron); }); @@ -1618,6 +1627,10 @@ describe("Cron issue regressions", () => { const dueAt = Date.parse("2026-02-06T10:05:03.000Z"); const job = createDueIsolatedJob({ id: "queued-failure", nowMs: dueAt, nextRunAtMs: dueAt }); const log = createNoopLogger(); + const errorLogged = createDeferred(); + log.error.mockImplementation(() => { + errorLogged.resolve(); + }); const badStore = `${makeStorePath().storePath}.dir`; await fs.mkdir(badStore, { recursive: true }); const state = createRunningCronServiceState({ @@ -1630,7 +1643,8 @@ describe("Cron issue regressions", () => { const result = await enqueueRun(state, job.id, "force"); expect(result).toEqual({ ok: true, enqueued: true, runId: expect.any(String) }); - await vi.waitFor(() => expect(log.error).toHaveBeenCalledTimes(1)); + await errorLogged.promise; + expect(log.error).toHaveBeenCalledTimes(1); expect(log.error.mock.calls[0]?.[1]).toBe( "cron: queued manual run background execution failed", ); diff --git a/src/daemon/runtime-hints.windows-paths.test.ts b/src/daemon/runtime-hints.windows-paths.test.ts index 450f517ec11..095a40768ce 100644 --- a/src/daemon/runtime-hints.windows-paths.test.ts +++ b/src/daemon/runtime-hints.windows-paths.test.ts @@ -1,21 +1,22 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; -afterEach(() => { - vi.resetModules(); - vi.doUnmock("./launchd.js"); -}); +const resolveGatewayLogPathsMock = vi.fn(() => ({ + stdoutPath: "C:\\tmp\\openclaw-state\\logs\\gateway.log", + stderrPath: "C:\\tmp\\openclaw-state\\logs\\gateway.err.log", +})); + +vi.mock("./launchd.js", () => ({ + resolveGatewayLogPaths: resolveGatewayLogPathsMock, +})); + +let buildPlatformRuntimeLogHints: typeof import("./runtime-hints.js").buildPlatformRuntimeLogHints; describe("buildPlatformRuntimeLogHints", () => { - it("strips windows drive prefixes from darwin display paths", async () => { - vi.doMock("./launchd.js", () => ({ - resolveGatewayLogPaths: () => ({ - stdoutPath: "C:\\tmp\\openclaw-state\\logs\\gateway.log", - stderrPath: "C:\\tmp\\openclaw-state\\logs\\gateway.err.log", - }), - })); - - const { buildPlatformRuntimeLogHints } = await import("./runtime-hints.js"); + beforeAll(async () => { + ({ buildPlatformRuntimeLogHints } = await import("./runtime-hints.js")); + }); + it("strips windows drive prefixes from darwin display paths", () => { expect( buildPlatformRuntimeLogHints({ platform: "darwin", diff --git a/src/daemon/schtasks-exec.test.ts b/src/daemon/schtasks-exec.test.ts index 52edb573ea7..339ec2f4fd8 100644 --- a/src/daemon/schtasks-exec.test.ts +++ b/src/daemon/schtasks-exec.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { execSchtasks } from "./schtasks-exec.js"; const runCommandWithTimeout = vi.hoisted(() => vi.fn()); @@ -6,8 +7,6 @@ vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeout(...args), })); -const { execSchtasks } = await import("./schtasks-exec.js"); - beforeEach(() => { runCommandWithTimeout.mockReset(); }); diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 0041107264a..dafa7b7e32e 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -12,6 +12,7 @@ import { splitArgsPreservingQuotes } from "./arg-split.js"; import { parseSystemdExecStart } from "./systemd-unit.js"; import { isNonFatalSystemdInstallProbeError, + isSystemdServiceEnabled, isSystemdUserServiceAvailable, parseSystemdShow, readSystemdServiceExecStart, @@ -71,7 +72,6 @@ function assertMachineUserSystemctlArgs(args: string[], user: string, ...command } async function readManagedServiceEnabled(env: NodeJS.ProcessEnv = { HOME: TEST_MANAGED_HOME }) { - const { isSystemdServiceEnabled } = await import("./systemd.js"); vi.spyOn(fs, "access").mockResolvedValue(undefined); return isSystemdServiceEnabled({ env }); } @@ -180,7 +180,6 @@ describe("isSystemdServiceEnabled", () => { }); it("returns false without calling systemctl when the managed unit file is missing", async () => { - const { isSystemdServiceEnabled } = await import("./systemd.js"); const err = new Error("missing unit") as NodeJS.ErrnoException; err.code = "ENOENT"; vi.spyOn(fs, "access").mockRejectedValueOnce(err); @@ -286,7 +285,6 @@ describe("isSystemdServiceEnabled", () => { }); it("throws when systemctl is-enabled fails for non-state errors", async () => { - const { isSystemdServiceEnabled } = await import("./systemd.js"); vi.spyOn(fs, "access").mockResolvedValue(undefined); execFileMock .mockImplementationOnce((_cmd, args, _opts, cb) => { @@ -309,7 +307,6 @@ describe("isSystemdServiceEnabled", () => { }); it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => { - const { isSystemdServiceEnabled } = await import("./systemd.js"); vi.spyOn(fs, "access").mockResolvedValue(undefined); execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => { // On Ubuntu 24.04, `systemctl --user is-enabled ` exits with diff --git a/src/entry.version-fast-path.test.ts b/src/entry.version-fast-path.test.ts index bfa81cfaa57..f779aa9b00a 100644 --- a/src/entry.version-fast-path.test.ts +++ b/src/entry.version-fast-path.test.ts @@ -98,7 +98,6 @@ describe("entry root version fast path", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); await import("./entry.js"); - await vi.waitFor(() => { expect(logSpy).toHaveBeenCalledWith("OpenClaw 9.9.9-test (abc1234)"); expect(exitSpy).toHaveBeenCalledWith(0); @@ -112,7 +111,6 @@ describe("entry root version fast path", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); await import("./entry.js"); - await vi.waitFor(() => { expect(logSpy).toHaveBeenCalledWith("OpenClaw 9.9.9-test"); expect(exitSpy).toHaveBeenCalledWith(0); @@ -126,7 +124,6 @@ describe("entry root version fast path", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); await import("./entry.js"); - await vi.waitFor(() => { expect(runCliMock).toHaveBeenCalledWith(["node", "openclaw", "--version"]); }); @@ -142,7 +139,6 @@ describe("entry root version fast path", () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); await import("./entry.js"); - await vi.waitFor(() => { expect(runCliMock).toHaveBeenCalledWith(["node", "openclaw", "--version"]); }); diff --git a/src/gateway/server-startup.test.ts b/src/gateway/server-startup.test.ts index 27ca00194ca..46f7678b316 100644 --- a/src/gateway/server-startup.test.ts +++ b/src/gateway/server-startup.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; const ensureOpenClawModelsJsonMock = vi.fn< @@ -39,14 +39,21 @@ vi.mock("../agents/pi-embedded-runner/model.js", () => ({ ) => resolveModelMock(provider, modelId, agentDir, cfg, options), })); +let prewarmConfiguredPrimaryModel: typeof import("./server-startup.js").__testing.prewarmConfiguredPrimaryModel; + describe("gateway startup primary model warmup", () => { + beforeAll(async () => { + ({ + __testing: { prewarmConfiguredPrimaryModel }, + } = await import("./server-startup.js")); + }); + beforeEach(() => { ensureOpenClawModelsJsonMock.mockClear(); resolveModelMock.mockClear(); }); it("prewarms an explicit configured primary model", async () => { - const { __testing } = await import("./server-startup.js"); const cfg = { agents: { defaults: { @@ -57,7 +64,7 @@ describe("gateway startup primary model warmup", () => { }, } as OpenClawConfig; - await __testing.prewarmConfiguredPrimaryModel({ + await prewarmConfiguredPrimaryModel({ cfg, log: { warn: vi.fn() }, }); @@ -69,9 +76,7 @@ describe("gateway startup primary model warmup", () => { }); it("skips warmup when no explicit primary model is configured", async () => { - const { __testing } = await import("./server-startup.js"); - - await __testing.prewarmConfiguredPrimaryModel({ + await prewarmConfiguredPrimaryModel({ cfg: {} as OpenClawConfig, log: { warn: vi.fn() }, }); diff --git a/src/image-generation/provider-registry.test.ts b/src/image-generation/provider-registry.test.ts index 39d8b7021a2..a7c41175aa0 100644 --- a/src/image-generation/provider-registry.test.ts +++ b/src/image-generation/provider-registry.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; const { resolveRuntimePluginRegistryMock } = vi.hoisted(() => ({ @@ -15,17 +15,16 @@ let getImageGenerationProvider: typeof import("./provider-registry.js").getImage let listImageGenerationProviders: typeof import("./provider-registry.js").listImageGenerationProviders; describe("image-generation provider registry", () => { + beforeAll(async () => { + ({ getImageGenerationProvider, listImageGenerationProviders } = + await import("./provider-registry.js")); + }); + afterEach(() => { resolveRuntimePluginRegistryMock.mockReset(); resolveRuntimePluginRegistryMock.mockReturnValue(undefined); }); - beforeEach(async () => { - vi.resetModules(); - ({ getImageGenerationProvider, listImageGenerationProviders } = - await import("./provider-registry.js")); - }); - it("does not load plugins when listing without config", () => { expect(listImageGenerationProviders()).toEqual([]); expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(); diff --git a/src/image-generation/runtime.test.ts b/src/image-generation/runtime.test.ts index a830864e597..89b42716f8d 100644 --- a/src/image-generation/runtime.test.ts +++ b/src/image-generation/runtime.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; @@ -24,20 +24,18 @@ function setCompatibleActiveImageGenerationRegistry( } describe("image-generation runtime helpers", () => { + beforeAll(async () => { + ({ generateImage, listRuntimeImageGenerationProviders } = await import("./runtime.js")); + }); + afterEach(() => { resolveRuntimePluginRegistryMock.mockReset(); resolveRuntimePluginRegistryMock.mockReturnValue(undefined); resetPluginRuntimeStateForTest(); - vi.doUnmock("../plugins/loader.js"); }); - beforeEach(async () => { - vi.resetModules(); + beforeEach(() => { resetPluginRuntimeStateForTest(); - vi.doMock("../plugins/loader.js", () => ({ - resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock, - })); - ({ generateImage, listRuntimeImageGenerationProviders } = await import("./runtime.js")); }); it("generates images through the active image-generation registry", async () => { diff --git a/src/infra/env.test.ts b/src/infra/env.test.ts index 7cfac44bb86..872cd4d7695 100644 --- a/src/infra/env.test.ts +++ b/src/infra/env.test.ts @@ -22,6 +22,7 @@ beforeEach(async () => { vi.resetModules(); ({ isTruthyEnvValue, logAcceptedEnvOption, normalizeEnv, normalizeZaiEnv } = await import("./env.js")); + loggerMocks.info.mockClear(); }); describe("normalizeZaiEnv", () => { diff --git a/src/infra/exec-approval-forwarder.test.ts b/src/infra/exec-approval-forwarder.test.ts index 3f58503bd9d..76dfcd8c612 100644 --- a/src/infra/exec-approval-forwarder.test.ts +++ b/src/infra/exec-approval-forwarder.test.ts @@ -29,6 +29,11 @@ afterEach(() => { const emptyRegistry = createTestRegistry([]); +async function flushPendingDelivery(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + function isDiscordExecApprovalClientEnabledForTest(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -368,9 +373,8 @@ describe("exec approval forwarder", () => { const { deliver, forwarder } = createForwarder({ cfg: TARGETS_CFG }); await expect(forwarder.handleRequested(baseRequest)).resolves.toBe(true); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); expect(beforeDeliverPayload).toHaveBeenCalledWith( expect.objectContaining({ hint: { kind: "approval-pending", approvalKind: "exec" }, diff --git a/src/infra/exec-approval-surface.test.ts b/src/infra/exec-approval-surface.test.ts index 8c22f543f7a..67217d62578 100644 --- a/src/infra/exec-approval-surface.test.ts +++ b/src/infra/exec-approval-surface.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const loadConfigMock = vi.hoisted(() => vi.fn()); const getChannelPluginMock = vi.hoisted(() => vi.fn()); @@ -6,47 +6,48 @@ const listChannelPluginsMock = vi.hoisted(() => vi.fn()); const isDeliverableMessageChannelMock = vi.hoisted(() => vi.fn()); const normalizeMessageChannelMock = vi.hoisted(() => vi.fn()); +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: (...args: unknown[]) => loadConfigMock(...args), + }; +}); + +vi.mock("../channels/plugins/index.js", () => ({ + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), + listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args), +})); + +vi.mock("../utils/message-channel.js", () => ({ + INTERNAL_MESSAGE_CHANNEL: "web", + isDeliverableMessageChannel: (...args: unknown[]) => isDeliverableMessageChannelMock(...args), + normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), +})); + type ExecApprovalSurfaceModule = typeof import("./exec-approval-surface.js"); let hasConfiguredExecApprovalDmRoute: ExecApprovalSurfaceModule["hasConfiguredExecApprovalDmRoute"]; let resolveExecApprovalInitiatingSurfaceState: ExecApprovalSurfaceModule["resolveExecApprovalInitiatingSurfaceState"]; -async function loadExecApprovalSurfaceModule() { - vi.resetModules(); - loadConfigMock.mockReset(); - getChannelPluginMock.mockReset(); - listChannelPluginsMock.mockReset(); - isDeliverableMessageChannelMock.mockReset(); - normalizeMessageChannelMock.mockReset(); - normalizeMessageChannelMock.mockImplementation((value?: string | null) => - typeof value === "string" ? value.trim().toLowerCase() : undefined, - ); - isDeliverableMessageChannelMock.mockImplementation( - (value?: string) => value === "slack" || value === "discord" || value === "telegram", - ); - vi.doMock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: (...args: unknown[]) => loadConfigMock(...args), - }; - }); - vi.doMock("../channels/plugins/index.js", () => ({ - getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), - listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args), - })); - vi.doMock("../utils/message-channel.js", () => ({ - INTERNAL_MESSAGE_CHANNEL: "web", - isDeliverableMessageChannel: (...args: unknown[]) => isDeliverableMessageChannelMock(...args), - normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), - })); - ({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } = - await import("./exec-approval-surface.js")); -} - describe("resolveExecApprovalInitiatingSurfaceState", () => { - beforeEach(async () => { - await loadExecApprovalSurfaceModule(); + beforeAll(async () => { + ({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } = + await import("./exec-approval-surface.js")); + }); + + beforeEach(() => { + loadConfigMock.mockReset(); + getChannelPluginMock.mockReset(); + listChannelPluginsMock.mockReset(); + isDeliverableMessageChannelMock.mockReset(); + normalizeMessageChannelMock.mockReset(); + normalizeMessageChannelMock.mockImplementation((value?: string | null) => + typeof value === "string" ? value.trim().toLowerCase() : undefined, + ); + isDeliverableMessageChannelMock.mockImplementation( + (value?: string) => value === "slack" || value === "discord" || value === "telegram", + ); }); it.each([ @@ -163,8 +164,18 @@ describe("resolveExecApprovalInitiatingSurfaceState", () => { }); describe("hasConfiguredExecApprovalDmRoute", () => { - beforeEach(async () => { - await loadExecApprovalSurfaceModule(); + beforeEach(() => { + loadConfigMock.mockReset(); + getChannelPluginMock.mockReset(); + listChannelPluginsMock.mockReset(); + isDeliverableMessageChannelMock.mockReset(); + normalizeMessageChannelMock.mockReset(); + normalizeMessageChannelMock.mockImplementation((value?: string | null) => + typeof value === "string" ? value.trim().toLowerCase() : undefined, + ); + isDeliverableMessageChannelMock.mockImplementation( + (value?: string) => value === "slack" || value === "discord" || value === "telegram", + ); }); it.each([ diff --git a/src/infra/exec-approvals-store.test.ts b/src/infra/exec-approvals-store.test.ts index 365e40b1f1d..6d07f18e647 100644 --- a/src/infra/exec-approvals-store.test.ts +++ b/src/infra/exec-approvals-store.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { makeTempDir } from "./exec-approvals-test-helpers.js"; const requestJsonlSocketMock = vi.hoisted(() => vi.fn()); @@ -26,8 +26,7 @@ let resolveExecApprovalsSocketPath: ExecApprovalsModule["resolveExecApprovalsSoc const tempDirs: string[] = []; const originalOpenClawHome = process.env.OPENCLAW_HOME; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ addAllowlistEntry, ensureExecApprovals, @@ -39,6 +38,9 @@ beforeEach(async () => { resolveExecApprovalsPath, resolveExecApprovalsSocketPath, } = await import("./exec-approvals.js")); +}); + +beforeEach(() => { requestJsonlSocketMock.mockReset(); }); diff --git a/src/infra/net/proxy-fetch.test.ts b/src/infra/net/proxy-fetch.test.ts index 7746fc45ffc..f058d521d42 100644 --- a/src/infra/net/proxy-fetch.test.ts +++ b/src/infra/net/proxy-fetch.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const PROXY_ENV_KEYS = [ "HTTPS_PROXY", @@ -75,13 +75,15 @@ function restoreProxyEnv(): void { } describe("makeProxyFetch", () => { - beforeEach(async () => { - vi.resetModules(); - vi.clearAllMocks(); + beforeAll(async () => { ({ getProxyUrlFromFetch, makeProxyFetch, PROXY_FETCH_PROXY_URL, resolveProxyFetchFromEnv } = await import("./proxy-fetch.js")); }); + beforeEach(() => { + vi.clearAllMocks(); + }); + it("uses undici fetch with ProxyAgent dispatcher", async () => { const proxyUrl = "http://proxy.test:8080"; undiciFetch.mockResolvedValue({ ok: true }); @@ -216,5 +218,4 @@ afterAll(() => { for (const id of mockedModuleIds) { vi.doUnmock(id); } - vi.resetModules(); }); diff --git a/src/infra/net/ssrf.dispatcher.test.ts b/src/infra/net/ssrf.dispatcher.test.ts index e66aca3a1e7..d88bf0429b6 100644 --- a/src/infra/net/ssrf.dispatcher.test.ts +++ b/src/infra/net/ssrf.dispatcher.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { TEST_UNDICI_RUNTIME_DEPS_KEY } from "./undici-runtime.js"; const { agentCtor, envHttpProxyAgentCtor, proxyAgentCtor } = vi.hoisted(() => ({ @@ -20,8 +20,11 @@ import type { PinnedHostname } from "./ssrf.js"; let createPinnedDispatcher: typeof import("./ssrf.js").createPinnedDispatcher; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { + ({ createPinnedDispatcher } = await import("./ssrf.js")); +}); + +beforeEach(() => { agentCtor.mockClear(); envHttpProxyAgentCtor.mockClear(); proxyAgentCtor.mockClear(); @@ -30,7 +33,6 @@ beforeEach(async () => { EnvHttpProxyAgent: envHttpProxyAgentCtor, ProxyAgent: proxyAgentCtor, }; - ({ createPinnedDispatcher } = await import("./ssrf.js")); }); afterEach(() => { diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index 6fc248d5ff3..fbdfc4fe978 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { Agent, @@ -75,14 +75,16 @@ let ensureGlobalUndiciStreamTimeouts: typeof import("./undici-global-dispatcher. let resetGlobalUndiciStreamTimeoutsForTests: typeof import("./undici-global-dispatcher.js").resetGlobalUndiciStreamTimeoutsForTests; describe("ensureGlobalUndiciStreamTimeouts", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ DEFAULT_UNDICI_STREAM_TIMEOUT_MS, ensureGlobalUndiciEnvProxyDispatcher, ensureGlobalUndiciStreamTimeouts, resetGlobalUndiciStreamTimeoutsForTests, } = await import("./undici-global-dispatcher.js")); + }); + + beforeEach(() => { vi.clearAllMocks(); resetGlobalUndiciStreamTimeoutsForTests(); setCurrentDispatcher(new Agent()); @@ -238,5 +240,4 @@ afterAll(() => { for (const id of mockedModuleIds) { vi.doUnmock(id); } - vi.resetModules(); }); diff --git a/src/infra/outbound/agent-delivery.test.ts b/src/infra/outbound/agent-delivery.test.ts index a0d6bb01b63..14369ebb3c7 100644 --- a/src/infra/outbound/agent-delivery.test.ts +++ b/src/infra/outbound/agent-delivery.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ resolveOutboundTarget: vi.fn(() => ({ ok: true as const, to: "+1999" })), @@ -73,9 +73,11 @@ import type { OpenClawConfig } from "../../config/config.js"; let resolveAgentDeliveryPlan: typeof import("./agent-delivery.js").resolveAgentDeliveryPlan; let resolveAgentOutboundTarget: typeof import("./agent-delivery.js").resolveAgentOutboundTarget; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ resolveAgentDeliveryPlan, resolveAgentOutboundTarget } = await import("./agent-delivery.js")); +}); + +beforeEach(() => { mocks.resolveOutboundTarget.mockClear(); mocks.resolveSessionDeliveryTarget.mockClear(); }); diff --git a/src/infra/outbound/channel-selection.test.ts b/src/infra/outbound/channel-selection.test.ts index 50612843616..092d9f65e5d 100644 --- a/src/infra/outbound/channel-selection.test.ts +++ b/src/infra/outbound/channel-selection.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ listChannelPlugins: vi.fn(), @@ -21,8 +21,7 @@ let listConfiguredMessageChannels: ChannelSelectionModule["listConfiguredMessage let resolveMessageChannelSelection: ChannelSelectionModule["resolveMessageChannelSelection"]; let runtimeModule: RuntimeModule; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { runtimeModule = await import("../../runtime.js"); ({ __testing, listConfiguredMessageChannels, resolveMessageChannelSelection } = await import("./channel-selection.js")); diff --git a/src/infra/outbound/identity.test.ts b/src/infra/outbound/identity.test.ts index 6b151d26f3e..9ff16fc5dbb 100644 --- a/src/infra/outbound/identity.test.ts +++ b/src/infra/outbound/identity.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveAgentIdentityMock = vi.hoisted(() => vi.fn()); const resolveAgentAvatarMock = vi.hoisted(() => vi.fn()); @@ -16,11 +16,15 @@ type IdentityModule = typeof import("./identity.js"); let normalizeOutboundIdentity: IdentityModule["normalizeOutboundIdentity"]; let resolveAgentOutboundIdentity: IdentityModule["resolveAgentOutboundIdentity"]; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ normalizeOutboundIdentity, resolveAgentOutboundIdentity } = await import("./identity.js")); }); +beforeEach(() => { + resolveAgentIdentityMock.mockReset(); + resolveAgentAvatarMock.mockReset(); +}); + describe("normalizeOutboundIdentity", () => { it.each([ { diff --git a/src/infra/outbound/message.channels.test.ts b/src/infra/outbound/message.channels.test.ts index d582740854f..46db87d9e85 100644 --- a/src/infra/outbound/message.channels.test.ts +++ b/src/infra/outbound/message.channels.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { @@ -22,9 +22,11 @@ vi.mock("../../gateway/call.js", () => ({ let sendMessage: typeof import("./message.js").sendMessage; let sendPoll: typeof import("./message.js").sendPoll; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ sendMessage, sendPoll } = await import("./message.js")); +}); + +beforeEach(() => { callGatewayMock.mockClear(); setRegistry(emptyRegistry); }); diff --git a/src/infra/outbound/message.test.ts b/src/infra/outbound/message.test.ts index d6e7e6c4141..7e8fea00c35 100644 --- a/src/infra/outbound/message.test.ts +++ b/src/infra/outbound/message.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ getChannelPlugin: vi.fn(), @@ -7,46 +7,58 @@ const mocks = vi.hoisted(() => ({ resolveRuntimePluginRegistry: vi.fn(), })); +vi.mock("../../channels/plugins/index.js", () => ({ + normalizeChannelId: (channel?: string) => channel?.trim().toLowerCase() ?? undefined, + getChannelPlugin: mocks.getChannelPlugin, + listChannelPlugins: () => [], +})); + +vi.mock("../../agents/agent-scope.js", () => ({ + resolveDefaultAgentId: () => "main", + resolveSessionAgentId: ({ + sessionKey, + }: { + sessionKey?: string; + config?: unknown; + agentId?: string; + }) => { + const match = sessionKey?.match(/^agent:([^:]+)/i); + return match?.[1] ?? "main"; + }, + resolveAgentWorkspaceDir: () => "/tmp/openclaw-test-workspace", +})); + +vi.mock("../../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: ({ config }: { config: unknown }) => ({ config, changes: [] }), +})); + +vi.mock("../../plugins/loader.js", () => ({ + resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry, +})); + +vi.mock("./targets.js", () => ({ + resolveOutboundTarget: mocks.resolveOutboundTarget, +})); + +vi.mock("./deliver.js", () => ({ + deliverOutboundPayloads: mocks.deliverOutboundPayloads, +})); + import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; let sendMessage: typeof import("./message.js").sendMessage; +let resetOutboundChannelResolutionStateForTest: typeof import("./channel-resolution.js").resetOutboundChannelResolutionStateForTest; describe("sendMessage", () => { - beforeEach(async () => { - vi.resetModules(); - vi.doMock("../../channels/plugins/index.js", () => ({ - normalizeChannelId: (channel?: string) => channel?.trim().toLowerCase() ?? undefined, - getChannelPlugin: mocks.getChannelPlugin, - listChannelPlugins: () => [], - })); - vi.doMock("../../agents/agent-scope.js", () => ({ - resolveDefaultAgentId: () => "main", - resolveSessionAgentId: ({ - sessionKey, - }: { - sessionKey?: string; - config?: unknown; - agentId?: string; - }) => { - const match = sessionKey?.match(/^agent:([^:]+)/i); - return match?.[1] ?? "main"; - }, - resolveAgentWorkspaceDir: () => "/tmp/openclaw-test-workspace", - })); - vi.doMock("../../config/plugin-auto-enable.js", () => ({ - applyPluginAutoEnable: ({ config }: { config: unknown }) => ({ config, changes: [] }), - })); - vi.doMock("../../plugins/loader.js", () => ({ - resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry, - })); - vi.doMock("./targets.js", () => ({ - resolveOutboundTarget: mocks.resolveOutboundTarget, - })); - vi.doMock("./deliver.js", () => ({ - deliverOutboundPayloads: mocks.deliverOutboundPayloads, - })); + beforeAll(async () => { + ({ sendMessage } = await import("./message.js")); + ({ resetOutboundChannelResolutionStateForTest } = await import("./channel-resolution.js")); + }); + + beforeEach(() => { setActivePluginRegistry(createTestRegistry([])); + resetOutboundChannelResolutionStateForTest(); mocks.getChannelPlugin.mockClear(); mocks.resolveOutboundTarget.mockClear(); mocks.deliverOutboundPayloads.mockClear(); @@ -57,8 +69,6 @@ describe("sendMessage", () => { }); mocks.resolveOutboundTarget.mockImplementation(({ to }: { to: string }) => ({ ok: true, to })); mocks.deliverOutboundPayloads.mockResolvedValue([{ channel: "mattermost", messageId: "m1" }]); - - ({ sendMessage } = await import("./message.js")); }); it("passes explicit agentId to outbound delivery for scoped media roots", async () => { diff --git a/src/infra/outbound/outbound-policy.test.ts b/src/infra/outbound/outbound-policy.test.ts index 555067ac5a8..33b9c9037d1 100644 --- a/src/infra/outbound/outbound-policy.test.ts +++ b/src/infra/outbound/outbound-policy.test.ts @@ -1,5 +1,5 @@ import { Container, Separator, TextDisplay } from "@buape/carbon"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { vi } from "vitest"; import type { ChannelMessageActionName } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -53,6 +53,19 @@ const mocks = vi.hoisted(() => ({ ), })); +vi.mock("./channel-adapters.js", () => ({ + getChannelMessageAdapter: mocks.getChannelMessageAdapter, +})); + +vi.mock("./target-normalization.js", () => ({ + normalizeTargetForProvider: mocks.normalizeTargetForProvider, +})); + +vi.mock("./target-resolver.js", () => ({ + formatTargetDisplay: mocks.formatTargetDisplay, + lookupDirectoryDisplay: mocks.lookupDirectoryDisplay, +})); + const slackConfig = { channels: { slack: { @@ -96,19 +109,7 @@ function expectCrossContextPolicyResult(params: { } describe("outbound policy helpers", () => { - beforeEach(async () => { - vi.resetModules(); - vi.clearAllMocks(); - vi.doMock("./channel-adapters.js", () => ({ - getChannelMessageAdapter: mocks.getChannelMessageAdapter, - })); - vi.doMock("./target-normalization.js", () => ({ - normalizeTargetForProvider: mocks.normalizeTargetForProvider, - })); - vi.doMock("./target-resolver.js", () => ({ - formatTargetDisplay: mocks.formatTargetDisplay, - lookupDirectoryDisplay: mocks.lookupDirectoryDisplay, - })); + beforeAll(async () => { ({ applyCrossContextDecoration, buildCrossContextDecoration, @@ -117,6 +118,10 @@ describe("outbound policy helpers", () => { } = await import("./outbound-policy.js")); }); + beforeEach(() => { + vi.clearAllMocks(); + }); + it.each([ { cfg: { diff --git a/src/infra/outbound/session-context.test.ts b/src/infra/outbound/session-context.test.ts index 1446d665f35..0a1d181f0dd 100644 --- a/src/infra/outbound/session-context.test.ts +++ b/src/infra/outbound/session-context.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn()); @@ -6,15 +6,18 @@ type SessionContextModule = typeof import("./session-context.js"); let buildOutboundSessionContext: SessionContextModule["buildOutboundSessionContext"]; -beforeEach(async () => { - vi.resetModules(); - resolveSessionAgentIdMock.mockReset(); - vi.doMock("../../agents/agent-scope.js", () => ({ - resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args), - })); +vi.mock("../../agents/agent-scope.js", () => ({ + resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args), +})); + +beforeAll(async () => { ({ buildOutboundSessionContext } = await import("./session-context.js")); }); +beforeEach(() => { + resolveSessionAgentIdMock.mockReset(); +}); + describe("buildOutboundSessionContext", () => { it("returns undefined when both session key and agent id are blank", () => { expect( diff --git a/src/infra/outbound/target-normalization.test.ts b/src/infra/outbound/target-normalization.test.ts index 67f502e1e7a..d8ac878492c 100644 --- a/src/infra/outbound/target-normalization.test.ts +++ b/src/infra/outbound/target-normalization.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const normalizeChannelIdMock = vi.hoisted(() => vi.fn()); const getChannelPluginMock = vi.hoisted(() => vi.fn()); @@ -9,26 +9,31 @@ type TargetNormalizationModule = typeof import("./target-normalization.js"); let buildTargetResolverSignature: TargetNormalizationModule["buildTargetResolverSignature"]; let normalizeChannelTargetInput: TargetNormalizationModule["normalizeChannelTargetInput"]; let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForProvider"]; +let resetTargetNormalizerCacheForTests: TargetNormalizationModule["__testing"]["resetTargetNormalizerCacheForTests"]; -async function loadTargetNormalizationModule() { - vi.doMock("../../channels/plugins/index.js", () => ({ - normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), - getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), - })); - vi.doMock("../../plugins/runtime.js", () => ({ - getActivePluginChannelRegistryVersion: (...args: unknown[]) => - getActivePluginChannelRegistryVersionMock(...args), - })); +vi.mock("../../channels/plugins/index.js", () => ({ + normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), + getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), +})); + +vi.mock("../../plugins/runtime.js", () => ({ + getActivePluginChannelRegistryVersion: (...args: unknown[]) => + getActivePluginChannelRegistryVersionMock(...args), +})); + +beforeAll(async () => { ({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } = await import("./target-normalization.js")); -} + ({ + __testing: { resetTargetNormalizerCacheForTests }, + } = await import("./target-normalization.js")); +}); -beforeEach(async () => { - vi.resetModules(); +beforeEach(() => { normalizeChannelIdMock.mockReset(); getChannelPluginMock.mockReset(); getActivePluginChannelRegistryVersionMock.mockReset(); - await loadTargetNormalizationModule(); + resetTargetNormalizerCacheForTests(); }); describe("normalizeChannelTargetInput", () => { diff --git a/src/infra/outbound/target-normalization.ts b/src/infra/outbound/target-normalization.ts index a183c1579f3..75f5fbc45af 100644 --- a/src/infra/outbound/target-normalization.ts +++ b/src/infra/outbound/target-normalization.ts @@ -14,6 +14,14 @@ type TargetNormalizerCacheEntry = { const targetNormalizerCacheByChannelId = new Map(); +function resetTargetNormalizerCacheForTests(): void { + targetNormalizerCacheByChannelId.clear(); +} + +export const __testing = { + resetTargetNormalizerCacheForTests, +} as const; + function resolveTargetNormalizer(channelId: ChannelId): TargetNormalizer { const version = getActivePluginChannelRegistryVersion(); const cached = targetNormalizerCacheByChannelId.get(channelId); diff --git a/src/infra/outbound/target-resolver.test.ts b/src/infra/outbound/target-resolver.test.ts index 1eac65908c7..ffd65a70ac0 100644 --- a/src/infra/outbound/target-resolver.test.ts +++ b/src/infra/outbound/target-resolver.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelDirectoryEntry } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; type TargetResolverModule = typeof import("./target-resolver.js"); @@ -17,8 +17,21 @@ const mocks = vi.hoisted(() => ({ getActivePluginChannelRegistryVersion: vi.fn(() => 1), })); -beforeEach(async () => { - vi.resetModules(); +vi.mock("../../channels/plugins/index.js", () => ({ + getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args), + normalizeChannelId: (value: string) => value, +})); + +vi.mock("../../plugins/runtime.js", () => ({ + getActivePluginChannelRegistryVersion: () => mocks.getActivePluginChannelRegistryVersion(), +})); + +beforeAll(async () => { + ({ resetDirectoryCache, resolveMessagingTarget, formatTargetDisplay } = + await import("./target-resolver.js")); +}); + +beforeEach(() => { mocks.listPeers.mockReset(); mocks.listPeersLive.mockReset(); mocks.listGroups.mockReset(); @@ -27,15 +40,7 @@ beforeEach(async () => { mocks.getChannelPlugin.mockReset(); mocks.getActivePluginChannelRegistryVersion.mockReset(); mocks.getActivePluginChannelRegistryVersion.mockReturnValue(1); - vi.doMock("../../channels/plugins/index.js", () => ({ - getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args), - normalizeChannelId: (value: string) => value, - })); - vi.doMock("../../plugins/runtime.js", () => ({ - getActivePluginChannelRegistryVersion: () => mocks.getActivePluginChannelRegistryVersion(), - })); - ({ resetDirectoryCache, resolveMessagingTarget, formatTargetDisplay } = - await import("./target-resolver.js")); + resetDirectoryCache(); }); async function expectOkResolution( diff --git a/src/infra/outbound/targets.channel-resolution.test.ts b/src/infra/outbound/targets.channel-resolution.test.ts deleted file mode 100644 index 0c9ec599a1d..00000000000 --- a/src/infra/outbound/targets.channel-resolution.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - getChannelPlugin: vi.fn(), - resolveRuntimePluginRegistry: vi.fn(), -})); - -const TEST_WORKSPACE_ROOT = "/tmp/openclaw-test-workspace"; - -function normalizeChannel(value?: string) { - return value?.trim().toLowerCase() ?? undefined; -} - -function applyPluginAutoEnableForTests(config: unknown) { - return { config, changes: [] as unknown[] }; -} - -function createTelegramPlugin() { - return { - id: "telegram", - meta: { label: "Telegram" }, - config: { - listAccountIds: () => [], - resolveAccount: () => ({}), - }, - }; -} - -vi.mock("../../channels/plugins/index.js", () => ({ - getChannelPlugin: mocks.getChannelPlugin, - normalizeChannelId: normalizeChannel, -})); - -vi.mock("../../agents/agent-scope.js", () => ({ - resolveDefaultAgentId: () => "main", - resolveAgentWorkspaceDir: () => TEST_WORKSPACE_ROOT, -})); - -vi.mock("../../plugins/loader.js", () => ({ - resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry, -})); - -vi.mock("../../config/plugin-auto-enable.js", () => ({ - applyPluginAutoEnable(args: { config: unknown }) { - return applyPluginAutoEnableForTests(args.config); - }, -})); - -let setActivePluginRegistry: typeof import("../../plugins/runtime.js").setActivePluginRegistry; -let createTestRegistry: typeof import("../../test-utils/channel-plugins.js").createTestRegistry; -let resetOutboundChannelResolutionStateForTest: typeof import("./channel-resolution.js").resetOutboundChannelResolutionStateForTest; -let resolveOutboundTarget: typeof import("./targets.js").resolveOutboundTarget; - -describe("resolveOutboundTarget channel resolution", () => { - let registrySeq = 0; - const resolveTelegramTarget = () => - resolveOutboundTarget({ - channel: "telegram", - to: "123456", - cfg: { channels: { telegram: { botToken: "test-token" } } }, - mode: "explicit", - }); - - beforeAll(async () => { - vi.resetModules(); - ({ setActivePluginRegistry } = await import("../../plugins/runtime.js")); - ({ createTestRegistry } = await import("../../test-utils/channel-plugins.js")); - ({ resetOutboundChannelResolutionStateForTest } = await import("./channel-resolution.js")); - ({ resolveOutboundTarget } = await import("./targets.js")); - }); - - beforeEach(() => { - registrySeq += 1; - resetOutboundChannelResolutionStateForTest(); - setActivePluginRegistry(createTestRegistry([]), `targets-test-${registrySeq}`); - mocks.getChannelPlugin.mockReset(); - mocks.resolveRuntimePluginRegistry.mockReset(); - }); - - it("recovers telegram plugin resolution so announce delivery does not fail with Unsupported channel: telegram", () => { - const telegramPlugin = createTelegramPlugin(); - mocks.getChannelPlugin.mockReturnValueOnce(undefined).mockReturnValueOnce(telegramPlugin); - - const result = resolveTelegramTarget(); - - expect(result).toEqual({ ok: true, to: "123456" }); - expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1); - }); - - it("retries bootstrap on subsequent resolve when the first bootstrap attempt fails", () => { - const telegramPlugin = createTelegramPlugin(); - mocks.getChannelPlugin - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(telegramPlugin) - .mockReturnValue(telegramPlugin); - mocks.resolveRuntimePluginRegistry - .mockImplementationOnce(() => { - throw new Error("bootstrap failed"); - }) - .mockImplementation(() => undefined); - - const first = resolveTelegramTarget(); - const second = resolveTelegramTarget(); - - expect(first.ok).toBe(false); - expect(second).toEqual({ ok: true, to: "123456" }); - expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(2); - }); -}); diff --git a/src/infra/pairing-token.test.ts b/src/infra/pairing-token.test.ts index 9788e448e49..b295c54c56c 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 { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const randomBytesMock = vi.hoisted(() => vi.fn()); @@ -17,12 +17,15 @@ let generatePairingToken: PairingTokenModule["generatePairingToken"]; let PAIRING_TOKEN_BYTES: PairingTokenModule["PAIRING_TOKEN_BYTES"]; let verifyPairingToken: PairingTokenModule["verifyPairingToken"]; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } = await import("./pairing-token.js")); }); +beforeEach(() => { + randomBytesMock.mockReset(); +}); + describe("generatePairingToken", () => { it("uses the configured byte count and returns a base64url token", () => { randomBytesMock.mockReturnValueOnce(Buffer.from([0xfb, 0xff, 0x00])); diff --git a/src/infra/plugin-approval-forwarder.test.ts b/src/infra/plugin-approval-forwarder.test.ts index 01a6c482d9c..79451d203a0 100644 --- a/src/infra/plugin-approval-forwarder.test.ts +++ b/src/infra/plugin-approval-forwarder.test.ts @@ -61,6 +61,11 @@ function makePluginRequest(overrides?: Partial): PluginAp }; } +async function flushPendingDelivery(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + describe("plugin approval forwarding", () => { beforeEach(() => { setActivePluginRegistry(emptyRegistry); @@ -78,10 +83,8 @@ describe("plugin approval forwarding", () => { const { forwarder } = createForwarder({ cfg: PLUGIN_TARGETS_CFG, deliver }); const result = await forwarder.handlePluginApprovalRequested!(makePluginRequest()); expect(result).toBe(true); - // Allow delivery to be async - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); const deliveryArgs = deliver.mock.calls[0]?.[0] as | { payloads?: Array<{ text?: string; interactive?: unknown }> } | undefined; @@ -123,9 +126,8 @@ describe("plugin approval forwarding", () => { const request = makePluginRequest(); request.request.severity = "critical"; await forwarder.handlePluginApprovalRequested!(request); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); const text = (deliver.mock.calls[0]?.[0] as { payloads?: Array<{ text?: string }> })?.payloads?.[0] ?.text ?? ""; @@ -159,9 +161,8 @@ describe("plugin approval forwarding", () => { const { forwarder } = createForwarder({ cfg, deliver }); const result = await forwarder.handlePluginApprovalRequested!(makePluginRequest()); expect(result).toBe(true); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); }); it("returns false when no approvals config at all", async () => { @@ -196,9 +197,8 @@ describe("plugin approval forwarding", () => { const deliver = vi.fn().mockResolvedValue([]); const { forwarder } = createForwarder({ cfg: PLUGIN_TARGETS_CFG, deliver }); await forwarder.handlePluginApprovalRequested!(makePluginRequest()); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); const deliveryArgs = deliver.mock.calls[0]?.[0] as | { payloads?: Array<{ text?: string }> } | undefined; @@ -225,9 +225,8 @@ describe("plugin approval forwarding", () => { const deliver = vi.fn().mockResolvedValue([]); const { forwarder } = createForwarder({ cfg: PLUGIN_TARGETS_CFG, deliver }); await forwarder.handlePluginApprovalRequested!(makePluginRequest()); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); expect(beforeDeliverPayload).toHaveBeenCalled(); }); @@ -256,9 +255,8 @@ describe("plugin approval forwarding", () => { // First register request so targets are tracked await forwarder.handlePluginApprovalRequested!(makePluginRequest()); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); deliver.mockClear(); const resolved: PluginApprovalResolved = { @@ -268,6 +266,7 @@ describe("plugin approval forwarding", () => { ts: 2000, }; await forwarder.handlePluginApprovalResolved!(resolved); + await flushPendingDelivery(); expect(deliver).toHaveBeenCalled(); const deliveryArgs = deliver.mock.calls[0]?.[0] as | { payloads?: Array<{ text?: string }> } @@ -283,9 +282,8 @@ describe("plugin approval forwarding", () => { // First register request so targets are tracked await forwarder.handlePluginApprovalRequested!(makePluginRequest()); - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); deliver.mockClear(); const resolved: PluginApprovalResolved = { @@ -337,10 +335,8 @@ describe("plugin approval forwarding", () => { const deliver = vi.fn().mockResolvedValue([]); const { forwarder } = createForwarder({ cfg: PLUGIN_TARGETS_CFG, deliver }); await forwarder.handlePluginApprovalRequested!(makePluginRequest()); - // Wait for the async delivery to flush before stopping - await vi.waitFor(() => { - expect(deliver).toHaveBeenCalled(); - }); + await flushPendingDelivery(); + expect(deliver).toHaveBeenCalled(); forwarder.stop(); deliver.mockClear(); // After stop, resolved should not deliver diff --git a/src/infra/ports.test.ts b/src/infra/ports.test.ts index 4c3d3597f40..cb448cb2ab5 100644 --- a/src/infra/ports.test.ts +++ b/src/infra/ports.test.ts @@ -1,5 +1,5 @@ import net from "node:net"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { stripAnsi } from "../terminal/ansi.js"; const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn()); @@ -15,12 +15,15 @@ let PortInUseError: typeof import("./ports.js").PortInUseError; const describeUnix = process.platform === "win32" ? describe.skip : describe; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ inspectPortUsage } = await import("./ports-inspect.js")); ({ ensurePortAvailable, handlePortError, PortInUseError } = await import("./ports.js")); }); +beforeEach(() => { + runCommandWithTimeoutMock.mockReset(); +}); + describe("ports helpers", () => { it("ensurePortAvailable rejects when port busy", async () => { const server = net.createServer(); @@ -66,10 +69,6 @@ describe("ports helpers", () => { }); describeUnix("inspectPortUsage", () => { - beforeEach(() => { - runCommandWithTimeoutMock.mockClear(); - }); - it("reports busy when lsof is missing but loopback listener exists", async () => { const server = net.createServer(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index 89e861e591b..db994be82c7 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveProviderUsageAuthWithPluginMock = vi.fn( async (..._args: unknown[]): Promise => null, @@ -15,11 +15,13 @@ vi.mock("../plugins/provider-runtime.js", async (importOriginal) => { let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; describe("resolveProviderAuths plugin boundary", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ resolveProviderAuths } = await import("./provider-usage.auth.js")); + }); + + beforeEach(() => { 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 72c365fdd13..a5bfa8c3e24 100644 --- a/src/infra/provider-usage.load.plugin.test.ts +++ b/src/infra/provider-usage.load.plugin.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createProviderUsageFetch } from "../test-utils/provider-usage-fetch.js"; const resolveProviderUsageSnapshotWithPluginMock = vi.fn(); @@ -17,11 +17,13 @@ let loadProviderUsageSummary: typeof import("./provider-usage.load.js").loadProv const usageNow = Date.UTC(2026, 0, 7, 0, 0, 0); describe("provider-usage.load plugin boundary", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ loadProviderUsageSummary } = await import("./provider-usage.load.js")); + }); + + beforeEach(() => { 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 4ff0823e4c3..036298b7a1e 100644 --- a/src/infra/restart-stale-pids.test.ts +++ b/src/infra/restart-stale-pids.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; // This entire file tests lsof-based Unix port polling. The feature is a deliberate // no-op on Windows (findGatewayPidsOnPortSync returns [] immediately). Running these @@ -86,13 +86,12 @@ function installInitialBusyPoll( } describe.skipIf(isWindows)("restart-stale-pids", () => { - beforeEach(() => { - vi.resetModules(); - }); - - beforeEach(async () => { + beforeAll(async () => { ({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } = await import("./restart-stale-pids.js")); + }); + + beforeEach(() => { mockSpawnSync.mockReset(); mockResolveGatewayPort.mockReset(); mockRestartWarn.mockReset(); diff --git a/src/infra/restart.test.ts b/src/infra/restart.test.ts index a47f34732ff..f2e947936c4 100644 --- a/src/infra/restart.test.ts +++ b/src/infra/restart.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const spawnSyncMock = vi.hoisted(() => vi.fn()); const resolveLsofCommandSyncMock = vi.hoisted(() => vi.fn()); @@ -30,27 +30,12 @@ let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGate let currentTimeMs = 0; -beforeEach(async () => { - vi.resetModules(); - vi.doMock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - spawnSync: (...args: Parameters) => spawnSyncMock(...args), - }; - }); - vi.doMock("./ports-lsof.js", () => ({ - resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args), - })); - vi.doMock("../config/paths.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), - }; - }); +beforeAll(async () => { ({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } = await import("./restart-stale-pids.js")); +}); + +beforeEach(() => { spawnSyncMock.mockReset(); resolveLsofCommandSyncMock.mockReset(); resolveGatewayPortMock.mockReset(); diff --git a/src/infra/secure-random.test.ts b/src/infra/secure-random.test.ts index 97bb5ed8d01..46eb3a179ac 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 { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const cryptoMocks = vi.hoisted(() => ({ randomBytes: vi.fn((bytes: number) => Buffer.alloc(bytes, 0xab)), @@ -19,8 +19,7 @@ let generateSecureInt: typeof import("./secure-random.js").generateSecureInt; let generateSecureToken: typeof import("./secure-random.js").generateSecureToken; let generateSecureUuid: typeof import("./secure-random.js").generateSecureUuid; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ generateSecureFraction, generateSecureHex, @@ -30,6 +29,11 @@ beforeEach(async () => { } = await import("./secure-random.js")); }); +beforeEach(() => { + cryptoMocks.randomBytes.mockClear(); + cryptoMocks.randomUUID.mockReset(); +}); + describe("secure-random", () => { it("delegates UUID generation to crypto.randomUUID", () => { cryptoMocks.randomUUID.mockReturnValueOnce("uuid-1").mockReturnValueOnce("uuid-2"); diff --git a/src/infra/transport-ready.test.ts b/src/infra/transport-ready.test.ts index e55dcb7dd7b..8531ce24698 100644 --- a/src/infra/transport-ready.test.ts +++ b/src/infra/transport-ready.test.ts @@ -1,50 +1,54 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const transportReadyMocks = vi.hoisted(() => ({ + injectedSleepError: null as Error | null, +})); -let injectedSleepError: Error | null = null; type TransportReadyModule = typeof import("./transport-ready.js"); let waitForTransportReady: TransportReadyModule["waitForTransportReady"]; +vi.mock("./backoff.js", () => ({ + sleepWithAbort: async (ms: number, signal?: AbortSignal) => { + if (transportReadyMocks.injectedSleepError) { + throw transportReadyMocks.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 }); + }); + }, +})); + function createRuntime() { return { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; } describe("waitForTransportReady", () => { - 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 }); - }); - }, - })); + beforeAll(async () => { ({ waitForTransportReady } = await import("./transport-ready.js")); }); + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { vi.useRealTimers(); - injectedSleepError = null; + transportReadyMocks.injectedSleepError = null; }); it("returns when the check succeeds and logs after the delay", async () => { @@ -154,7 +158,7 @@ describe("waitForTransportReady", () => { it("rethrows non-abort sleep failures", async () => { const runtime = createRuntime(); - injectedSleepError = new Error("sleep exploded"); + transportReadyMocks.injectedSleepError = new Error("sleep exploded"); await expect( waitForTransportReady({ diff --git a/src/infra/windows-task-restart.test.ts b/src/infra/windows-task-restart.test.ts index 5da5625f9b8..8db63418825 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, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { captureFullEnv } from "../test-utils/env.js"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -53,11 +53,16 @@ afterEach(() => { }); describe("relaunchGatewayScheduledTask", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ relaunchGatewayScheduledTask } = await import("./windows-task-restart.js")); }); + beforeEach(() => { + spawnMock.mockReset(); + resolvePreferredOpenClawTmpDirMock.mockReset(); + resolvePreferredOpenClawTmpDirMock.mockReturnValue(os.tmpdir()); + }); + 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 bc1aa23dad0..018863d02cd 100644 --- a/src/infra/wsl.test.ts +++ b/src/infra/wsl.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { captureEnv } from "../test-utils/env.js"; const readFileSyncMock = vi.hoisted(() => vi.fn()); @@ -32,16 +32,15 @@ function setPlatform(platform: NodeJS.Platform): void { describe("wsl detection", () => { let envSnapshot: ReturnType; + beforeAll(async () => { + ({ isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js")); + }); + beforeEach(() => { - vi.resetModules(); envSnapshot = captureEnv(["WSL_INTEROP", "WSL_DISTRO_NAME", "WSLENV"]); readFileSyncMock.mockReset(); readFileMock.mockReset(); setPlatform("linux"); - }); - - beforeEach(async () => { - ({ isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js")); resetWSLStateForTests(); }); diff --git a/src/logging/config.test.ts b/src/logging/config.test.ts index a72a6b9acce..f11c9217575 100644 --- a/src/logging/config.test.ts +++ b/src/logging/config.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { readLoggingConfig } from "./config.js"; const loadConfigMock = vi.hoisted(() => vi.fn()); @@ -24,8 +25,6 @@ describe("readLoggingConfig", () => { throw new Error("loadConfig should not be called"); }); - const { readLoggingConfig } = await import("./config.js"); - expect(readLoggingConfig()).toBeUndefined(); expect(loadConfigMock).not.toHaveBeenCalled(); }); diff --git a/src/media-understanding/runtime.test.ts b/src/media-understanding/runtime.test.ts index 1374ecdd901..a71d0db97ab 100644 --- a/src/media-understanding/runtime.test.ts +++ b/src/media-understanding/runtime.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withBundledPluginAllowlistCompat, @@ -13,11 +13,22 @@ import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; +const { resolveRuntimePluginRegistryMock } = vi.hoisted(() => ({ + resolveRuntimePluginRegistryMock: vi.fn< + (params?: unknown) => ReturnType | undefined + >(() => undefined), +})); + +vi.mock("../plugins/loader.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock, + }; +}); + let describeImageFile: typeof import("./runtime.js").describeImageFile; let runMediaUnderstandingFile: typeof import("./runtime.js").runMediaUnderstandingFile; -let resolveRuntimePluginRegistryMock: ReturnType< - typeof vi.fn<(params?: unknown) => ReturnType | undefined> ->; function setCompatibleActiveMediaUnderstandingRegistry( pluginRegistry: ReturnType, @@ -53,21 +64,13 @@ function setCompatibleActiveMediaUnderstandingRegistry( } describe("media-understanding runtime helpers", () => { + beforeAll(async () => { + ({ describeImageFile, runMediaUnderstandingFile } = await import("./runtime.js")); + }); + afterEach(() => { resolveRuntimePluginRegistryMock.mockReset(); resolveRuntimePluginRegistryMock.mockReturnValue(undefined); - vi.doUnmock("../plugins/loader.js"); - }); - - beforeEach(async () => { - vi.resetModules(); - resolveRuntimePluginRegistryMock = vi.fn< - (params?: unknown) => ReturnType | undefined - >(() => undefined); - vi.doMock("../plugins/loader.js", () => ({ - resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock, - })); - ({ describeImageFile, runMediaUnderstandingFile } = await import("./runtime.js")); }); it("describes images through the active media-understanding registry", async () => { diff --git a/src/media/server.outside-workspace.test.ts b/src/media/server.outside-workspace.test.ts index f925ca21a38..5c454d90820 100644 --- a/src/media/server.outside-workspace.test.ts +++ b/src/media/server.outside-workspace.test.ts @@ -46,7 +46,6 @@ describe("media server outside-workspace mapping", () => { beforeAll(async () => { vi.useRealTimers(); vi.doUnmock("undici"); - vi.resetModules(); const require = createRequire(import.meta.url); ({ SafeOpenError } = await import("../infra/fs-safe.js")); ({ startMediaServer } = await import("./server.js")); diff --git a/src/media/server.test.ts b/src/media/server.test.ts index d65b1ebec51..d5ebce83f49 100644 --- a/src/media/server.test.ts +++ b/src/media/server.test.ts @@ -105,7 +105,6 @@ describe("media server", () => { beforeAll(async () => { vi.useRealTimers(); vi.doUnmock("undici"); - vi.resetModules(); const require = createRequire(import.meta.url); ({ startMediaServer } = await import("./server.js")); ({ MEDIA_MAX_BYTES } = await import("./store.js")); diff --git a/src/media/store.outside-workspace.test.ts b/src/media/store.outside-workspace.test.ts index 0add32bc54e..ca00c3db3ad 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, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js"; const mocks = vi.hoisted(() => ({ @@ -32,13 +32,9 @@ describe("media store outside-workspace mapping", () => { let tempHome: TempHomeEnv; let home = ""; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ 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/pairing/setup-code.test.ts b/src/pairing/setup-code.test.ts index 4f117a6912e..99847cada12 100644 --- a/src/pairing/setup-code.test.ts +++ b/src/pairing/setup-code.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { SecretInput } from "../config/types.secrets.js"; vi.mock("../infra/device-bootstrap.js", () => ({ @@ -158,16 +158,18 @@ describe("pairing setup code", () => { } beforeEach(() => { - vi.resetModules(); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", ""); vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", ""); vi.stubEnv("OPENCLAW_GATEWAY_PORT", ""); }); - beforeEach(async () => { + beforeAll(async () => { ({ encodePairingSetupCode, resolvePairingSetupFromConfig } = await import("./setup-code.js")); ({ issueDeviceBootstrapToken: issueDeviceBootstrapTokenMock } = await import("../infra/device-bootstrap.js")); + }); + + beforeEach(() => { vi.mocked(issueDeviceBootstrapTokenMock).mockClear(); }); diff --git a/src/plugin-sdk/keyed-async-queue.test.ts b/src/plugin-sdk/keyed-async-queue.test.ts index 07caebf260a..65549175fbc 100644 --- a/src/plugin-sdk/keyed-async-queue.test.ts +++ b/src/plugin-sdk/keyed-async-queue.test.ts @@ -43,10 +43,10 @@ describe("enqueueKeyedTask", () => { }, }); - await vi.waitFor(() => { - expect(order).toContain("a1:start"); - expect(order).toContain("b1:start"); - }); + await Promise.resolve(); + await Promise.resolve(); + expect(order).toContain("a1:start"); + expect(order).toContain("b1:start"); expect(order).not.toContain("a2:start"); gate.resolve(); diff --git a/src/plugin-sdk/outbound-media.test.ts b/src/plugin-sdk/outbound-media.test.ts index b68f382cd3a..1a102e90a0f 100644 --- a/src/plugin-sdk/outbound-media.test.ts +++ b/src/plugin-sdk/outbound-media.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const loadWebMediaMock = vi.hoisted(() => vi.fn()); @@ -11,9 +11,11 @@ type OutboundMediaModule = typeof import("./outbound-media.js"); let loadOutboundMediaFromUrl: OutboundMediaModule["loadOutboundMediaFromUrl"]; describe("loadOutboundMediaFromUrl", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ loadOutboundMediaFromUrl } = await import("./outbound-media.js")); + }); + + beforeEach(() => { loadWebMediaMock.mockReset(); }); diff --git a/src/plugins/bundled-web-search.test.ts b/src/plugins/bundled-web-search.test.ts index 0620215cb95..2314d074d73 100644 --- a/src/plugins/bundled-web-search.test.ts +++ b/src/plugins/bundled-web-search.test.ts @@ -1,8 +1,12 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-web-search-ids.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; +let hasBundledWebSearchCredential: typeof import("./bundled-web-search-registry.js").hasBundledWebSearchCredential; +let listBundledWebSearchProviders: typeof import("./bundled-web-search.js").listBundledWebSearchProviders; +let resolveBundledWebSearchPluginIds: typeof import("./bundled-web-search.js").resolveBundledWebSearchPluginIds; + function resolveManifestBundledWebSearchPluginIds() { return loadPluginManifestRegistry({}) .plugins.filter( @@ -14,13 +18,18 @@ function resolveManifestBundledWebSearchPluginIds() { } async function resolveRegistryBundledWebSearchPluginIds() { - const { listBundledWebSearchProviders } = await import("./bundled-web-search.js"); return listBundledWebSearchProviders() .map(({ pluginId }) => pluginId) .filter((value, index, values) => values.indexOf(value) === index) .toSorted((left, right) => left.localeCompare(right)); } +beforeAll(async () => { + ({ listBundledWebSearchProviders, resolveBundledWebSearchPluginIds } = + await import("./bundled-web-search.js")); + ({ hasBundledWebSearchCredential } = await import("./bundled-web-search-registry.js")); +}); + function expectBundledWebSearchIds(actual: readonly string[], expected: readonly string[]) { expect(actual).toEqual(expected); } @@ -33,12 +42,7 @@ function expectBundledWebSearchAlignment(params: { } describe("bundled web search metadata", () => { - beforeEach(() => { - vi.resetModules(); - }); - it("keeps bundled web search compat ids aligned with bundled manifests", async () => { - const { resolveBundledWebSearchPluginIds } = await import("./bundled-web-search.js"); expectBundledWebSearchAlignment({ actual: resolveBundledWebSearchPluginIds({}), expected: resolveManifestBundledWebSearchPluginIds(), @@ -54,10 +58,6 @@ describe("bundled web search metadata", () => { }); describe("hasBundledWebSearchCredential", () => { - beforeEach(() => { - vi.resetModules(); - }); - const baseCfg = { agents: { defaults: { model: { primary: "ollama/mistral-8b" } } }, browser: { enabled: false }, @@ -103,7 +103,6 @@ describe("hasBundledWebSearchCredential", () => { env: { OPENROUTER_API_KEY: "sk-or-v1-test" }, }, ] as const)("$name", async ({ config, env }) => { - const { hasBundledWebSearchCredential } = await import("./bundled-web-search-registry.js"); expect(hasBundledWebSearchCredential({ config, env })).toBe(true); }); }); diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index b3116f122bc..e7c4e1c946a 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "./registry.js"; @@ -132,8 +132,11 @@ function expectCompatChainApplied(params: { } describe("resolvePluginCapabilityProviders", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ resolvePluginCapabilityProviders } = await import("./capability-provider-runtime.js")); + }); + + beforeEach(() => { mocks.resolveRuntimePluginRegistry.mockReset(); mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined); mocks.loadPluginManifestRegistry.mockReset(); @@ -144,7 +147,6 @@ describe("resolvePluginCapabilityProviders", () => { mocks.withBundledPluginEnablementCompat.mockImplementation(({ config }) => config); mocks.withBundledPluginVitestCompat.mockReset(); mocks.withBundledPluginVitestCompat.mockImplementation(({ config }) => config); - ({ resolvePluginCapabilityProviders } = await import("./capability-provider-runtime.js")); }); it("uses the active registry when capability providers are already loaded", () => { diff --git a/src/plugins/marketplace.test.ts b/src/plugins/marketplace.test.ts index 9400113270b..f0f805a4ec6 100644 --- a/src/plugins/marketplace.test.ts +++ b/src/plugins/marketplace.test.ts @@ -1,7 +1,7 @@ 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, beforeAll, describe, expect, it, vi } from "vitest"; import { withEnvAsync } from "../test-utils/env.js"; const installPluginFromPathMock = vi.fn(); @@ -20,6 +20,9 @@ const fetchWithSsrFGuardMock = vi.hoisted(() => }), ); const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn()); +let installPluginFromMarketplace: typeof import("./marketplace.js").installPluginFromMarketplace; +let listMarketplacePlugins: typeof import("./marketplace.js").listMarketplacePlugins; +let resolveMarketplaceInstallShortcut: typeof import("./marketplace.js").resolveMarketplaceInstallShortcut; vi.mock("./install.js", () => ({ installPluginFromPath: (...args: unknown[]) => installPluginFromPathMock(...args), @@ -38,6 +41,11 @@ vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), })); +beforeAll(async () => { + ({ installPluginFromMarketplace, listMarketplacePlugins, resolveMarketplaceInstallShortcut } = + await import("./marketplace.js")); +}); + async function withTempDir(fn: (dir: string) => Promise): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-marketplace-test-")); try { @@ -96,7 +104,6 @@ function mockRemoteMarketplaceClone(params: { manifest: unknown; pluginDir?: str async function expectRemoteMarketplaceError(params: { manifest: unknown; expectedError: string }) { mockRemoteMarketplaceClone({ manifest: params.manifest }); - const { listMarketplacePlugins } = await import("./marketplace.js"); const result = await listMarketplacePlugins({ marketplace: "owner/repo" }); expect(result).toEqual({ @@ -188,7 +195,6 @@ describe("marketplace plugins", () => { ], }); - const { listMarketplacePlugins } = await import("./marketplace.js"); expectMarketplaceManifestListing(await listMarketplacePlugins({ marketplace: rootDir })); }); }); @@ -216,7 +222,6 @@ describe("marketplace plugins", () => { extensions: ["index.ts"], }); - const { installPluginFromMarketplace } = await import("./marketplace.js"); const result = await installPluginFromMarketplace({ marketplace: manifestPath, plugin: "frontend-design", @@ -248,7 +253,6 @@ describe("marketplace plugins", () => { }), ); - const { resolveMarketplaceInstallShortcut } = await import("./marketplace.js"); const shortcut = await withEnvAsync( { HOME: homeDir, OPENCLAW_HOME: openClawHome }, async () => await resolveMarketplaceInstallShortcut("superpowers@claude-plugins-official"), @@ -283,7 +287,6 @@ describe("marketplace plugins", () => { extensions: ["index.ts"], }); - const { installPluginFromMarketplace } = await import("./marketplace.js"); const result = await installPluginFromMarketplace({ marketplace: "owner/repo", plugin: "frontend-design", @@ -307,7 +310,6 @@ describe("marketplace plugins", () => { ], }); - const { installPluginFromMarketplace } = await import("./marketplace.js"); const result = await installPluginFromMarketplace({ marketplace: manifestPath, plugin: "frontend-design", diff --git a/src/plugins/memory-runtime.test.ts b/src/plugins/memory-runtime.test.ts index 8867b98b835..768ed12792d 100644 --- a/src/plugins/memory-runtime.test.ts +++ b/src/plugins/memory-runtime.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveRuntimePluginRegistryMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); @@ -97,8 +97,15 @@ async function expectCloseMemoryRuntimeCase(params: { } describe("memory runtime auto-enable loading", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ + getActiveMemorySearchManager, + resolveActiveMemoryBackendConfig, + closeActiveMemorySearchManagers, + } = await import("./memory-runtime.js")); + }); + + beforeEach(() => { resolveRuntimePluginRegistryMock.mockReset(); applyPluginAutoEnableMock.mockReset(); getMemoryRuntimeMock.mockReset(); @@ -106,11 +113,6 @@ describe("memory runtime auto-enable loading", () => { config: params.config, changes: [], })); - ({ - getActiveMemorySearchManager, - resolveActiveMemoryBackendConfig, - closeActiveMemorySearchManagers, - } = await import("./memory-runtime.js")); }); it.each([ diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index afa91e93536..f8afdf769b0 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const loadOpenClawPluginsMock = vi.fn(); const loadPluginManifestRegistryMock = vi.fn(); @@ -163,8 +163,12 @@ function expectBundledProviderLoad(params?: { config?: unknown; env?: NodeJS.Pro } describe("resolvePluginProviders", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ resolveOwningPluginIdsForProvider } = await import("./providers.js")); + ({ resolvePluginProviders } = await import("./providers.runtime.js")); + }); + + beforeEach(() => { loadOpenClawPluginsMock.mockReset(); loadOpenClawPluginsMock.mockReturnValue({ providers: [{ pluginId: "google", provider: { id: "demo-provider" } }], @@ -187,8 +191,6 @@ describe("resolvePluginProviders", () => { origin: "workspace", }), ]); - ({ resolveOwningPluginIdsForProvider } = await import("./providers.js")); - ({ resolvePluginProviders } = await import("./providers.runtime.js")); }); it("forwards an explicit env to plugin loading", () => { diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index f5b6fc06e73..7bdb639c2a5 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createCompatibilityNotice, createCustomHook, @@ -214,8 +214,19 @@ function expectBundleInspectState( } describe("buildPluginStatusReport", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ + buildAllPluginInspectReports, + buildPluginCompatibilityNotices, + buildPluginCompatibilityWarnings, + buildPluginInspectReport, + buildPluginStatusReport, + formatPluginCompatibilityNotice, + summarizePluginCompatibility, + } = await import("./status.js")); + }); + + beforeEach(() => { loadConfigMock.mockReset(); loadOpenClawPluginsMock.mockReset(); applyPluginAutoEnableMock.mockReset(); @@ -235,15 +246,6 @@ describe("buildPluginStatusReport", () => { (params: { config: unknown }) => params.config, ); setPluginLoadResult({ plugins: [] }); - ({ - buildAllPluginInspectReports, - buildPluginCompatibilityNotices, - buildPluginCompatibilityWarnings, - buildPluginInspectReport, - buildPluginStatusReport, - formatPluginCompatibilityNotice, - summarizePluginCompatibility, - } = await import("./status.js")); }); it("forwards an explicit env to plugin loading", () => { diff --git a/src/process/command-queue.test.ts b/src/process/command-queue.test.ts index 6b873a3f500..119d8303ee9 100644 --- a/src/process/command-queue.test.ts +++ b/src/process/command-queue.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { importFreshModule } from "../../test/helpers/import-fresh.js"; import { CommandLane } from "./lanes.js"; @@ -56,8 +56,7 @@ function enqueueBlockedMainTask( } describe("command queue", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ clearCommandLane, CommandLaneClearedError, @@ -72,6 +71,9 @@ describe("command queue", () => { setCommandLaneConcurrency, waitForActiveTasks, } = await import("./command-queue.js")); + }); + + beforeEach(() => { resetCommandQueueStateForTest(); // Queue state is global across module instances, so reset main lane // concurrency explicitly to avoid cross-file leakage. @@ -250,9 +252,7 @@ describe("command queue", () => { await blocker; }); - await vi.waitFor(() => { - expect(getActiveTaskCount()).toBeGreaterThanOrEqual(1); - }); + expect(getActiveTaskCount()).toBeGreaterThanOrEqual(1); // Enqueue another task — it should be stuck behind the blocker let task2Ran = false; @@ -260,9 +260,7 @@ describe("command queue", () => { task2Ran = true; }); - await vi.waitFor(() => { - expect(getQueueSize(lane)).toBeGreaterThanOrEqual(2); - }); + expect(getQueueSize(lane)).toBeGreaterThanOrEqual(2); expect(task2Ran).toBe(false); // Simulate SIGUSR1: reset all lanes. Queued work (task2) should be @@ -396,10 +394,8 @@ describe("command queue", () => { return "done"; }); - await vi.waitFor(() => { - expect(commandQueueB.getQueueSize(lane)).toBe(1); - expect(commandQueueB.getActiveTaskCount()).toBe(1); - }); + expect(commandQueueB.getQueueSize(lane)).toBe(1); + expect(commandQueueB.getActiveTaskCount()).toBe(1); release(); await expect(task).resolves.toBe("done"); diff --git a/src/process/exec.no-output-timer.test.ts b/src/process/exec.no-output-timer.test.ts index 5df44a09c8b..8943c3200ee 100644 --- a/src/process/exec.no-output-timer.test.ts +++ b/src/process/exec.no-output-timer.test.ts @@ -57,6 +57,7 @@ describe("runCommandWithTimeout no-output timer", () => { beforeEach(async () => { vi.resetModules(); ({ runCommandWithTimeout } = await import("./exec.js")); + spawnMock.mockClear(); }); afterEach(() => { diff --git a/src/process/kill-tree.test.ts b/src/process/kill-tree.test.ts index 7260938b438..5178abf0faa 100644 --- a/src/process/kill-tree.test.ts +++ b/src/process/kill-tree.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), @@ -25,9 +25,11 @@ async function withPlatform(platform: NodeJS.Platform, run: () => Promise describe("killProcessTree", () => { let killSpy: ReturnType; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ killProcessTree } = await import("./kill-tree.js")); + }); + + beforeEach(() => { 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 dbb030fe86c..7680c0349fb 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, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { spawnWithFallbackMock, killProcessTreeMock } = vi.hoisted(() => ({ spawnWithFallbackMock: vi.fn(), @@ -54,9 +54,11 @@ async function createAdapterHarness(params?: { describe("createChildAdapter", () => { const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ 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 83e650c073a..32ca418b533 100644 --- a/src/process/supervisor/adapters/pty.test.ts +++ b/src/process/supervisor/adapters/pty.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { spawnMock, ptyKillMock, killProcessTreeMock } = vi.hoisted(() => ({ spawnMock: vi.fn(), @@ -39,9 +39,11 @@ function expectSpawnEnv() { describe("createPtyAdapter", () => { let createPtyAdapter: typeof import("./pty.js").createPtyAdapter; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ 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 eb3427d462f..daee348944d 100644 --- a/src/process/supervisor/supervisor.pty-command.test.ts +++ b/src/process/supervisor/supervisor.pty-command.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { createPtyAdapterMock } = vi.hoisted(() => ({ createPtyAdapterMock: vi.fn(), @@ -35,9 +35,11 @@ function createStubPtyAdapter() { describe("process supervisor PTY command contract", () => { let createProcessSupervisor: typeof import("./supervisor.js").createProcessSupervisor; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ createProcessSupervisor } = await import("./supervisor.js")); + }); + + beforeEach(() => { createPtyAdapterMock.mockClear(); }); diff --git a/src/secrets/configure.test.ts b/src/secrets/configure.test.ts index 68ab567c1f2..cad2e0ee156 100644 --- a/src/secrets/configure.test.ts +++ b/src/secrets/configure.test.ts @@ -1,11 +1,9 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const selectMock = vi.hoisted(() => vi.fn()); const createSecretsConfigIOMock = vi.hoisted(() => vi.fn()); const readJsonObjectIfExistsMock = vi.hoisted(() => vi.fn()); -const mockedModuleIds = ["@clack/prompts", "./config-io.js", "./storage-scan.js"] as const; - vi.mock("@clack/prompts", () => ({ confirm: vi.fn(), select: (...args: unknown[]) => selectMock(...args), @@ -29,13 +27,6 @@ describe("runSecretsConfigureInteractive", () => { readJsonObjectIfExistsMock.mockReset(); }); - afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); - }); - it("does not load auth-profiles when running providers-only", async () => { Object.defineProperty(process.stdin, "isTTY", { value: true, diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 4ecd1845f82..c8595e2b54c 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; @@ -12,11 +12,6 @@ const { resolveBundledPluginWebSearchProvidersMock } = vi.hoisted(() => ({ resolveBundledPluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), })); -const mockedModuleIds = [ - "../plugins/web-search-providers.js", - "../plugins/web-search-providers.runtime.js", -] as const; - let bundledWebSearchProviders: typeof import("../plugins/web-search-providers.js"); let runtimeWebSearchProviders: typeof import("../plugins/web-search-providers.runtime.js"); let secretResolve: typeof import("./resolve.js"); @@ -219,12 +214,6 @@ describe("runtime web tools resolution", () => { vi.restoreAllMocks(); }); - afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - }); - it("keeps web search disabled when search config is absent", async () => { const bundledProviderSpy = vi.mocked( bundledWebSearchProviders.resolveBundledPluginWebSearchProviders, diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 3cbca0410c0..587d82f5129 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; @@ -13,11 +13,6 @@ const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvid resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), })); -const mockedModuleIds = [ - "../plugins/web-search-providers.js", - "../plugins/web-search-providers.runtime.js", -] as const; - let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; @@ -261,13 +256,6 @@ describe("secrets runtime target coverage", () => { ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); }); - afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); - }); - it("handles every openclaw.json registry target when configured as active", async () => { const entries = listSecretTargetRegistryEntries().filter( (entry) => entry.configFile === "openclaw.json", diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index b64310d7f5e..d0fa574e5a6 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -1,7 +1,7 @@ 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 { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; @@ -14,11 +14,6 @@ const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvid resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), })); -const mockedModuleIds = [ - "../plugins/web-search-providers.js", - "../plugins/web-search-providers.runtime.js", -] as const; - vi.mock("../plugins/web-search-providers.js", () => ({ resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock, })); @@ -125,7 +120,6 @@ function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): Auth describe("secrets runtime snapshot", () => { beforeAll(async () => { - vi.resetModules(); ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); ({ activateSecretsRuntimeSnapshot, @@ -150,13 +144,6 @@ describe("secrets runtime snapshot", () => { resolvePluginWebSearchProvidersMock.mockReset(); }); - afterAll(() => { - for (const id of mockedModuleIds) { - vi.doUnmock(id); - } - vi.resetModules(); - }); - it("resolves env refs for config and auth profiles", async () => { const config = asConfig({ agents: { diff --git a/src/security/windows-acl.test.ts b/src/security/windows-acl.test.ts index 36e0b3b5e0d..fc144c6c34d 100644 --- a/src/security/windows-acl.test.ts +++ b/src/security/windows-acl.test.ts @@ -25,7 +25,6 @@ let resolveWindowsUserPrincipal: typeof import("./windows-acl.js").resolveWindow let summarizeWindowsAcl: typeof import("./windows-acl.js").summarizeWindowsAcl; beforeAll(async () => { - vi.resetModules(); ({ createIcaclsResetCommand, formatIcaclsResetCommand, diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index 23408636d2d..e6990f9cad3 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -10,6 +10,7 @@ import { withTempDir } from "../test-helpers/temp-dir.js"; import { installInMemoryTaskAndFlowRegistryRuntime } from "../test-utils/task-flow-registry-runtime.js"; import { createFlowRecord, getFlowById, resetFlowRegistryForTests } from "./flow-registry.js"; import { + cancelTaskById, createTaskRecord, findLatestTaskForSessionKey, findTaskByRunId, @@ -62,22 +63,6 @@ vi.mock("../agents/subagent-control.js", () => ({ killSubagentRunAdmin: (params: unknown) => hoisted.killSubagentRunAdminMock(params), })); -async function loadFreshTaskRegistryModulesForControlTest() { - vi.resetModules(); - vi.doMock("./task-registry-delivery-runtime.js", () => ({ - sendMessage: hoisted.sendMessageMock, - })); - vi.doMock("../acp/control-plane/manager.js", () => ({ - getAcpSessionManager: () => ({ - cancelSession: hoisted.cancelSessionMock, - }), - })); - vi.doMock("../agents/subagent-control.js", () => ({ - killSubagentRunAdmin: (params: unknown) => hoisted.killSubagentRunAdminMock(params), - })); - return await import("./task-registry.js"); -} - async function waitForAssertion(assertion: () => void, timeoutMs = 2_000, stepMs = 5) { const startedAt = Date.now(); for (;;) { @@ -1498,120 +1483,110 @@ describe("task-registry", () => { }); it("cancels ACP-backed tasks through the ACP session manager", async () => { - await withTaskRegistryTempDir(async (root) => { - const registry = await loadFreshTaskRegistryModulesForControlTest(); + await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { process.env.OPENCLAW_STATE_DIR = root; - registry.resetTaskRegistryForTests(); - try { - hoisted.cancelSessionMock.mockResolvedValue(undefined); + resetTaskRegistryForTests(); + hoisted.cancelSessionMock.mockResolvedValue(undefined); - const task = registry.createTaskRecord({ - runtime: "acp", - requesterSessionKey: "agent:main:main", - requesterOrigin: { + const task = createTaskRecord({ + runtime: "acp", + requesterSessionKey: "agent:main:main", + requesterOrigin: { + channel: "telegram", + to: "telegram:123", + }, + childSessionKey: "agent:codex:acp:child", + runId: "run-cancel-acp", + task: "Investigate issue", + status: "running", + deliveryStatus: "pending", + }); + + const result = await cancelTaskById({ + cfg: {} as never, + taskId: task.taskId, + }); + + expect(hoisted.cancelSessionMock).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + sessionKey: "agent:codex:acp:child", + reason: "task-cancel", + }), + ); + expect(result).toMatchObject({ + found: true, + cancelled: true, + task: expect.objectContaining({ + taskId: task.taskId, + status: "cancelled", + error: "Cancelled by operator.", + }), + }); + await waitForAssertion(() => + expect(hoisted.sendMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ channel: "telegram", to: "telegram:123", - }, - childSessionKey: "agent:codex:acp:child", - runId: "run-cancel-acp", - task: "Investigate issue", - status: "running", - deliveryStatus: "pending", - }); - - const result = await registry.cancelTaskById({ - cfg: {} as never, - taskId: task.taskId, - }); - - expect(hoisted.cancelSessionMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: {}, - sessionKey: "agent:codex:acp:child", - reason: "task-cancel", + content: "Background task cancelled: ACP background task (run run-canc).", }), - ); - expect(result).toMatchObject({ - found: true, - cancelled: true, - task: expect.objectContaining({ - taskId: task.taskId, - status: "cancelled", - error: "Cancelled by operator.", - }), - }); - await waitForAssertion(() => - expect(hoisted.sendMessageMock).toHaveBeenCalledWith( - expect.objectContaining({ - channel: "telegram", - to: "telegram:123", - content: "Background task cancelled: ACP background task (run run-canc).", - }), - ), - ); - } finally { - registry.resetTaskRegistryForTests(); - } + ), + ); }); }); it("cancels subagent-backed tasks through subagent control", async () => { - await withTaskRegistryTempDir(async (root) => { - const registry = await loadFreshTaskRegistryModulesForControlTest(); + await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { process.env.OPENCLAW_STATE_DIR = root; - registry.resetTaskRegistryForTests(); - try { - hoisted.killSubagentRunAdminMock.mockResolvedValue({ - found: true, - killed: true, - }); + resetTaskRegistryForTests(); + hoisted.killSubagentRunAdminMock.mockResolvedValue({ + found: true, + killed: true, + }); - const task = registry.createTaskRecord({ - runtime: "subagent", - requesterSessionKey: "agent:main:main", - requesterOrigin: { + const task = createTaskRecord({ + runtime: "subagent", + requesterSessionKey: "agent:main:main", + requesterOrigin: { + channel: "telegram", + to: "telegram:123", + }, + childSessionKey: "agent:worker:subagent:child", + runId: "run-cancel-subagent", + task: "Investigate issue", + status: "running", + deliveryStatus: "pending", + }); + + const result = await cancelTaskById({ + cfg: {} as never, + taskId: task.taskId, + }); + + expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + sessionKey: "agent:worker:subagent:child", + }), + ); + expect(result).toMatchObject({ + found: true, + cancelled: true, + task: expect.objectContaining({ + taskId: task.taskId, + status: "cancelled", + error: "Cancelled by operator.", + }), + }); + await waitForAssertion(() => + expect(hoisted.sendMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ channel: "telegram", to: "telegram:123", - }, - childSessionKey: "agent:worker:subagent:child", - runId: "run-cancel-subagent", - task: "Investigate issue", - status: "running", - deliveryStatus: "pending", - }); - - const result = await registry.cancelTaskById({ - cfg: {} as never, - taskId: task.taskId, - }); - - expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: {}, - sessionKey: "agent:worker:subagent:child", + content: "Background task cancelled: Subagent task (run run-canc).", }), - ); - expect(result).toMatchObject({ - found: true, - cancelled: true, - task: expect.objectContaining({ - taskId: task.taskId, - status: "cancelled", - error: "Cancelled by operator.", - }), - }); - await waitForAssertion(() => - expect(hoisted.sendMessageMock).toHaveBeenCalledWith( - expect.objectContaining({ - channel: "telegram", - to: "telegram:123", - content: "Background task cancelled: Subagent task (run run-canc).", - }), - ), - ); - } finally { - registry.resetTaskRegistryForTests(); - } + ), + ); }); }); }); diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 45d0091a1c5..74f4f9f22a3 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; @@ -31,10 +31,7 @@ function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlu } describe("speech provider registry", () => { - beforeEach(async () => { - vi.resetModules(); - resolveRuntimePluginRegistryMock.mockReset(); - resolveRuntimePluginRegistryMock.mockReturnValue(undefined); + beforeAll(async () => { ({ getSpeechProvider, listSpeechProviders, @@ -43,6 +40,11 @@ describe("speech provider registry", () => { } = await import("./provider-registry.js")); }); + beforeEach(() => { + resolveRuntimePluginRegistryMock.mockReset(); + resolveRuntimePluginRegistryMock.mockReturnValue(undefined); + }); + afterEach(() => {}); it("uses active plugin speech providers without reloading plugins", () => { diff --git a/src/web-search/runtime.test.ts b/src/web-search/runtime.test.ts index 901c9e67d79..f3998579b3d 100644 --- a/src/web-search/runtime.test.ts +++ b/src/web-search/runtime.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; @@ -64,15 +64,17 @@ describe("web search runtime", () => { let activateSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").activateSecretsRuntimeSnapshot; let clearSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").clearSecretsRuntimeSnapshot; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ runWebSearch } = await import("./runtime.js")); + ({ activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } = + await import("../secrets/runtime.js")); + }); + + beforeEach(() => { resolveBundledPluginWebSearchProvidersMock.mockReset(); resolveRuntimeWebSearchProvidersMock.mockReset(); resolveBundledPluginWebSearchProvidersMock.mockReturnValue([]); resolveRuntimeWebSearchProvidersMock.mockReturnValue([]); - ({ runWebSearch } = await import("./runtime.js")); - ({ activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } = - await import("../secrets/runtime.js")); }); afterEach(() => { diff --git a/test/fixtures/test-timings.unit.json b/test/fixtures/test-timings.unit.json index 3f035667171..1fb884600ab 100644 --- a/test/fixtures/test-timings.unit.json +++ b/test/fixtures/test-timings.unit.json @@ -7,10 +7,6 @@ "durationMs": 6483.10009765625, "testCount": 5 }, - "src/infra/outbound/targets.channel-resolution.test.ts": { - "durationMs": 5879.394287109375, - "testCount": 2 - }, "src/cron/service.issue-regressions.test.ts": { "durationMs": 4524.16552734375, "testCount": 39