From 4029ce738c5a8f286875fdae49a48a463548e432 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Mar 2026 19:31:02 +0000 Subject: [PATCH] test: speed up targeted unit suites --- extensions/line/src/bot-handlers.test.ts | 9 +++-- src/acp/control-plane/manager.test.ts | 14 ++++++-- src/acp/persistent-bindings.test.ts | 7 ++-- src/agents/tools/pdf-tool.test.ts | 7 +++- src/infra/outbound/deliver.test.ts | 7 ++-- src/infra/session-maintenance-warning.test.ts | 34 ++++++++++++------- src/infra/session-maintenance-warning.ts | 9 +++++ .../web-search-providers.runtime.test.ts | 25 +++++++++----- src/plugins/web-search-providers.runtime.ts | 13 ++++++- src/secrets/runtime.test.ts | 7 ++-- src/security/windows-acl.test.ts | 8 +++-- 11 files changed, 103 insertions(+), 37 deletions(-) diff --git a/extensions/line/src/bot-handlers.test.ts b/extensions/line/src/bot-handlers.test.ts index cc7a07193b3..72766b881db 100644 --- a/extensions/line/src/bot-handlers.test.ts +++ b/extensions/line/src/bot-handlers.test.ts @@ -1,6 +1,6 @@ import type { MessageEvent, PostbackEvent } from "@line/bot-sdk"; import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { LineAccountConfig } from "./types.js"; // Avoid pulling in globals/pairing/media dependencies; this suite only asserts @@ -209,8 +209,12 @@ async function startInflightReplayDuplicate(params: { } describe("handleLineWebhookEvents", () => { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); + ({ handleLineWebhookEvents, createLineWebhookReplayCache } = await import("./bot-handlers.js")); + }); + + beforeEach(() => { buildLineMessageContextMock.mockReset(); buildLineMessageContextMock.mockImplementation(async () => ({ ctxPayload: { From: "line:group:group-1" }, @@ -225,7 +229,6 @@ describe("handleLineWebhookEvents", () => { readAllowFromStoreMock.mockImplementation(async () => [] as string[]); upsertPairingRequestMock.mockReset(); upsertPairingRequestMock.mockImplementation(async () => ({ code: "CODE", created: true })); - ({ handleLineWebhookEvents, createLineWebhookReplayCache } = await import("./bot-handlers.js")); }); it("blocks group messages when groupPolicy is disabled", async () => { const processMessage = vi.fn(); diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index 75e2cfe12de..1adc0e9cd75 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -1,6 +1,6 @@ import { setTimeout as scheduleNativeTimeout } from "node:timers"; import { setTimeout as sleep } from "node:timers/promises"; -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 type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js"; import type { AcpRuntime, AcpRuntimeCapabilities } from "../runtime/types.js"; @@ -35,6 +35,7 @@ vi.mock("../runtime/registry.js", async (importOriginal) => { let AcpSessionManager: typeof import("./manager.js").AcpSessionManager; let AcpRuntimeError: typeof import("../runtime/errors.js").AcpRuntimeError; +let resetAcpSessionManagerForTests: typeof import("./manager.js").__testing.resetAcpSessionManagerForTests; const baseCfg = { acp: { @@ -149,10 +150,17 @@ function extractRuntimeOptionsFromUpserts(): Array { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); - ({ AcpSessionManager } = await import("./manager.js")); + ({ + AcpSessionManager, + __testing: { resetAcpSessionManagerForTests }, + } = await import("./manager.js")); ({ AcpRuntimeError } = await import("../runtime/errors.js")); + }); + + beforeEach(() => { + resetAcpSessionManagerForTests(); vi.useRealTimers(); hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]); hoisted.readAcpSessionEntryMock.mockReset(); diff --git a/src/acp/persistent-bindings.test.ts b/src/acp/persistent-bindings.test.ts index 27564626e2f..446c3032b1a 100644 --- a/src/acp/persistent-bindings.test.ts +++ b/src/acp/persistent-bindings.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { parseFeishuConversationId } from "../../extensions/feishu/src/conversation-id.js"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import type { ChannelConfiguredBindingProvider, ChannelPlugin } from "../channels/plugins/types.js"; @@ -311,7 +311,7 @@ function mockReadySession(params: { return sessionKey; } -beforeEach(async () => { +beforeAll(async () => { vi.resetModules(); persistentBindingsResolveModule = await import("./persistent-bindings.resolve.js"); lifecycleBindingsModule = await import("./persistent-bindings.lifecycle.js"); @@ -323,6 +323,9 @@ beforeEach(async () => { ensureConfiguredAcpBindingSession: lifecycleBindingsModule.ensureConfiguredAcpBindingSession, resetAcpSessionInPlace: lifecycleBindingsModule.resetAcpSessionInPlace, }; +}); + +beforeEach(() => { setActivePluginRegistry( createTestRegistry([ { diff --git a/src/agents/tools/pdf-tool.test.ts b/src/agents/tools/pdf-tool.test.ts index 3a73ddb79b2..f1fb4c692e3 100644 --- a/src/agents/tools/pdf-tool.test.ts +++ b/src/agents/tools/pdf-tool.test.ts @@ -16,8 +16,12 @@ const completeMock = vi.hoisted(() => vi.fn()); type PdfToolModule = typeof import("./pdf-tool.js"); let createPdfTool: PdfToolModule["createPdfTool"]; let resolvePdfModelConfigForTool: PdfToolModule["resolvePdfModelConfigForTool"]; +let pdfToolModulePromise: Promise | null = null; async function importPdfToolModule(): Promise { + if (pdfToolModulePromise) { + return await pdfToolModulePromise; + } vi.resetModules(); vi.doMock("@mariozechner/pi-ai", async (importOriginal) => { const actual = await importOriginal(); @@ -26,7 +30,8 @@ async function importPdfToolModule(): Promise { complete: completeMock, }; }); - return import("./pdf-tool.js"); + pdfToolModulePromise = import("./pdf-tool.js"); + return await pdfToolModulePromise; } async function withTempAgentDir(run: (agentDir: string) => Promise): Promise { diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index d2373480103..4e659ca3d39 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -1,5 +1,5 @@ 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 { markdownToSignalTextChunks } from "../../../extensions/signal/src/format.js"; import { signalOutbound, @@ -200,9 +200,12 @@ function expectSuccessfulWhatsAppInternalHookPayload( } describe("deliverOutboundPayloads", () => { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); ({ deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js")); + }); + + beforeEach(() => { setActivePluginRegistry(defaultRegistry); mocks.appendAssistantMessageToSessionTranscript.mockClear(); hookMocks.runner.hasHooks.mockClear(); diff --git a/src/infra/session-maintenance-warning.test.ts b/src/infra/session-maintenance-warning.test.ts index 25adad09fb8..12940213650 100644 --- a/src/infra/session-maintenance-warning.test.ts +++ b/src/infra/session-maintenance-warning.test.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ resolveSessionAgentId: vi.fn(() => "agent-from-key"), @@ -18,6 +18,7 @@ const mocks = vi.hoisted(() => ({ type SessionMaintenanceWarningModule = typeof import("./session-maintenance-warning.js"); let deliverSessionMaintenanceWarning: SessionMaintenanceWarningModule["deliverSessionMaintenanceWarning"]; +let resetSessionMaintenanceWarningForTests: SessionMaintenanceWarningModule["__testing"]["resetSessionMaintenanceWarningForTests"]; function createParams( overrides: Partial[0]> = {}, @@ -43,18 +44,8 @@ describe("deliverSessionMaintenanceWarning", () => { let prevVitest: string | undefined; let prevNodeEnv: string | undefined; - beforeEach(async () => { - prevVitest = process.env.VITEST; - prevNodeEnv = process.env.NODE_ENV; - delete process.env.VITEST; - process.env.NODE_ENV = "development"; + beforeAll(async () => { vi.resetModules(); - mocks.resolveSessionAgentId.mockClear(); - mocks.deliveryContextFromSession.mockClear(); - mocks.normalizeMessageChannel.mockClear(); - mocks.isDeliverableMessageChannel.mockClear(); - mocks.deliverOutboundPayloads.mockClear(); - mocks.enqueueSystemEvent.mockClear(); vi.doMock("../agents/agent-scope.js", () => ({ resolveSessionAgentId: mocks.resolveSessionAgentId, })); @@ -71,7 +62,24 @@ describe("deliverSessionMaintenanceWarning", () => { vi.doMock("./system-events.js", () => ({ enqueueSystemEvent: mocks.enqueueSystemEvent, })); - ({ deliverSessionMaintenanceWarning } = await import("./session-maintenance-warning.js")); + ({ + deliverSessionMaintenanceWarning, + __testing: { resetSessionMaintenanceWarningForTests }, + } = await import("./session-maintenance-warning.js")); + }); + + beforeEach(() => { + prevVitest = process.env.VITEST; + prevNodeEnv = process.env.NODE_ENV; + delete process.env.VITEST; + process.env.NODE_ENV = "development"; + resetSessionMaintenanceWarningForTests(); + mocks.resolveSessionAgentId.mockClear(); + mocks.deliveryContextFromSession.mockClear(); + mocks.normalizeMessageChannel.mockClear(); + mocks.isDeliverableMessageChannel.mockClear(); + mocks.deliverOutboundPayloads.mockClear(); + mocks.enqueueSystemEvent.mockClear(); }); afterEach(() => { diff --git a/src/infra/session-maintenance-warning.ts b/src/infra/session-maintenance-warning.ts index 048dfcd213b..84dbd8b94f2 100644 --- a/src/infra/session-maintenance-warning.ts +++ b/src/infra/session-maintenance-warning.ts @@ -18,6 +18,15 @@ const warnedContexts = new Map(); const log = createSubsystemLogger("session-maintenance-warning"); let deliverRuntimePromise: Promise | null = null; +function resetSessionMaintenanceWarningForTests() { + warnedContexts.clear(); + deliverRuntimePromise = null; +} + +export const __testing = { + resetSessionMaintenanceWarningForTests, +} as const; + function loadDeliverRuntime() { deliverRuntimePromise ??= import("./outbound/deliver-runtime.js"); return deliverRuntimePromise; diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index 022387869f2..46b5a0c9fed 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.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"; type RegistryModule = typeof import("./registry.js"); type RuntimeModule = typeof import("./runtime.js"); @@ -22,7 +22,10 @@ let loadPluginManifestRegistryMock: ReturnType; let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"]; let resolvePluginWebSearchProviders: WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"]; let resolveRuntimeWebSearchProviders: WebSearchProvidersRuntimeModule["resolveRuntimeWebSearchProviders"]; +let resetWebSearchProviderSnapshotCacheForTests: WebSearchProvidersRuntimeModule["__testing"]["resetWebSearchProviderSnapshotCacheForTests"]; let loadOpenClawPluginsMock: ReturnType; +let loaderModule: typeof import("./loader.js"); +let manifestRegistryModule: ManifestRegistryModule; function buildMockedWebSearchProviders(params?: { config?: { plugins?: Record }; @@ -73,10 +76,20 @@ function buildMockedWebSearchProviders(params?: { } describe("resolvePluginWebSearchProviders", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ createEmptyPluginRegistry } = await import("./registry.js")); - const manifestRegistryModule = await import("./manifest-registry.js"); + manifestRegistryModule = await import("./manifest-registry.js"); + loaderModule = await import("./loader.js"); + ({ setActivePluginRegistry } = await import("./runtime.js")); + ({ + resolvePluginWebSearchProviders, + resolveRuntimeWebSearchProviders, + __testing: { resetWebSearchProviderSnapshotCacheForTests }, + } = await import("./web-search-providers.runtime.js")); + }); + + beforeEach(() => { + resetWebSearchProviderSnapshotCacheForTests(); loadPluginManifestRegistryMock = vi .spyOn(manifestRegistryModule, "loadPluginManifestRegistry") .mockReturnValue({ @@ -112,7 +125,6 @@ describe("resolvePluginWebSearchProviders", () => { ) => infer R ? R : never); - const loaderModule = await import("./loader.js"); loadOpenClawPluginsMock = vi .spyOn(loaderModule, "loadOpenClawPlugins") .mockImplementation((params) => { @@ -120,9 +132,6 @@ describe("resolvePluginWebSearchProviders", () => { registry.webSearchProviders = buildMockedWebSearchProviders(params); return registry; }); - ({ setActivePluginRegistry } = await import("./runtime.js")); - ({ resolvePluginWebSearchProviders, resolveRuntimeWebSearchProviders } = - await import("./web-search-providers.runtime.js")); setActivePluginRegistry(createEmptyPluginRegistry()); vi.useRealTimers(); }); diff --git a/src/plugins/web-search-providers.runtime.ts b/src/plugins/web-search-providers.runtime.ts index 6c03c6270ba..609527d5970 100644 --- a/src/plugins/web-search-providers.runtime.ts +++ b/src/plugins/web-search-providers.runtime.ts @@ -17,11 +17,22 @@ type WebSearchProviderSnapshotCacheEntry = { expiresAt: number; providers: PluginWebSearchProviderEntry[]; }; -const webSearchProviderSnapshotCache = new WeakMap< +let webSearchProviderSnapshotCache = new WeakMap< OpenClawConfig, WeakMap> >(); +function resetWebSearchProviderSnapshotCacheForTests() { + webSearchProviderSnapshotCache = new WeakMap< + OpenClawConfig, + WeakMap> + >(); +} + +export const __testing = { + resetWebSearchProviderSnapshotCacheForTests, +} as const; + const DEFAULT_DISCOVERY_CACHE_MS = 1000; const DEFAULT_MANIFEST_CACHE_MS = 1000; diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 13ab208da68..a93c216f1a2 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, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, 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"; @@ -123,7 +123,7 @@ function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): Auth } describe("secrets runtime snapshot", () => { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); ({ clearConfigCache } = await import("../config/config.js")); ({ @@ -132,6 +132,9 @@ describe("secrets runtime snapshot", () => { getActiveRuntimeWebToolsMetadata, prepareSecretsRuntimeSnapshot, } = await import("./runtime.js")); + }); + + beforeEach(() => { resolveBundledPluginWebSearchProvidersMock.mockReset(); resolveBundledPluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders()); resolvePluginWebSearchProvidersMock.mockReset(); diff --git a/src/security/windows-acl.test.ts b/src/security/windows-acl.test.ts index dafc71a7cbb..8b058149a6c 100644 --- a/src/security/windows-acl.test.ts +++ b/src/security/windows-acl.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 { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js"; const MOCK_USERNAME = "MockUser"; @@ -24,7 +24,7 @@ let parseIcaclsOutput: typeof import("./windows-acl.js").parseIcaclsOutput; let resolveWindowsUserPrincipal: typeof import("./windows-acl.js").resolveWindowsUserPrincipal; let summarizeWindowsAcl: typeof import("./windows-acl.js").summarizeWindowsAcl; -beforeEach(async () => { +beforeAll(async () => { vi.resetModules(); ({ createIcaclsResetCommand, @@ -37,6 +37,10 @@ beforeEach(async () => { } = await import("./windows-acl.js")); }); +beforeEach(() => { + vi.unstubAllEnvs(); +}); + function aclEntry(params: { principal: string; rights?: string[];