From ffd34f8896acde42beed5e792e750b9cc5852a9d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 04:38:38 +0100 Subject: [PATCH] test: reduce agent test import churn --- src/acp/persistent-bindings.lifecycle.test.ts | 10 +- src/agents/auth-profiles.chutes.test.ts | 8 +- .../bash-tools.exec-approval-request.test.ts | 10 +- .../bash-tools.exec-host-gateway.test.ts | 10 +- src/agents/bash-tools.exec-runtime.test.ts | 45 +++---- .../bash-tools.exec.approval-id.test.ts | 23 ++-- src/agents/bash-tools.exec.path.test.ts | 30 +---- .../bash-tools.process.supervisor.test.ts | 20 ++- src/agents/bootstrap-cache.test.ts | 14 ++- src/agents/context.lookup.test.ts | 46 +++---- src/agents/live-model-switch.test.ts | 1 - ...els-config.runtime-source-snapshot.test.ts | 7 +- .../openrouter-model-capabilities.test.ts | 13 +- .../pi-model-discovery.compat.e2e.test.ts | 1 - .../pi-tools.before-tool-call.e2e.test.ts | 64 ++-------- src/agents/session-slug.test.ts | 18 ++- src/agents/simple-completion-runtime.test.ts | 10 +- src/agents/skills-install-fallback.test.ts | 27 +--- .../subagent-registry-completion.test.ts | 10 +- src/agents/tools/nodes-tool.test.ts | 34 +---- src/agents/tools/pdf-tool.test.ts | 116 +++++++----------- src/agents/tools/sessions-spawn-tool.test.ts | 23 +--- src/agents/tools/sessions.test.ts | 20 +-- src/agents/tools/web-search.redirect.test.ts | 21 ++-- src/process/exec.no-output-timer.test.ts | 7 +- 25 files changed, 225 insertions(+), 363 deletions(-) diff --git a/src/acp/persistent-bindings.lifecycle.test.ts b/src/acp/persistent-bindings.lifecycle.test.ts index 37c0702d8ed..899d64eaced 100644 --- a/src/acp/persistent-bindings.lifecycle.test.ts +++ b/src/acp/persistent-bindings.lifecycle.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 managerMocks = vi.hoisted(() => ({ @@ -40,8 +40,11 @@ const baseCfg = { let resetAcpSessionInPlace: typeof import("./persistent-bindings.lifecycle.js").resetAcpSessionInPlace; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { + ({ resetAcpSessionInPlace } = await import("./persistent-bindings.lifecycle.js")); +}); + +beforeEach(() => { managerMocks.closeSession.mockReset().mockResolvedValue({ runtimeClosed: true, metaCleared: false, @@ -50,7 +53,6 @@ beforeEach(async () => { managerMocks.updateSessionRuntimeOptions.mockReset().mockResolvedValue(undefined); sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined); resolveMocks.resolveConfiguredAcpBindingSpecBySessionKey.mockReset().mockReturnValue(null); - ({ resetAcpSessionInPlace } = await import("./persistent-bindings.lifecycle.js")); }); describe("resetAcpSessionInPlace", () => { diff --git a/src/agents/auth-profiles.chutes.test.ts b/src/agents/auth-profiles.chutes.test.ts index 7d752794e8e..8659dec93cf 100644 --- a/src/agents/auth-profiles.chutes.test.ts +++ b/src/agents/auth-profiles.chutes.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, beforeEach, describe, expect, it, vi } from "vitest"; import { withEnvAsync } from "../test-utils/env.js"; import type { AuthProfileStore } from "./auth-profiles.js"; import { CHUTES_TOKEN_ENDPOINT } from "./chutes-oauth.js"; @@ -19,11 +19,13 @@ let resetFileLockStateForTest: typeof import("../infra/file-lock.js").resetFileL describe("auth-profiles (chutes)", () => { let tempDir: string | null = null; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ clearRuntimeAuthProfileStoreSnapshots, ensureAuthProfileStore, resolveApiKeyForProfile } = await import("./auth-profiles.js")); ({ resetFileLockStateForTest } = await import("../infra/file-lock.js")); + }); + + beforeEach(() => { clearRuntimeAuthProfileStoreSnapshots(); resetFileLockStateForTest(); }); diff --git a/src/agents/bash-tools.exec-approval-request.test.ts b/src/agents/bash-tools.exec-approval-request.test.ts index ab3853c080b..7911b9bdf2b 100644 --- a/src/agents/bash-tools.exec-approval-request.test.ts +++ b/src/agents/bash-tools.exec-approval-request.test.ts @@ -12,18 +12,12 @@ let callGatewayTool: typeof import("./tools/gateway.js").callGatewayTool; let requestExecApprovalDecision: typeof import("./bash-tools.exec-approval-request.js").requestExecApprovalDecision; describe("requestExecApprovalDecision", () => { - async function loadFreshApprovalRequestModulesForTest() { - vi.resetModules(); + beforeAll(async () => { ({ callGatewayTool } = await import("./tools/gateway.js")); ({ requestExecApprovalDecision } = await import("./bash-tools.exec-approval-request.js")); - } - - beforeAll(async () => { - await loadFreshApprovalRequestModulesForTest(); }); - beforeEach(async () => { - await loadFreshApprovalRequestModulesForTest(); + beforeEach(() => { vi.mocked(callGatewayTool).mockClear(); }); diff --git a/src/agents/bash-tools.exec-host-gateway.test.ts b/src/agents/bash-tools.exec-host-gateway.test.ts index 7834d3fe7ad..22fd7e02c11 100644 --- a/src/agents/bash-tools.exec-host-gateway.test.ts +++ b/src/agents/bash-tools.exec-host-gateway.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const createAndRegisterDefaultExecApprovalRequestMock = vi.hoisted(() => vi.fn()); const buildExecApprovalPendingToolResultMock = vi.hoisted(() => vi.fn()); @@ -82,8 +82,11 @@ vi.mock("../infra/exec-obfuscation-detect.js", () => ({ let processGatewayAllowlist: typeof import("./bash-tools.exec-host-gateway.js").processGatewayAllowlist; describe("processGatewayAllowlist", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + ({ processGatewayAllowlist } = await import("./bash-tools.exec-host-gateway.js")); + }); + + beforeEach(() => { buildExecApprovalPendingToolResultMock.mockReset(); buildExecApprovalFollowupTargetMock.mockReset(); buildExecApprovalFollowupTargetMock.mockReturnValue(null); @@ -102,7 +105,6 @@ describe("processGatewayAllowlist", () => { sentApproverDms: false, unavailableReason: null, }); - ({ processGatewayAllowlist } = await import("./bash-tools.exec-host-gateway.js")); }); it("still requires approval when allowlist execution plan is unavailable despite durable trust", async () => { diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index 19e2378e578..6d606aa2bde 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -1,20 +1,33 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; const requestHeartbeatNowMock = vi.hoisted(() => vi.fn()); const enqueueSystemEventMock = vi.hoisted(() => vi.fn()); +vi.mock("../infra/heartbeat-wake.js", () => ({ + requestHeartbeatNow: requestHeartbeatNowMock, +})); + +vi.mock("../infra/system-events.js", () => ({ + enqueueSystemEvent: enqueueSystemEventMock, +})); + let buildExecExitOutcome: typeof import("./bash-tools.exec-runtime.js").buildExecExitOutcome; let detectCursorKeyMode: typeof import("./bash-tools.exec-runtime.js").detectCursorKeyMode; let emitExecSystemEvent: typeof import("./bash-tools.exec-runtime.js").emitExecSystemEvent; let formatExecFailureReason: typeof import("./bash-tools.exec-runtime.js").formatExecFailureReason; let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget; -describe("detectCursorKeyMode", () => { - beforeAll(async () => { - ({ detectCursorKeyMode } = await import("./bash-tools.exec-runtime.js")); - }); +beforeAll(async () => { + ({ + buildExecExitOutcome, + detectCursorKeyMode, + emitExecSystemEvent, + formatExecFailureReason, + resolveExecTarget, + } = await import("./bash-tools.exec-runtime.js")); +}); +describe("detectCursorKeyMode", () => { it("returns null when no toggle found", () => { expect(detectCursorKeyMode("hello world")).toBe(null); expect(detectCursorKeyMode("")).toBe(null); @@ -43,10 +56,6 @@ describe("detectCursorKeyMode", () => { }); describe("resolveExecTarget", () => { - beforeAll(async () => { - ({ resolveExecTarget } = await import("./bash-tools.exec-runtime.js")); - }); - it("keeps implicit auto on sandbox when a sandbox runtime is available", () => { expect( resolveExecTarget({ @@ -160,25 +169,9 @@ describe("resolveExecTarget", () => { }); describe("emitExecSystemEvent", () => { - beforeEach(async () => { - vi.resetModules(); + beforeEach(() => { requestHeartbeatNowMock.mockClear(); enqueueSystemEventMock.mockClear(); - vi.doMock("../infra/heartbeat-wake.js", async () => { - return await mergeMockedModule( - await vi.importActual( - "../infra/heartbeat-wake.js", - ), - () => ({ - requestHeartbeatNow: requestHeartbeatNowMock, - }), - ); - }); - vi.doMock("../infra/system-events.js", () => ({ - enqueueSystemEvent: enqueueSystemEventMock, - })); - ({ buildExecExitOutcome, emitExecSystemEvent, formatExecFailureReason } = - await import("./bash-tools.exec-runtime.js")); }); it("scopes heartbeat wake to the event session key", () => { diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index 448ca2da66a..596a6539118 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; 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, beforeEach, describe, expect, it, vi } from "vitest"; import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; import { buildSystemRunPreparePayload } from "../test-utils/system-run-prepare-payload.js"; @@ -36,15 +36,6 @@ let detectCommandObfuscation: typeof import("../infra/exec-obfuscation-detect.js let getExecApprovalApproverDmNoticeText: typeof import("../infra/exec-approval-reply.js").getExecApprovalApproverDmNoticeText; let sendMessage: typeof import("../infra/outbound/message.js").sendMessage; -async function loadExecApprovalModules() { - vi.resetModules(); - ({ callGatewayTool } = await import("./tools/gateway.js")); - ({ createExecTool } = await import("./bash-tools.exec.js")); - ({ detectCommandObfuscation } = await import("../infra/exec-obfuscation-detect.js")); - ({ getExecApprovalApproverDmNoticeText } = await import("../infra/exec-approval-reply.js")); - ({ sendMessage } = await import("../infra/outbound/message.js")); -} - function buildPreparedSystemRunPayload(rawInvokeParams: unknown) { const invoke = (rawInvokeParams ?? {}) as { params?: { @@ -224,6 +215,14 @@ describe("exec approvals", () => { let previousHome: string | undefined; let previousUserProfile: string | undefined; + beforeAll(async () => { + ({ callGatewayTool } = await import("./tools/gateway.js")); + ({ createExecTool } = await import("./bash-tools.exec.js")); + ({ detectCommandObfuscation } = await import("../infra/exec-obfuscation-detect.js")); + ({ getExecApprovalApproverDmNoticeText } = await import("../infra/exec-approval-reply.js")); + ({ sendMessage } = await import("../infra/outbound/message.js")); + }); + beforeEach(async () => { previousHome = process.env.HOME; previousUserProfile = process.env.USERPROFILE; @@ -231,7 +230,9 @@ describe("exec approvals", () => { process.env.HOME = tempDir; // Windows uses USERPROFILE for os.homedir() process.env.USERPROFILE = tempDir; - await loadExecApprovalModules(); + vi.mocked(callGatewayTool).mockReset(); + vi.mocked(detectCommandObfuscation).mockReset(); + vi.mocked(sendMessage).mockReset(); }); afterEach(() => { diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.test.ts index 7c4f0ada132..b0a80916e50 100644 --- a/src/agents/bash-tools.exec.path.test.ts +++ b/src/agents/bash-tools.exec.path.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 type { ExecApprovalsResolved } from "../infra/exec-approvals.js"; import { captureEnv } from "../test-utils/env.js"; import { sanitizeBinaryOutput } from "./shell-utils.js"; @@ -66,26 +66,6 @@ function createExecApprovals(): ExecApprovalsResolved { }; } -async function loadFreshBashExecPathModulesForTest() { - vi.resetModules(); - vi.doMock("../infra/shell-env.js", async (importOriginal) => { - const mod = await importOriginal(); - return { - ...mod, - getShellPathFromLoginShell: shellEnvMocks.getShellPathFromLoginShell, - resolveShellEnvFallbackTimeoutMs: shellEnvMocks.resolveShellEnvFallbackTimeoutMs, - }; - }); - vi.doMock("../infra/exec-approvals.js", async (importOriginal) => { - const mod = await importOriginal(); - return { ...mod, resolveExecApprovals: () => createExecApprovals() }; - }); - const bashExec = await import("./bash-tools.exec.js"); - return { - createExecTool: bashExec.createExecTool, - }; -} - const normalizeText = (value?: string) => sanitizeBinaryOutput(value ?? "") .replace(/\r\n/g, "\n") @@ -101,13 +81,16 @@ const normalizePathEntries = (value?: string) => describe("exec PATH login shell merge", () => { let envSnapshot: ReturnType; - beforeEach(async () => { + beforeAll(async () => { + ({ createExecTool } = await import("./bash-tools.exec.js")); + }); + + beforeEach(() => { envSnapshot = captureEnv(["PATH", "SHELL"]); shellEnvMocks.getShellPathFromLoginShell.mockReset(); shellEnvMocks.getShellPathFromLoginShell.mockReturnValue("/custom/bin:/opt/bin"); shellEnvMocks.resolveShellEnvFallbackTimeoutMs.mockReset(); shellEnvMocks.resolveShellEnvFallbackTimeoutMs.mockReturnValue(1234); - ({ createExecTool } = await loadFreshBashExecPathModulesForTest()); }); afterEach(() => { @@ -256,7 +239,6 @@ describe("exec host env validation", () => { const original = process.env.SSLKEYLOGFILE; process.env.SSLKEYLOGFILE = "/tmp/openclaw-ssl-keys.log"; try { - const { createExecTool } = await import("./bash-tools.exec.js"); const tool = createExecTool({ host: "gateway", security: "full", ask: "off" }); const result = await tool.execute("call1", { command: "printf '%s' \"${SSLKEYLOGFILE:-}\"", diff --git a/src/agents/bash-tools.process.supervisor.test.ts b/src/agents/bash-tools.process.supervisor.test.ts index e776233ff19..61cae6b4407 100644 --- a/src/agents/bash-tools.process.supervisor.test.ts +++ b/src/agents/bash-tools.process.supervisor.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 { supervisorMock } = vi.hoisted(() => ({ supervisorMock: { @@ -29,14 +29,6 @@ let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").re let createProcessSessionFixture: typeof import("./bash-process-registry.test-helpers.js").createProcessSessionFixture; let createProcessTool: typeof import("./bash-tools.process.js").createProcessTool; -async function loadFreshProcessToolModulesForTest() { - vi.resetModules(); - ({ addSession, getFinishedSession, getSession, resetProcessRegistryForTests } = - await import("./bash-process-registry.js")); - ({ createProcessSessionFixture } = await import("./bash-process-registry.test-helpers.js")); - ({ createProcessTool } = await import("./bash-tools.process.js")); -} - function createBackgroundSession(id: string, pid?: number) { return createProcessSessionFixture({ id, @@ -47,8 +39,14 @@ function createBackgroundSession(id: string, pid?: number) { } describe("process tool supervisor cancellation", () => { - beforeEach(async () => { - await loadFreshProcessToolModulesForTest(); + beforeAll(async () => { + ({ addSession, getFinishedSession, getSession, resetProcessRegistryForTests } = + await import("./bash-process-registry.js")); + ({ createProcessSessionFixture } = await import("./bash-process-registry.test-helpers.js")); + ({ createProcessTool } = await import("./bash-tools.process.js")); + }); + + beforeEach(() => { supervisorMock.spawn.mockClear(); supervisorMock.cancel.mockClear(); supervisorMock.cancelScope.mockClear(); diff --git a/src/agents/bootstrap-cache.test.ts b/src/agents/bootstrap-cache.test.ts index 501e1b19e59..2509d6a5277 100644 --- a/src/agents/bootstrap-cache.test.ts +++ b/src/agents/bootstrap-cache.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 { WorkspaceBootstrapFile } from "./workspace.js"; vi.mock("./workspace.js", () => ({ @@ -22,11 +22,13 @@ describe("getOrLoadBootstrapFiles", () => { const mockLoad = () => vi.mocked(workspaceModule.loadWorkspaceBootstrapFiles); - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ clearAllBootstrapSnapshots, getOrLoadBootstrapFiles } = await import("./bootstrap-cache.js")); workspaceModule = await import("./workspace.js"); + }); + + beforeEach(() => { clearAllBootstrapSnapshots(); mockLoad().mockResolvedValue(files); }); @@ -75,11 +77,13 @@ describe("clearBootstrapSnapshot", () => { const mockLoad = () => vi.mocked(workspaceModule.loadWorkspaceBootstrapFiles); - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { ({ clearAllBootstrapSnapshots, clearBootstrapSnapshot, getOrLoadBootstrapFiles } = await import("./bootstrap-cache.js")); workspaceModule = await import("./workspace.js"); + }); + + beforeEach(() => { clearAllBootstrapSnapshots(); mockLoad().mockResolvedValue([makeFile("AGENTS.md", "content")]); }); diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 8db58dce89d..ec1e156c0be 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; type DiscoveredModel = { id: string; contextWindow: number }; +type ContextModule = typeof import("./context.js"); function mockContextDeps(params: { loadConfig: () => unknown; @@ -56,23 +57,29 @@ async function flushAsyncWarmup() { await new Promise((r) => setTimeout(r, 0)); } -async function importResolveContextTokensForModel() { - const { resolveContextTokensForModel } = await import("./context.js"); +let lastContextModule: ContextModule | null = null; + +async function importContextModule(): Promise { + const module = await import("./context.js"); + lastContextModule = module; await flushAsyncWarmup(); + return module; +} + +async function importResolveContextTokensForModel() { + const { resolveContextTokensForModel } = await importContextModule(); return resolveContextTokensForModel; } describe("lookupContextTokens", () => { beforeEach(() => { vi.resetModules(); + lastContextModule = null; }); afterEach(async () => { - try { - const { resetContextWindowCacheForTest } = await import("./context.js"); - resetContextWindowCacheForTest(); - } catch { - // Ignore reset failures when a test aborts before the module loads. + if (lastContextModule) { + lastContextModule.resetContextWindowCacheForTest(); } await flushAsyncWarmup(); }); @@ -88,7 +95,7 @@ describe("lookupContextTokens", () => { }, })); - const { lookupContextTokens } = await import("./context.js"); + const { lookupContextTokens } = await importContextModule(); expect(lookupContextTokens("openrouter/claude-sonnet")).toBe(321_000); }); @@ -103,7 +110,7 @@ describe("lookupContextTokens", () => { }, })); - const { lookupContextTokens } = await import("./context.js"); + const { lookupContextTokens } = await importContextModule(); expect(lookupContextTokens("openrouter/claude-sonnet", { allowAsyncLoad: false })).toBe( 321_000, ); @@ -138,8 +145,7 @@ describe("lookupContextTokens", () => { const loadConfigMock = vi.fn(() => ({ models: {} })); const { ensureOpenClawModelsJson } = mockContextModuleDeps(loadConfigMock); process.argv = scenario.argv; - await import("./context.js"); - await flushAsyncWarmup(); + await importContextModule(); expect(loadConfigMock).toHaveBeenCalledTimes(scenario.expectedCalls); expect(ensureOpenClawModelsJson).toHaveBeenCalledTimes(scenario.expectedCalls); } @@ -168,7 +174,7 @@ describe("lookupContextTokens", () => { mockContextModuleDeps(loadConfigMock); try { - const { lookupContextTokens } = await import("./context.js"); + const { lookupContextTokens } = await importContextModule(); expect(lookupContextTokens("openrouter/claude-sonnet")).toBeUndefined(); expect(loadConfigMock).toHaveBeenCalledTimes(1); expect(lookupContextTokens("openrouter/claude-sonnet")).toBeUndefined(); @@ -187,7 +193,7 @@ describe("lookupContextTokens", () => { { id: "gemini-3.1-pro-preview", contextWindow: 128_000 }, ]); - const { lookupContextTokens } = await import("./context.js"); + const { lookupContextTokens } = await importContextModule(); lookupContextTokens("gemini-3.1-pro-preview"); await flushAsyncWarmup(); // Conservative minimum: bare-id cache feeds runtime flush/compaction paths. @@ -203,7 +209,7 @@ describe("lookupContextTokens", () => { { id: "google-gemini-cli/gemini-3.1-pro-preview", contextWindow: 1_048_576 }, ]); - const { lookupContextTokens, resolveContextTokensForModel } = await import("./context.js"); + const { lookupContextTokens, resolveContextTokensForModel } = await importContextModule(); lookupContextTokens("google-gemini-cli/gemini-3.1-pro-preview"); await flushAsyncWarmup(); @@ -257,7 +263,7 @@ describe("lookupContextTokens", () => { mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }]); const cfg = createContextOverrideConfig("google", "gemini-2.5-pro", 2_000_000); - const { lookupContextTokens, resolveContextTokensForModel } = await import("./context.js"); + const { lookupContextTokens, resolveContextTokensForModel } = await importContextModule(); lookupContextTokens("google/gemini-2.5-pro"); await flushAsyncWarmup(); @@ -292,8 +298,7 @@ describe("lookupContextTokens", () => { }, }; - const { resolveContextTokensForModel } = await import("./context.js"); - await flushAsyncWarmup(); + const { resolveContextTokensForModel } = await importContextModule(); // Exact key "bedrock" wins over the alias-normalized match "amazon-bedrock". const bedrockResult = resolveContextTokensForModel({ @@ -321,7 +326,7 @@ describe("lookupContextTokens", () => { mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }]); const cfg = createContextOverrideConfig("google", "gemini-2.5-pro", 2_000_000); - const { lookupContextTokens, resolveContextTokensForModel } = await import("./context.js"); + const { lookupContextTokens, resolveContextTokensForModel } = await importContextModule(); lookupContextTokens("google/gemini-2.5-pro"); await flushAsyncWarmup(); @@ -354,7 +359,7 @@ describe("lookupContextTokens", () => { { id: "google-gemini-cli/gemini-3.1-pro-preview", contextWindow: 1_048_576 }, ]); - const { lookupContextTokens, resolveContextTokensForModel } = await import("./context.js"); + const { lookupContextTokens, resolveContextTokensForModel } = await importContextModule(); lookupContextTokens("google-gemini-cli/gemini-3.1-pro-preview"); await flushAsyncWarmup(); @@ -371,8 +376,7 @@ describe("lookupContextTokens", () => { mockDiscoveryDeps([]); const cfg = createContextOverrideConfig("z.ai", "glm-5", 256_000); - const { resolveContextTokensForModel } = await import("./context.js"); - await flushAsyncWarmup(); + const { resolveContextTokensForModel } = await importContextModule(); const result = resolveContextTokensForModel({ cfg: cfg as never, diff --git a/src/agents/live-model-switch.test.ts b/src/agents/live-model-switch.test.ts index 7c4bbbc2224..3dbbde9070e 100644 --- a/src/agents/live-model-switch.test.ts +++ b/src/agents/live-model-switch.test.ts @@ -40,7 +40,6 @@ async function loadModule() { describe("live model switch", () => { beforeEach(() => { - vi.resetModules(); state.abortEmbeddedPiRunMock.mockReset().mockReturnValue(false); state.requestEmbeddedRunModelSwitchMock.mockReset(); state.consumeEmbeddedRunModelSwitchMock.mockReset(); diff --git a/src/agents/models-config.runtime-source-snapshot.test.ts b/src/agents/models-config.runtime-source-snapshot.test.ts index c0172215a30..0ff4143e398 100644 --- a/src/agents/models-config.runtime-source-snapshot.test.ts +++ b/src/agents/models-config.runtime-source-snapshot.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 { NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js"; import { @@ -29,8 +29,7 @@ let ensureOpenClawModelsJson: typeof import("./models-config.js").ensureOpenClaw let resetModelsJsonReadyCacheForTest: typeof import("./models-config.js").resetModelsJsonReadyCacheForTest; let readGeneratedModelsJson: typeof import("./models-config.test-utils.js").readGeneratedModelsJson; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { ({ clearConfigCache, clearRuntimeConfigSnapshot, loadConfig, setRuntimeConfigSnapshot } = await import("../config/config.js")); ({ ensureOpenClawModelsJson, resetModelsJsonReadyCacheForTest } = @@ -39,6 +38,8 @@ beforeEach(async () => { }); afterEach(() => { + clearRuntimeConfigSnapshot(); + clearConfigCache(); resetModelsJsonReadyCacheForTest(); }); diff --git a/src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts b/src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts index a2bca6a30e4..366f71350c2 100644 --- a/src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts +++ b/src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { importFreshModule } from "../../../test/helpers/import-fresh.js"; async function withOpenRouterStateDir(run: (stateDir: string) => Promise) { const stateDir = mkdtempSync(join(tmpdir(), "openclaw-openrouter-capabilities-")); @@ -13,9 +14,15 @@ async function withOpenRouterStateDir(run: (stateDir: string) => Promise) } } +async function importOpenRouterModelCapabilities(scope: string) { + return await importFreshModule( + import.meta.url, + `./openrouter-model-capabilities.js?scope=${scope}`, + ); +} + describe("openrouter-model-capabilities", () => { afterEach(() => { - vi.resetModules(); vi.unstubAllGlobals(); delete process.env.OPENCLAW_STATE_DIR; }); @@ -56,7 +63,7 @@ describe("openrouter-model-capabilities", () => { ), ); - const module = await import("./openrouter-model-capabilities.js"); + const module = await importOpenRouterModelCapabilities("top-level-max-tokens"); await module.loadOpenRouterModelCapabilities("acme/top-level-max-completion"); expect(module.getOpenRouterModelCapabilities("acme/top-level-max-completion")).toMatchObject({ @@ -97,7 +104,7 @@ describe("openrouter-model-capabilities", () => { ); vi.stubGlobal("fetch", fetchSpy); - const module = await import("./openrouter-model-capabilities.js"); + const module = await importOpenRouterModelCapabilities("awaited-miss"); await module.loadOpenRouterModelCapabilities("acme/missing-model"); expect(module.getOpenRouterModelCapabilities("acme/missing-model")).toBeUndefined(); expect(fetchSpy).toHaveBeenCalledTimes(1); diff --git a/src/agents/pi-model-discovery.compat.e2e.test.ts b/src/agents/pi-model-discovery.compat.e2e.test.ts index dcba11e7cd0..a7e18c58ad8 100644 --- a/src/agents/pi-model-discovery.compat.e2e.test.ts +++ b/src/agents/pi-model-discovery.compat.e2e.test.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; describe("pi-model-discovery module compatibility", () => { afterEach(() => { - vi.resetModules(); vi.doUnmock("@mariozechner/pi-coding-agent"); }); diff --git a/src/agents/pi-tools.before-tool-call.e2e.test.ts b/src/agents/pi-tools.before-tool-call.e2e.test.ts index 0283aa02a2b..906b7c2ac9c 100644 --- a/src/agents/pi-tools.before-tool-call.e2e.test.ts +++ b/src/agents/pi-tools.before-tool-call.e2e.test.ts @@ -6,9 +6,13 @@ import { } from "../infra/diagnostic-events.js"; import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; -import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; +import { + runBeforeToolCallHook, + wrapToolWithBeforeToolCallHook, +} from "./pi-tools.before-tool-call.js"; import { CRITICAL_THRESHOLD, GLOBAL_CIRCUIT_BREAKER_THRESHOLD } from "./tool-loop-detection.js"; import type { AnyAgentTool } from "./tools/common.js"; +import { callGatewayTool } from "./tools/gateway.js"; vi.mock("../plugins/hook-runner-global.js", async (importOriginal) => { const actual = await importOriginal(); @@ -330,26 +334,21 @@ describe("before_tool_call loop detection behavior", () => { }); describe("before_tool_call requireApproval handling", () => { - let runBeforeToolCallHook: (typeof import("./pi-tools.before-tool-call.js"))["runBeforeToolCallHook"]; let hookRunner: { hasHooks: ReturnType; runBeforeToolCall: ReturnType; }; + const mockCallGateway = vi.mocked(callGatewayTool); - beforeEach(async () => { - vi.resetModules(); - ({ runBeforeToolCallHook } = await import("./pi-tools.before-tool-call.js")); - + beforeEach(() => { resetDiagnosticSessionStateForTest(); resetDiagnosticEventsForTest(); hookRunner = { hasHooks: vi.fn().mockReturnValue(true), runBeforeToolCall: vi.fn(), }; - const { getGlobalHookRunner: currentGetGlobalHookRunner } = - await import("../plugins/hook-runner-global.js"); // oxlint-disable-next-line typescript/no-explicit-any - vi.mocked(currentGetGlobalHookRunner).mockReturnValue(hookRunner as any); + mockGetGlobalHookRunner.mockReturnValue(hookRunner as any); // Keep the global singleton aligned as a fallback in case another setup path // preloads hook-runner-global before this test's module reset/mocks take effect. const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state"); @@ -364,15 +363,10 @@ describe("before_tool_call requireApproval handling", () => { }; } hookRunnerGlobalState[hookRunnerGlobalStateKey].hookRunner = hookRunner; - // Clear gateway mock state between tests to prevent call-count leaks. - const { callGatewayTool } = await import("./tools/gateway.js"); - vi.mocked(callGatewayTool).mockReset(); + mockCallGateway.mockReset(); }); it("blocks without triggering approval when both block and requireApproval are set", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ block: true, blockReason: "Blocked by security plugin", @@ -395,9 +389,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls gateway RPC and unblocks on allow-once", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Sensitive", @@ -433,9 +424,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("blocks on deny decision", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Dangerous", @@ -457,9 +445,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("blocks on timeout with default deny behavior", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Timeout test", @@ -481,9 +466,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("allows on timeout when timeoutBehavior is allow and preserves hook params", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ params: { command: "safe-command" }, requireApproval: { @@ -509,9 +491,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("falls back to block on gateway error", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Gateway down", @@ -532,9 +511,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("blocks when gateway returns no id", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "No ID", @@ -555,8 +531,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("blocks on immediate null decision without calling waitDecision even when timeoutBehavior is allow", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -585,9 +559,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("unblocks immediately when abort signal fires during waitDecision", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Abortable", @@ -620,9 +591,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("removes abort listener after waitDecision resolves", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); - hookRunner.runBeforeToolCall.mockResolvedValue({ requireApproval: { title: "Cleanup listener", @@ -648,8 +616,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with allow-once on approval", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -673,8 +639,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("does not await onResolution before returning approval outcome", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(() => new Promise(() => {})); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -717,8 +681,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with deny on denial", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -742,8 +704,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with timeout when decision is null", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -767,8 +727,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with cancelled on gateway error", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -793,8 +751,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with cancelled when abort signal fires", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ @@ -827,8 +783,6 @@ describe("before_tool_call requireApproval handling", () => { }); it("calls onResolution with cancelled when gateway returns no id", async () => { - const { callGatewayTool } = await import("./tools/gateway.js"); - const mockCallGateway = vi.mocked(callGatewayTool); const onResolution = vi.fn(); hookRunner.runBeforeToolCall.mockResolvedValue({ diff --git a/src/agents/session-slug.test.ts b/src/agents/session-slug.test.ts index a227c0f0d67..6f29546efda 100644 --- a/src/agents/session-slug.test.ts +++ b/src/agents/session-slug.test.ts @@ -1,24 +1,22 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const randomMocks = vi.hoisted(() => ({ generateSecureInt: vi.fn(), })); +vi.mock("../infra/secure-random.js", () => ({ + generateSecureInt: randomMocks.generateSecureInt, +})); + let createSessionSlug: typeof import("./session-slug.js").createSessionSlug; -beforeEach(async () => { - vi.resetModules(); - randomMocks.generateSecureInt.mockReset(); - vi.doMock("../infra/secure-random.js", () => ({ - generateSecureInt: randomMocks.generateSecureInt, - })); +beforeAll(async () => { ({ createSessionSlug } = await import("./session-slug.js")); }); describe("session slug", () => { - afterEach(() => { - vi.doUnmock("../infra/secure-random.js"); - vi.restoreAllMocks(); + beforeEach(() => { + randomMocks.generateSecureInt.mockReset(); }); it("generates a two-word slug by default", () => { diff --git a/src/agents/simple-completion-runtime.test.ts b/src/agents/simple-completion-runtime.test.ts index 79465ba8069..dbe2de2629e 100644 --- a/src/agents/simple-completion-runtime.test.ts +++ b/src/agents/simple-completion-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 hoisted = vi.hoisted(() => ({ resolveModelMock: vi.fn(), @@ -23,8 +23,11 @@ vi.mock("./github-copilot-token.js", () => ({ let prepareSimpleCompletionModel: typeof import("./simple-completion-runtime.js").prepareSimpleCompletionModel; -beforeEach(async () => { - vi.resetModules(); +beforeAll(async () => { + ({ prepareSimpleCompletionModel } = await import("./simple-completion-runtime.js")); +}); + +beforeEach(() => { hoisted.resolveModelMock.mockReset(); hoisted.getApiKeyForModelMock.mockReset(); hoisted.applyLocalNoAuthHeaderOverrideMock.mockReset(); @@ -54,7 +57,6 @@ beforeEach(async () => { source: "cache:/tmp/copilot-token.json", baseUrl: "https://api.individual.githubcopilot.com", }); - ({ prepareSimpleCompletionModel } = await import("./simple-completion-runtime.js")); }); describe("prepareSimpleCompletionModel", () => { diff --git a/src/agents/skills-install-fallback.test.ts b/src/agents/skills-install-fallback.test.ts index 3cb86227a05..6acfa83dd70 100644 --- a/src/agents/skills-install-fallback.test.ts +++ b/src/agents/skills-install-fallback.test.ts @@ -36,28 +36,7 @@ vi.mock("../infra/brew.js", () => ({ let installSkill: typeof import("./skills-install.js").installSkill; let buildWorkspaceSkillStatus: typeof import("./skills-status.js").buildWorkspaceSkillStatus; -async function loadFreshSkillsInstallModulesForTest() { - vi.resetModules(); - vi.doMock("../process/exec.js", () => ({ - runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), - })); - vi.doMock("../infra/net/fetch-guard.js", () => ({ - fetchWithSsrFGuard: vi.fn(), - })); - vi.doMock("../security/skill-scanner.js", async (importOriginal) => ({ - ...(await importOriginal()), - scanDirectoryWithSummary: (...args: unknown[]) => scanDirectoryWithSummaryMock(...args), - })); - vi.doMock("../shared/config-eval.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - hasBinary: (bin: string) => hasBinaryMock(bin), - }; - }); - vi.doMock("../infra/brew.js", () => ({ - resolveBrewExecutable: () => undefined, - })); +async function loadSkillsInstallModulesForTest() { ({ installSkill } = await import("./skills-install.js")); ({ buildWorkspaceSkillStatus } = await import("./skills-status.js")); } @@ -121,14 +100,14 @@ describe("skills-install fallback edge cases", () => { await writeSkillWithInstaller(workspaceDir, "py-tool", "uv", { package: "example-package", }); + await loadSkillsInstallModulesForTest(); }); - beforeEach(async () => { + beforeEach(() => { runCommandWithTimeoutMock.mockClear(); scanDirectoryWithSummaryMock.mockClear(); hasBinaryMock.mockClear(); scanDirectoryWithSummaryMock.mockResolvedValue({ critical: 0, warn: 0, findings: [] }); - await loadFreshSkillsInstallModulesForTest(); }); afterAll(async () => { diff --git a/src/agents/subagent-registry-completion.test.ts b/src/agents/subagent-registry-completion.test.ts index 782cd25c928..e171f230d9d 100644 --- a/src/agents/subagent-registry-completion.test.ts +++ b/src/agents/subagent-registry-completion.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { SUBAGENT_ENDED_REASON_COMPLETE } from "./subagent-lifecycle-events.js"; import type { SubagentRunRecord } from "./subagent-registry.types.js"; @@ -40,11 +40,13 @@ describe("emitSubagentEndedHookOnce", () => { }; }; - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { + mod = await import("./subagent-registry-completion.js"); + }); + + beforeEach(() => { lifecycleMocks.getGlobalHookRunner.mockClear(); lifecycleMocks.runSubagentEnded.mockClear(); - mod = await import("./subagent-registry-completion.js"); }); it("records ended hook marker even when no subagent_ended hooks are registered", async () => { diff --git a/src/agents/tools/nodes-tool.test.ts b/src/agents/tools/nodes-tool.test.ts index f7176bf6273..26156834d73 100644 --- a/src/agents/tools/nodes-tool.test.ts +++ b/src/agents/tools/nodes-tool.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(), @@ -64,33 +64,12 @@ vi.mock("../../cli/nodes-screen.js", () => ({ let createNodesTool: typeof import("./nodes-tool.js").createNodesTool; -async function loadFreshNodesToolModuleForTest() { - vi.resetModules(); - vi.doMock("./gateway.js", () => ({ - callGatewayTool: gatewayMocks.callGatewayTool, - readGatewayCallOptions: gatewayMocks.readGatewayCallOptions, - })); - vi.doMock("./nodes-utils.js", () => ({ - resolveNodeId: nodeUtilsMocks.resolveNodeId, - resolveNode: nodeUtilsMocks.resolveNode, - })); - vi.doMock("../../cli/nodes-camera.js", () => ({ - cameraTempPath: nodesCameraMocks.cameraTempPath, - parseCameraClipPayload: nodesCameraMocks.parseCameraClipPayload, - parseCameraSnapPayload: nodesCameraMocks.parseCameraSnapPayload, - writeCameraClipPayloadToFile: nodesCameraMocks.writeCameraClipPayloadToFile, - writeCameraPayloadToFile: nodesCameraMocks.writeCameraPayloadToFile, - })); - vi.doMock("../../cli/nodes-screen.js", () => ({ - parseScreenRecordPayload: screenMocks.parseScreenRecordPayload, - screenRecordTempPath: screenMocks.screenRecordTempPath, - writeScreenRecordToFile: screenMocks.writeScreenRecordToFile, - })); - ({ createNodesTool } = await import("./nodes-tool.js")); -} - describe("createNodesTool screen_record duration guardrails", () => { - beforeEach(async () => { + beforeAll(async () => { + ({ createNodesTool } = await import("./nodes-tool.js")); + }); + + beforeEach(() => { gatewayMocks.callGatewayTool.mockReset(); gatewayMocks.readGatewayCallOptions.mockReset(); gatewayMocks.readGatewayCallOptions.mockReturnValue({}); @@ -101,7 +80,6 @@ describe("createNodesTool screen_record duration guardrails", () => { nodesCameraMocks.cameraTempPath.mockClear(); nodesCameraMocks.parseCameraSnapPayload.mockClear(); nodesCameraMocks.writeCameraPayloadToFile.mockClear(); - await loadFreshNodesToolModuleForTest(); }); it("marks nodes as owner-only", () => { diff --git a/src/agents/tools/pdf-tool.test.ts b/src/agents/tools/pdf-tool.test.ts index 481264da2ea..2ab5d5caa9d 100644 --- a/src/agents/tools/pdf-tool.test.ts +++ b/src/agents/tools/pdf-tool.test.ts @@ -1,8 +1,15 @@ 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, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import * as pdfExtractModule from "../../media/pdf-extract.js"; +import * as webMedia from "../../media/web-media.js"; +import * as modelAuth from "../model-auth.js"; +import { modelSupportsDocument } from "../model-catalog.js"; +import * as modelsConfig from "../models-config.js"; +import * as modelDiscovery from "../pi-model-discovery.js"; +import * as pdfNativeProviders from "./pdf-native-providers.js"; import { coercePdfAssistantText, coercePdfModelConfig, @@ -13,26 +20,21 @@ import { const completeMock = vi.hoisted(() => vi.fn()); +vi.mock("@mariozechner/pi-ai", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + complete: completeMock, + }; +}); + 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(); - return { - ...actual, - complete: completeMock, - }; - }); - pdfToolModulePromise = import("./pdf-tool.js"); - return await pdfToolModulePromise; -} +beforeAll(async () => { + ({ createPdfTool, resolvePdfModelConfigForTool } = await import("./pdf-tool.js")); +}); async function withTempAgentDir(run: (agentDir: string) => Promise): Promise { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pdf-")); @@ -145,10 +147,8 @@ async function stubPdfToolInfra( modelFound?: boolean; }, ) { - const webMedia = await import("../../media/web-media.js"); const loadSpy = vi.spyOn(webMedia, "loadWebMediaRaw").mockResolvedValue(FAKE_PDF_MEDIA as never); - const modelDiscovery = await import("../pi-model-discovery.js"); vi.spyOn(modelDiscovery, "discoverAuthStorage").mockReturnValue({ setRuntimeApiKey: vi.fn(), } as never); @@ -163,13 +163,11 @@ async function stubPdfToolInfra( }) as never; vi.spyOn(modelDiscovery, "discoverModels").mockReturnValue({ find } as never); - const modelsConfig = await import("../models-config.js"); vi.spyOn(modelsConfig, "ensureOpenClawModelsJson").mockResolvedValue({ agentDir, wrote: false, }); - const modelAuth = await import("../model-auth.js"); vi.spyOn(modelAuth, "getApiKeyForModel").mockResolvedValue({ apiKey: "test-key" } as never); // pragma: allowlist secret vi.spyOn(modelAuth, "requireApiKey").mockReturnValue("test-key"); @@ -256,10 +254,9 @@ describe("providerSupportsNativePdf", () => { describe("resolvePdfModelConfigForTool", () => { const priorFetch = global.fetch; - beforeEach(async () => { + beforeEach(() => { resetAuthEnv(); completeMock.mockReset(); - ({ resolvePdfModelConfigForTool } = await importPdfToolModule()); }); afterEach(() => { @@ -337,10 +334,9 @@ describe("resolvePdfModelConfigForTool", () => { describe("createPdfTool", () => { const priorFetch = global.fetch; - beforeEach(async () => { + beforeEach(() => { resetAuthEnv(); completeMock.mockReset(); - ({ createPdfTool } = await importPdfToolModule()); }); afterEach(() => { @@ -454,11 +450,9 @@ describe("createPdfTool", () => { await withTempAgentDir(async (agentDir) => { await stubPdfToolInfra(agentDir, { provider: "anthropic", input: ["text", "document"] }); - const nativeProviders = await import("./pdf-native-providers.js"); - vi.spyOn(nativeProviders, "anthropicAnalyzePdf").mockResolvedValue("native summary"); + vi.spyOn(pdfNativeProviders, "anthropicAnalyzePdf").mockResolvedValue("native summary"); - const extractModule = await import("../../media/pdf-extract.js"); - const extractSpy = vi.spyOn(extractModule, "extractPdfContent"); + const extractSpy = vi.spyOn(pdfExtractModule, "extractPdfContent"); const cfg = withPdfModel(ANTHROPIC_PDF_MODEL); const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir })); @@ -496,8 +490,7 @@ describe("createPdfTool", () => { await withTempAgentDir(async (agentDir) => { await stubPdfToolInfra(agentDir, { provider: "openai", input: ["text"] }); - const extractModule = await import("../../media/pdf-extract.js"); - const extractSpy = vi.spyOn(extractModule, "extractPdfContent").mockResolvedValue({ + const extractSpy = vi.spyOn(pdfExtractModule, "extractPdfContent").mockResolvedValue({ text: "Extracted content", images: [], }); @@ -558,7 +551,6 @@ describe("native PDF provider API calls", () => { }); it("anthropicAnalyzePdf sends correct request shape", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -566,7 +558,7 @@ describe("native PDF provider API calls", () => { }), }); - const result = await anthropicAnalyzePdf({ + const result = await pdfNativeProviders.anthropicAnalyzePdf({ ...makeAnthropicAnalyzeParams({ modelId: "claude-opus-4-6", prompt: "Summarize this document", @@ -587,7 +579,6 @@ describe("native PDF provider API calls", () => { }); it("anthropicAnalyzePdf throws on API error", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); mockFetchResponse({ ok: false, status: 400, @@ -595,13 +586,12 @@ describe("native PDF provider API calls", () => { text: async () => "invalid request", }); - await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams())).rejects.toThrow( - "Anthropic PDF request failed", - ); + await expect( + pdfNativeProviders.anthropicAnalyzePdf(makeAnthropicAnalyzeParams()), + ).rejects.toThrow("Anthropic PDF request failed"); }); it("anthropicAnalyzePdf throws when response has no text", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); mockFetchResponse({ ok: true, json: async () => ({ @@ -609,13 +599,12 @@ describe("native PDF provider API calls", () => { }), }); - await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams())).rejects.toThrow( - "Anthropic PDF returned no text", - ); + await expect( + pdfNativeProviders.anthropicAnalyzePdf(makeAnthropicAnalyzeParams()), + ).rejects.toThrow("Anthropic PDF returned no text"); }); it("geminiAnalyzePdf sends correct request shape", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -627,7 +616,7 @@ describe("native PDF provider API calls", () => { }), }); - const result = await geminiAnalyzePdf({ + const result = await pdfNativeProviders.geminiAnalyzePdf({ ...makeGeminiAnalyzeParams({ modelId: "gemini-2.5-pro", prompt: "Summarize this", @@ -646,7 +635,6 @@ describe("native PDF provider API calls", () => { }); it("geminiAnalyzePdf throws on API error", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); mockFetchResponse({ ok: false, status: 500, @@ -654,25 +642,23 @@ describe("native PDF provider API calls", () => { text: async () => "server error", }); - await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow( + await expect(pdfNativeProviders.geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow( "Gemini PDF request failed", ); }); it("geminiAnalyzePdf throws when no candidates returned", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); mockFetchResponse({ ok: true, json: async () => ({ candidates: [] }), }); - await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow( + await expect(pdfNativeProviders.geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow( "Gemini PDF returned no candidates", ); }); it("anthropicAnalyzePdf supports multiple PDFs", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -680,7 +666,7 @@ describe("native PDF provider API calls", () => { }), }); - await anthropicAnalyzePdf({ + await pdfNativeProviders.anthropicAnalyzePdf({ ...makeAnthropicAnalyzeParams({ modelId: "claude-opus-4-6", prompt: "Compare these documents", @@ -700,7 +686,6 @@ describe("native PDF provider API calls", () => { }); it("anthropicAnalyzePdf uses custom base URL", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -708,7 +693,7 @@ describe("native PDF provider API calls", () => { }), }); - await anthropicAnalyzePdf({ + await pdfNativeProviders.anthropicAnalyzePdf({ ...makeAnthropicAnalyzeParams({ baseUrl: "https://custom.example.com" }), }); @@ -716,21 +701,18 @@ describe("native PDF provider API calls", () => { }); it("anthropicAnalyzePdf requires apiKey", async () => { - const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js"); - await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams({ apiKey: "" }))).rejects.toThrow( - "apiKey required", - ); + await expect( + pdfNativeProviders.anthropicAnalyzePdf(makeAnthropicAnalyzeParams({ apiKey: "" })), + ).rejects.toThrow("apiKey required"); }); it("geminiAnalyzePdf requires apiKey", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); - await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams({ apiKey: "" }))).rejects.toThrow( - "apiKey required", - ); + await expect( + pdfNativeProviders.geminiAnalyzePdf(makeGeminiAnalyzeParams({ apiKey: "" })), + ).rejects.toThrow("apiKey required"); }); it("geminiAnalyzePdf does not duplicate /v1beta when baseUrl already includes it", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -738,7 +720,7 @@ describe("native PDF provider API calls", () => { }), }); - await geminiAnalyzePdf( + await pdfNativeProviders.geminiAnalyzePdf( makeGeminiAnalyzeParams({ baseUrl: "https://generativelanguage.googleapis.com/v1beta", }), @@ -750,7 +732,6 @@ describe("native PDF provider API calls", () => { }); it("geminiAnalyzePdf normalizes bare Google API hosts to a single /v1beta root", async () => { - const { geminiAnalyzePdf } = await import("./pdf-native-providers.js"); const fetchMock = mockFetchResponse({ ok: true, json: async () => ({ @@ -758,7 +739,7 @@ describe("native PDF provider API calls", () => { }), }); - await geminiAnalyzePdf( + await pdfNativeProviders.geminiAnalyzePdf( makeGeminiAnalyzeParams({ baseUrl: "https://generativelanguage.googleapis.com", }), @@ -832,8 +813,7 @@ describe("pdf-tool.helpers", () => { // --------------------------------------------------------------------------- describe("model catalog document support", () => { - it("modelSupportsDocument returns true when input includes document", async () => { - const { modelSupportsDocument } = await import("../model-catalog.js"); + it("modelSupportsDocument returns true when input includes document", () => { expect( modelSupportsDocument({ id: "test", @@ -844,8 +824,7 @@ describe("model catalog document support", () => { ).toBe(true); }); - it("modelSupportsDocument returns false when input lacks document", async () => { - const { modelSupportsDocument } = await import("../model-catalog.js"); + it("modelSupportsDocument returns false when input lacks document", () => { expect( modelSupportsDocument({ id: "test", @@ -856,8 +835,7 @@ describe("model catalog document support", () => { ).toBe(false); }); - it("modelSupportsDocument returns false for undefined entry", async () => { - const { modelSupportsDocument } = await import("../model-catalog.js"); + it("modelSupportsDocument returns false for undefined entry", () => { expect(modelSupportsDocument(undefined)).toBe(false); }); }); diff --git a/src/agents/tools/sessions-spawn-tool.test.ts b/src/agents/tools/sessions-spawn-tool.test.ts index 2eb251e2cf4..4daa2f155a9 100644 --- a/src/agents/tools/sessions-spawn-tool.test.ts +++ b/src/agents/tools/sessions-spawn-tool.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(() => { const spawnSubagentDirectMock = vi.fn(); @@ -22,22 +22,12 @@ vi.mock("../acp-spawn.js", () => ({ let createSessionsSpawnTool: typeof import("./sessions-spawn-tool.js").createSessionsSpawnTool; -async function loadFreshSessionsSpawnToolModuleForTest() { - vi.resetModules(); - vi.doMock("../subagent-spawn.js", () => ({ - SUBAGENT_SPAWN_MODES: ["run", "session"], - spawnSubagentDirect: (...args: unknown[]) => hoisted.spawnSubagentDirectMock(...args), - })); - vi.doMock("../acp-spawn.js", () => ({ - ACP_SPAWN_MODES: ["run", "session"], - ACP_SPAWN_STREAM_TARGETS: ["parent"], - spawnAcpDirect: (...args: unknown[]) => hoisted.spawnAcpDirectMock(...args), - })); - ({ createSessionsSpawnTool } = await import("./sessions-spawn-tool.js")); -} - describe("sessions_spawn tool", () => { - beforeEach(async () => { + beforeAll(async () => { + ({ createSessionsSpawnTool } = await import("./sessions-spawn-tool.js")); + }); + + beforeEach(() => { hoisted.spawnSubagentDirectMock.mockReset().mockResolvedValue({ status: "accepted", childSessionKey: "agent:main:subagent:1", @@ -48,7 +38,6 @@ describe("sessions_spawn tool", () => { childSessionKey: "agent:codex:acp:1", runId: "run-acp", }); - await loadFreshSessionsSpawnToolModuleForTest(); }); it("uses subagent runtime by default", async () => { diff --git a/src/agents/tools/sessions.test.ts b/src/agents/tools/sessions.test.ts index 4a96a04d1c6..ec5d50577b8 100644 --- a/src/agents/tools/sessions.test.ts +++ b/src/agents/tools/sessions.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { extractAssistantText, sanitizeTextContent } from "./sessions-helpers.js"; @@ -44,23 +44,13 @@ type SessionsListResult = Awaited< ReturnType["execute"]> >; -async function loadFreshSessionsToolModulesForTest() { +beforeAll(async () => { vi.resetModules(); - vi.doMock("../../gateway/call.js", () => ({ - callGateway: (opts: unknown) => callGatewayMock(opts), - })); - vi.doMock("../../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => loadConfigMock() as never, - }; - }); ({ createSessionsListTool } = await import("./sessions-list-tool.js")); ({ createSessionsSendTool } = await import("./sessions-send-tool.js")); ({ resolveAnnounceTarget } = await import("./sessions-announce-target.js")); ({ setActivePluginRegistry } = await import("../../plugins/runtime.js")); -} +}); const installRegistry = async () => { setActivePluginRegistry( @@ -172,13 +162,13 @@ describe("sanitizeTextContent", () => { }); }); -beforeEach(async () => { +beforeEach(() => { loadConfigMock.mockReset(); loadConfigMock.mockReturnValue({ session: { scope: "per-sender", mainKey: "main" }, tools: { agentToAgent: { enabled: false } }, }); - await loadFreshSessionsToolModulesForTest(); + setActivePluginRegistry(createTestRegistry([])); }); describe("extractAssistantText", () => { diff --git a/src/agents/tools/web-search.redirect.test.ts b/src/agents/tools/web-search.redirect.test.ts index d00c6a31995..4ded0dd0139 100644 --- a/src/agents/tools/web-search.redirect.test.ts +++ b/src/agents/tools/web-search.redirect.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { withStrictWebToolsEndpointMock } = vi.hoisted(() => ({ withStrictWebToolsEndpointMock: vi.fn(), @@ -8,19 +8,19 @@ vi.mock("./web-guarded-fetch.js", () => ({ withStrictWebToolsEndpoint: withStrictWebToolsEndpointMock, })); +let resolveCitationRedirectUrl: typeof import("./web-search-citation-redirect.js").resolveCitationRedirectUrl; + describe("web_search redirect resolution hardening", () => { - async function resolveRedirectUrl() { - const module = await import("./web-search-citation-redirect.js"); - return module.resolveCitationRedirectUrl; - } + beforeAll(async () => { + vi.resetModules(); + ({ resolveCitationRedirectUrl } = await import("./web-search-citation-redirect.js")); + }); beforeEach(() => { - vi.resetModules(); withStrictWebToolsEndpointMock.mockReset(); }); it("resolves redirects via SSRF-guarded HEAD requests", async () => { - const resolve = await resolveRedirectUrl(); withStrictWebToolsEndpointMock.mockImplementation(async (_params, run) => { return await run({ response: new Response(null, { status: 200 }), @@ -28,7 +28,7 @@ describe("web_search redirect resolution hardening", () => { }); }); - const resolved = await resolve("https://example.com/start"); + const resolved = await resolveCitationRedirectUrl("https://example.com/start"); expect(resolved).toBe("https://example.com/final"); expect(withStrictWebToolsEndpointMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -41,8 +41,9 @@ describe("web_search redirect resolution hardening", () => { }); it("falls back to the original URL when guarded resolution fails", async () => { - const resolve = await resolveRedirectUrl(); withStrictWebToolsEndpointMock.mockRejectedValue(new Error("blocked")); - await expect(resolve("https://example.com/start")).resolves.toBe("https://example.com/start"); + await expect(resolveCitationRedirectUrl("https://example.com/start")).resolves.toBe( + "https://example.com/start", + ); }); }); diff --git a/src/process/exec.no-output-timer.test.ts b/src/process/exec.no-output-timer.test.ts index 8943c3200ee..4e9c685841f 100644 --- a/src/process/exec.no-output-timer.test.ts +++ b/src/process/exec.no-output-timer.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -54,9 +54,12 @@ function emitProcessExit( } describe("runCommandWithTimeout no-output timer", () => { - beforeEach(async () => { + beforeAll(async () => { vi.resetModules(); ({ runCommandWithTimeout } = await import("./exec.js")); + }); + + beforeEach(() => { spawnMock.mockClear(); });