From d67efbfbd3e08df639a4dec8751926cf12e6a72b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 22:59:15 +0000 Subject: [PATCH] test: stabilize test isolation --- .../bash-tools.exec.approval-id.test.ts | 12 ++- src/agents/model-fallback.probe.test.ts | 63 ++++++++++---- src/agents/tools/image-tool.test.ts | 83 ++++++++++++++++++- src/gateway/client.test.ts | 15 +++- src/gateway/server-plugins.test.ts | 10 ++- 5 files changed, 154 insertions(+), 29 deletions(-) diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index 3048e49807c..bc23a87ebac 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -29,6 +29,13 @@ let callGatewayTool: typeof import("./tools/gateway.js").callGatewayTool; let createExecTool: typeof import("./bash-tools.exec.js").createExecTool; let detectCommandObfuscation: typeof import("../infra/exec-obfuscation-detect.js").detectCommandObfuscation; +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")); +} + function buildPreparedSystemRunPayload(rawInvokeParams: unknown) { const invoke = (rawInvokeParams ?? {}) as { params?: { @@ -210,10 +217,7 @@ describe("exec approvals", () => { process.env.HOME = tempDir; // Windows uses USERPROFILE for os.homedir() process.env.USERPROFILE = tempDir; - vi.resetModules(); - ({ callGatewayTool } = await import("./tools/gateway.js")); - ({ createExecTool } = await import("./bash-tools.exec.js")); - ({ detectCommandObfuscation } = await import("../infra/exec-obfuscation-detect.js")); + await loadExecApprovalModules(); }); afterEach(() => { diff --git a/src/agents/model-fallback.probe.test.ts b/src/agents/model-fallback.probe.test.ts index e80c3e3edd4..e43ca5a4d8b 100644 --- a/src/agents/model-fallback.probe.test.ts +++ b/src/agents/model-fallback.probe.test.ts @@ -2,7 +2,6 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { registerLogTransport, resetLogger, setLoggerOverride } from "../logging/logger.js"; import type { AuthProfileStore } from "./auth-profiles.js"; import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js"; @@ -15,24 +14,53 @@ vi.mock("./auth-profiles.js", () => ({ resolveAuthProfileOrder: vi.fn(), })); -import { - ensureAuthProfileStore, - getSoonestCooldownExpiry, - isProfileInCooldown, - resolveProfilesUnavailableReason, - resolveAuthProfileOrder, -} from "./auth-profiles.js"; -import { _probeThrottleInternals, runWithModelFallback } from "./model-fallback.js"; +type AuthProfilesModule = typeof import("./auth-profiles.js"); +type ModelFallbackModule = typeof import("./model-fallback.js"); +type LoggerModule = typeof import("../logging/logger.js"); -const mockedEnsureAuthProfileStore = vi.mocked(ensureAuthProfileStore); -const mockedGetSoonestCooldownExpiry = vi.mocked(getSoonestCooldownExpiry); -const mockedIsProfileInCooldown = vi.mocked(isProfileInCooldown); -const mockedResolveProfilesUnavailableReason = vi.mocked(resolveProfilesUnavailableReason); -const mockedResolveAuthProfileOrder = vi.mocked(resolveAuthProfileOrder); +let mockedEnsureAuthProfileStore: ReturnType< + typeof vi.mocked +>; +let mockedGetSoonestCooldownExpiry: ReturnType< + typeof vi.mocked +>; +let mockedIsProfileInCooldown: ReturnType< + typeof vi.mocked +>; +let mockedResolveProfilesUnavailableReason: ReturnType< + typeof vi.mocked +>; +let mockedResolveAuthProfileOrder: ReturnType< + typeof vi.mocked +>; +let runWithModelFallback: ModelFallbackModule["runWithModelFallback"]; +let _probeThrottleInternals: ModelFallbackModule["_probeThrottleInternals"]; +let registerLogTransport: LoggerModule["registerLogTransport"]; +let resetLogger: LoggerModule["resetLogger"]; +let setLoggerOverride: LoggerModule["setLoggerOverride"]; const makeCfg = makeModelFallbackCfg; let unregisterLogTransport: (() => void) | undefined; +async function loadModelFallbackProbeModules() { + vi.resetModules(); + const authProfilesModule = await import("./auth-profiles.js"); + const loggerModule = await import("../logging/logger.js"); + const modelFallbackModule = await import("./model-fallback.js"); + mockedEnsureAuthProfileStore = vi.mocked(authProfilesModule.ensureAuthProfileStore); + mockedGetSoonestCooldownExpiry = vi.mocked(authProfilesModule.getSoonestCooldownExpiry); + mockedIsProfileInCooldown = vi.mocked(authProfilesModule.isProfileInCooldown); + mockedResolveProfilesUnavailableReason = vi.mocked( + authProfilesModule.resolveProfilesUnavailableReason, + ); + mockedResolveAuthProfileOrder = vi.mocked(authProfilesModule.resolveAuthProfileOrder); + runWithModelFallback = modelFallbackModule.runWithModelFallback; + _probeThrottleInternals = modelFallbackModule._probeThrottleInternals; + registerLogTransport = loggerModule.registerLogTransport; + resetLogger = loggerModule.resetLogger; + setLoggerOverride = loggerModule.setLoggerOverride; +} + function expectFallbackUsed( result: { result: unknown; attempts: Array<{ reason?: string }> }, run: { @@ -131,7 +159,8 @@ describe("runWithModelFallback – probe logic", () => { run, }); - beforeEach(() => { + beforeEach(async () => { + await loadModelFallbackProbeModules(); realDateNow = Date.now; Date.now = vi.fn(() => NOW); @@ -159,7 +188,7 @@ describe("runWithModelFallback – probe logic", () => { return []; }); // Default: only openai profiles are in cooldown; fallback providers are available - mockedIsProfileInCooldown.mockImplementation((_store, profileId: string) => { + mockedIsProfileInCooldown.mockImplementation((_store: AuthProfileStore, profileId: string) => { return profileId.startsWith("openai"); }); mockedResolveProfilesUnavailableReason.mockReturnValue("rate_limit"); @@ -355,7 +384,7 @@ describe("runWithModelFallback – probe logic", () => { } return []; }); - mockedIsProfileInCooldown.mockImplementation((_store, profileId: string) => + mockedIsProfileInCooldown.mockImplementation((_store: AuthProfileStore, profileId: string) => profileId.startsWith("google"), ); mockedGetSoonestCooldownExpiry.mockReturnValue(NOW + 30 * 1000); diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index af4402005bb..a42538318fa 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -11,12 +11,35 @@ import type { } from "../../plugin-sdk/media-understanding.js"; import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { minimaxUnderstandImage } from "../minimax-vlm.js"; -import { createOpenClawCodingTools } from "../pi-tools.js"; +import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js"; import { createUnsafeMountedSandbox } from "../test-helpers/unsafe-mounted-sandbox.js"; import { makeZeroUsageSnapshot } from "../usage.js"; import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js"; +type PiToolsModule = typeof import("../pi-tools.js"); +type CreateOpenClawCodingToolsArgs = Parameters[0]; +type MockOpenClawToolsOptions = { + config?: OpenClawConfig; + agentDir?: string; + workspaceDir?: string; + sandboxRoot?: string; + sandboxFsBridge?: SandboxFsBridge; + fsPolicy?: NonNullable[0]>["fsPolicy"]; + modelHasVision?: boolean; +}; + +const piToolsHarness = vi.hoisted(() => ({ + createStubTool(name: string) { + return { + name, + description: `${name} stub`, + parameters: { type: "object", properties: {} }, + execute: vi.fn(), + }; + }, +})); + const imageProviderHarness = vi.hoisted(() => { let providers = new Map(); return { @@ -63,6 +86,50 @@ vi.mock("../../media-understanding/provider-registry.js", async (importOriginal) }; }); +vi.mock("../bash-tools.js", () => ({ + createExecTool: vi.fn(() => piToolsHarness.createStubTool("exec")), + createProcessTool: vi.fn(() => piToolsHarness.createStubTool("process")), +})); + +vi.mock("../channel-tools.js", () => ({ + listChannelAgentTools: vi.fn(() => []), +})); + +vi.mock("../apply-patch.js", () => ({ + createApplyPatchTool: vi.fn(() => piToolsHarness.createStubTool("apply_patch")), +})); + +vi.mock("../pi-tools.before-tool-call.js", () => ({ + wrapToolWithBeforeToolCallHook: vi.fn((tool) => tool), +})); + +vi.mock("../pi-tools.abort.js", () => ({ + wrapToolWithAbortSignal: vi.fn((tool) => tool), +})); + +vi.mock("../openclaw-tools.js", async () => { + const { createImageTool } = await import("./image-tool.js"); + return { + createOpenClawTools: vi.fn((options?: MockOpenClawToolsOptions) => { + const imageTool = createImageTool({ + config: options?.config, + agentDir: options?.agentDir, + workspaceDir: options?.workspaceDir, + sandbox: + options?.sandboxRoot && options?.sandboxFsBridge + ? { + root: options.sandboxRoot, + bridge: options.sandboxFsBridge, + } + : undefined, + fsPolicy: options?.fsPolicy, + modelHasVision: options?.modelHasVision, + }); + return imageTool ? [imageTool] : []; + }), + }; +}); + async function writeAuthProfiles(agentDir: string, profiles: unknown) { await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile( @@ -72,6 +139,12 @@ async function writeAuthProfiles(agentDir: string, profiles: unknown) { ); } +async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenClawCodingToolsArgs) { + vi.resetModules(); + const { createOpenClawCodingTools } = await import("../pi-tools.js"); + return createOpenClawCodingTools(options); +} + async function withTempAgentDir(run: (agentDir: string) => Promise): Promise { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-")); try { @@ -732,7 +805,11 @@ describe("image tool implicit imageModel config", () => { await withTempAgentDir(async (agentDir) => { const cfg = createMinimaxImageConfig(); - const tools = createOpenClawCodingTools({ config: cfg, agentDir, workspaceDir }); + const tools = await createOpenClawCodingToolsWithFreshModules({ + config: cfg, + agentDir, + workspaceDir, + }); const tool = requireImageTool(tools.find((candidate) => candidate.name === "image")); await expectImageToolExecOk(tool, imagePath); @@ -776,7 +853,7 @@ describe("image tool implicit imageModel config", () => { tools: { fs: { workspaceOnly: true } }, }; - const tools = createOpenClawCodingTools({ + const tools = await createOpenClawCodingToolsWithFreshModules({ config: cfg, agentDir, sandbox, diff --git a/src/gateway/client.test.ts b/src/gateway/client.test.ts index 6fe39ad14e9..9c4da88fd25 100644 --- a/src/gateway/client.test.ts +++ b/src/gateway/client.test.ts @@ -110,8 +110,15 @@ vi.mock("../logger.js", async (importOriginal) => { }; }); -const { GatewayClient } = await import("./client.js"); -type GatewayClientInstance = InstanceType; +type GatewayClientModule = typeof import("./client.js"); +type GatewayClientInstance = InstanceType; + +let GatewayClient: GatewayClientModule["GatewayClient"]; + +async function loadGatewayClientModule() { + vi.resetModules(); + ({ GatewayClient } = await import("./client.js")); +} function getLatestWs(): MockWebSocket { const ws = wsInstances.at(-1); @@ -153,6 +160,10 @@ function expectSecurityConnectError( } } +beforeEach(async () => { + await loadGatewayClientModule(); +}); + describe("GatewayClient security checks", () => { const envSnapshot = captureEnv(["OPENCLAW_ALLOW_INSECURE_PRIVATE_WS"]); diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index a7de207cef5..73ae2bcfb70 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -25,9 +25,13 @@ vi.mock("../plugins/channel-plugin-ids.js", () => ({ resolveGatewayStartupPluginIds, })); -vi.mock("../channels/plugins/binding-registry.js", () => ({ - primeConfiguredBindingRegistry, -})); +vi.mock("../channels/plugins/binding-registry.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + primeConfiguredBindingRegistry, + }; +}); vi.mock("./server-methods.js", () => ({ handleGatewayRequest,