test: reduce agent test import churn

This commit is contained in:
Peter Steinberger
2026-04-03 04:38:38 +01:00
parent 847faa3d04
commit ffd34f8896
25 changed files with 225 additions and 363 deletions

View File

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

View File

@@ -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();
});

View File

@@ -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();
});

View File

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

View File

@@ -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<typeof import("../infra/heartbeat-wake.js")>(
"../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", () => {

View File

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

View File

@@ -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<typeof import("../infra/shell-env.js")>();
return {
...mod,
getShellPathFromLoginShell: shellEnvMocks.getShellPathFromLoginShell,
resolveShellEnvFallbackTimeoutMs: shellEnvMocks.resolveShellEnvFallbackTimeoutMs,
};
});
vi.doMock("../infra/exec-approvals.js", async (importOriginal) => {
const mod = await importOriginal<typeof import("../infra/exec-approvals.js")>();
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<typeof captureEnv>;
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:-}\"",

View File

@@ -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();

View File

@@ -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")]);
});

View File

@@ -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<ContextModule> {
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,

View File

@@ -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();

View File

@@ -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();
});

View File

@@ -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<void>) {
const stateDir = mkdtempSync(join(tmpdir(), "openclaw-openrouter-capabilities-"));
@@ -13,9 +14,15 @@ async function withOpenRouterStateDir(run: (stateDir: string) => Promise<void>)
}
}
async function importOpenRouterModelCapabilities(scope: string) {
return await importFreshModule<typeof import("./openrouter-model-capabilities.js")>(
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);

View File

@@ -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");
});

View File

@@ -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<typeof import("../plugins/hook-runner-global.js")>();
@@ -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<typeof vi.fn>;
runBeforeToolCall: ReturnType<typeof vi.fn>;
};
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<void>(() => {}));
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({

View File

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

View File

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

View File

@@ -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<typeof import("../security/skill-scanner.js")>()),
scanDirectoryWithSummary: (...args: unknown[]) => scanDirectoryWithSummaryMock(...args),
}));
vi.doMock("../shared/config-eval.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../shared/config-eval.js")>();
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 () => {

View File

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

View File

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

View File

@@ -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<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
type PdfToolModule = typeof import("./pdf-tool.js");
let createPdfTool: PdfToolModule["createPdfTool"];
let resolvePdfModelConfigForTool: PdfToolModule["resolvePdfModelConfigForTool"];
let pdfToolModulePromise: Promise<PdfToolModule> | null = null;
async function importPdfToolModule(): Promise<PdfToolModule> {
if (pdfToolModulePromise) {
return await pdfToolModulePromise;
}
vi.resetModules();
vi.doMock("@mariozechner/pi-ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
pdfToolModulePromise = import("./pdf-tool.js");
return await pdfToolModulePromise;
}
beforeAll(async () => {
({ createPdfTool, resolvePdfModelConfigForTool } = await import("./pdf-tool.js"));
});
async function withTempAgentDir<T>(run: (agentDir: string) => Promise<T>): Promise<T> {
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);
});
});

View File

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

View File

@@ -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<ReturnType<typeof import("./sessions-list-tool.js").createSessionsListTool>["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<typeof import("../../config/config.js")>();
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", () => {

View File

@@ -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",
);
});
});

View File

@@ -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();
});