test: stabilize test isolation

This commit is contained in:
Peter Steinberger 2026-03-23 22:59:15 +00:00
parent ae336d1602
commit d67efbfbd3
5 changed files with 154 additions and 29 deletions

View File

@ -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(() => {

View File

@ -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<AuthProfilesModule["ensureAuthProfileStore"]>
>;
let mockedGetSoonestCooldownExpiry: ReturnType<
typeof vi.mocked<AuthProfilesModule["getSoonestCooldownExpiry"]>
>;
let mockedIsProfileInCooldown: ReturnType<
typeof vi.mocked<AuthProfilesModule["isProfileInCooldown"]>
>;
let mockedResolveProfilesUnavailableReason: ReturnType<
typeof vi.mocked<AuthProfilesModule["resolveProfilesUnavailableReason"]>
>;
let mockedResolveAuthProfileOrder: ReturnType<
typeof vi.mocked<AuthProfilesModule["resolveAuthProfileOrder"]>
>;
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);

View File

@ -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<PiToolsModule["createOpenClawCodingTools"]>[0];
type MockOpenClawToolsOptions = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
sandboxRoot?: string;
sandboxFsBridge?: SandboxFsBridge;
fsPolicy?: NonNullable<Parameters<typeof createImageTool>[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<string, MediaUnderstandingProvider>();
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<T>(run: (agentDir: string) => Promise<T>): Promise<T> {
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,

View File

@ -110,8 +110,15 @@ vi.mock("../logger.js", async (importOriginal) => {
};
});
const { GatewayClient } = await import("./client.js");
type GatewayClientInstance = InstanceType<typeof GatewayClient>;
type GatewayClientModule = typeof import("./client.js");
type GatewayClientInstance = InstanceType<GatewayClientModule["GatewayClient"]>;
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"]);

View File

@ -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<typeof import("../channels/plugins/binding-registry.js")>();
return {
...actual,
primeConfiguredBindingRegistry,
};
});
vi.mock("./server-methods.js", () => ({
handleGatewayRequest,