mirror of https://github.com/openclaw/openclaw.git
test: reduce agent test import churn
This commit is contained in:
parent
847faa3d04
commit
ffd34f8896
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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:-}\"",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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")]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue