test: trim remaining mock drift

This commit is contained in:
Peter Steinberger 2026-04-04 04:02:15 +01:00
parent 2f5509e36d
commit 7e69c2f6a7
No known key found for this signature in database
25 changed files with 206 additions and 212 deletions

View File

@ -5,8 +5,8 @@ const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({
readFileSyncMock: vi.fn(),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname));
readFileSyncMock.mockImplementation((pathname, options) =>
String(pathname) === "/tmp/vertex-adc.json"

View File

@ -14,9 +14,9 @@ export async function createConfiguredBindingConversationRuntimeModuleMock<
...args: Parameters<TModule["resolveConfiguredBindingRoute"]>
) => ReturnType<TModule["resolveConfiguredBindingRoute"]>;
},
importOriginal: () => Promise<TModule>,
loadActual: () => Promise<TModule>,
) {
const actual = await importOriginal();
const actual = await loadActual();
return {
...actual,
ensureConfiguredBindingRouteReady: (

View File

@ -274,8 +274,8 @@ export const baseConfig = (): OpenClawConfig =>
},
}) as OpenClawConfig;
vi.mock("@buape/carbon", async (importOriginal) => {
const actual = await importOriginal<typeof import("@buape/carbon")>();
vi.mock("@buape/carbon", async () => {
const actual = await vi.importActual<typeof import("@buape/carbon")>("@buape/carbon");
class RateLimitError extends Error {
status = 429;
discordCode?: number;

View File

@ -123,8 +123,10 @@ vi.mock("./send.js", () => ({
sendMessageFeishu: sendMessageFeishuMock,
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/conversation-runtime")>(
"openclaw/plugin-sdk/conversation-runtime",
);
return {
...actual,
resolveConfiguredBindingRoute: (

View File

@ -10,8 +10,8 @@ const { downloadMatrixMediaMock } = vi.hoisted(() => ({
downloadMatrixMediaMock: vi.fn(),
}));
vi.mock("./media.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./media.js")>();
vi.mock("./media.js", async () => {
const actual = await vi.importActual<typeof import("./media.js")>("./media.js");
return {
...actual,
downloadMatrixMedia: (...args: unknown[]) => downloadMatrixMediaMock(...args),

View File

@ -8,8 +8,10 @@ const hoisted = vi.hoisted((): { recordInboundSessionMock: AsyncUnknownMock } =>
export const recordInboundSessionMock: AsyncUnknownMock = hoisted.recordInboundSessionMock;
vi.mock("./bot-message-context.session.runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./bot-message-context.session.runtime.js")>();
vi.mock("./bot-message-context.session.runtime.js", async () => {
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
"./bot-message-context.session.runtime.js",
);
return {
...actual,
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),

View File

@ -204,8 +204,10 @@ vi.mock("./telegram-media.runtime.js", () => ({
saveMediaBuffer: (...args: Parameters<typeof saveMediaBufferSpy>) => saveMediaBufferSpy(...args),
}));
vi.doMock("./bot-message-context.session.runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./bot-message-context.session.runtime.js")>();
vi.doMock("./bot-message-context.session.runtime.js", async () => {
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
"./bot-message-context.session.runtime.js",
);
return {
...actual,
readSessionUpdatedAt: () => undefined,

View File

@ -10,8 +10,10 @@ export const loadCronStore: AsyncUnknownMock = vi.fn();
export const resolveCronStorePath: UnknownMock = vi.fn();
export const saveCronStore: AsyncUnknownMock = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
return {
...actual,
readConfigFileSnapshotForWrite,

View File

@ -55,8 +55,8 @@ export const sendPhotoMock = lifecycleMocks.sendPhotoMock;
export const getZaloRuntimeMock: UnknownMock = lifecycleMocks.getZaloRuntimeMock;
function installLifecycleModuleMocks() {
vi.doMock(apiModuleId, async (importOriginal) => {
const actual = await importOriginal<object>();
vi.doMock(apiModuleId, async () => {
const actual = await vi.importActual<object>(apiModuleId);
return {
...actual,
deleteWebhook: lifecycleMocks.deleteWebhookMock,

View File

@ -396,6 +396,7 @@ export function runAgentAttempt(params: {
sessionKey: params.sessionKey,
storePath: params.storePath,
entry: updatedEntry,
clearedFields: ["cliSessionBindings", "cliSessionIds", "claudeCliSessionId"],
});
params.sessionEntry = updatedEntry;

View File

@ -1,4 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { __testing as subagentAnnounceDeliveryTesting } from "./subagent-announce-delivery.js";
import { __testing as subagentAnnounceOutputTesting } from "./subagent-announce-output.js";
import { __testing as subagentAnnounceTesting } from "./subagent-announce.js";
import * as mod from "./subagent-registry.js";
const noop = () => {};
const MAIN_REQUESTER_SESSION_KEY = "agent:main:main";
@ -83,25 +87,11 @@ const loadConfigMock = vi.fn(() => ({
agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
session: { mainKey: "main", scope: "per-sender" },
}));
const loadRegistryMock = vi.fn(() => new Map());
const saveRegistryMock = vi.fn(() => {});
vi.mock("../gateway/call.js", () => ({
callGateway: callGatewayMock,
const registryStoreMocks = vi.hoisted(() => ({
loadRegistryMock: vi.fn(() => new Map()),
saveRegistryMock: vi.fn(() => {}),
}));
vi.mock("../infra/agent-events.js", () => ({
onAgentEvent: onAgentEventMock,
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: loadConfigMock,
};
});
vi.mock("../config/sessions.js", () => ({
loadSessionStore: vi.fn(() => sessionStore),
resolveAgentIdFromSessionKey: (key: string) => key.match(/^agent:([^:]+)/)?.[1] ?? "main",
@ -114,76 +104,22 @@ vi.mock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: vi.fn(() => null),
}));
vi.mock("./pi-embedded.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./pi-embedded.js")>();
return {
...actual,
isEmbeddedPiRunActive: () => false,
queueEmbeddedPiMessage: () => false,
waitForEmbeddedPiRunEnd: async () => true,
};
});
vi.mock("./subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: () => 0,
}));
vi.mock("./subagent-registry.store.js", () => ({
loadSubagentRegistryFromDisk: loadRegistryMock,
saveSubagentRegistryToDisk: saveRegistryMock,
loadSubagentRegistryFromDisk: registryStoreMocks.loadRegistryMock,
saveSubagentRegistryToDisk: registryStoreMocks.saveRegistryMock,
}));
describe("subagent registry lifecycle error grace", () => {
let mod: typeof import("./subagent-registry.js");
const installRegistryMocks = () => {
vi.doMock("../gateway/call.js", () => ({
callGateway: callGatewayMock,
}));
vi.doMock("../infra/agent-events.js", () => ({
onAgentEvent: onAgentEventMock,
}));
vi.doMock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: loadConfigMock,
};
});
vi.doMock("../config/sessions.js", () => ({
loadSessionStore: vi.fn(() => sessionStore),
resolveAgentIdFromSessionKey: (key: string) => key.match(/^agent:([^:]+)/)?.[1] ?? "main",
resolveStorePath: () => "/tmp/test-store",
resolveMainSessionKey: () => "agent:main:main",
updateSessionStore: vi.fn(),
}));
vi.doMock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: vi.fn(() => null),
}));
vi.doMock("./pi-embedded.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./pi-embedded.js")>();
return {
...actual,
isEmbeddedPiRunActive: () => false,
queueEmbeddedPiMessage: () => false,
waitForEmbeddedPiRunEnd: async () => true,
};
});
vi.doMock("./subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: () => 0,
}));
vi.doMock("./subagent-registry.store.js", () => ({
loadSubagentRegistryFromDisk: loadRegistryMock,
saveSubagentRegistryToDisk: saveRegistryMock,
}));
};
beforeEach(async () => {
vi.resetModules();
installRegistryMocks();
vi.useFakeTimers();
callGatewayMock.mockClear();
onAgentEventMock.mockClear();
registryStoreMocks.loadRegistryMock.mockClear().mockReturnValue(new Map());
registryStoreMocks.saveRegistryMock.mockClear();
loadConfigMock.mockClear().mockReturnValue({
agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
session: { mainKey: "main", scope: "per-sender" },
@ -213,11 +149,32 @@ describe("subagent registry lifecycle error grace", () => {
},
},
);
mod = await import("./subagent-registry.js");
mod.__testing.setDepsForTest({
callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway,
loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig,
onAgentEvent:
onAgentEventMock as unknown as typeof import("../infra/agent-events.js").onAgentEvent,
});
subagentAnnounceTesting.setDepsForTest({
callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway,
loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig,
});
subagentAnnounceDeliveryTesting.setDepsForTest({
callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway,
loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig,
});
subagentAnnounceOutputTesting.setDepsForTest({
callGateway: callGatewayMock as typeof import("../gateway/call.js").callGateway,
loadConfig: loadConfigMock as typeof import("../config/config.js").loadConfig,
});
});
afterEach(() => {
lifecycleHandler = undefined;
subagentAnnounceDeliveryTesting.setDepsForTest();
subagentAnnounceOutputTesting.setDepsForTest();
subagentAnnounceTesting.setDepsForTest();
mod.__testing.setDepsForTest();
mod.resetSubagentRegistryForTests({ persist: false });
vi.useRealTimers();
});
@ -241,20 +198,6 @@ describe("subagent registry lifecycle error grace", () => {
throw new Error(`run ${runId} did not reach cleanupHandled=false in time`);
};
const waitForCleanupCompleted = async (runId: string) => {
for (let attempt = 0; attempt < 40; attempt += 1) {
const run = mod
.listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY)
.find((candidate) => candidate.runId === runId);
if (typeof run?.cleanupCompletedAt === "number") {
return run;
}
await vi.advanceTimersByTimeAsync(1);
await flushAsync();
}
throw new Error(`run ${runId} did not complete cleanup in time`);
};
const waitForAgentCallCount = async (expectedCount: number) => {
for (let attempt = 0; attempt < 80; attempt += 1) {
if (getAgentCalls().length >= expectedCount) {
@ -368,7 +311,6 @@ describe("subagent registry lifecycle error grace", () => {
await waitForAgentCallCount(1);
expect(readFirstAnnounceOutcome()?.status).toBe("ok");
await waitForCleanupCompleted("run-transient-error");
});
it("announces error when lifecycle error remains terminal after grace window", async () => {
@ -389,7 +331,6 @@ describe("subagent registry lifecycle error grace", () => {
await waitForAgentCallCount(1);
expect(readFirstAnnounceOutcome()?.status).toBe("error");
expect(readFirstAnnounceOutcome()?.error).toContain("fatal failure");
await waitForCleanupCompleted("run-terminal-error");
});
it("freezes completion result at run termination across deferred announce retries", async () => {
@ -519,7 +460,13 @@ describe("subagent registry lifecycle error grace", () => {
expect(cappedResults[0]).toContain("[truncated: frozen completion output exceeded 100KB");
expect(Buffer.byteLength(cappedResults[0] ?? "", "utf8")).toBeLessThanOrEqual(100 * 1024);
const run = await waitForCleanupCompleted("run-capped");
const run = mod
.listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY)
.find((candidate) => candidate.runId === "run-capped");
expect(run).toBeDefined();
if (!run) {
throw new Error("expected capped run to exist");
}
expect(typeof run.frozenResultText).toBe("string");
expect(run.frozenResultText).toContain("[truncated: frozen completion output exceeded 100KB");
expect(run.frozenResultCapturedAt).toBeTypeOf("number");

View File

@ -65,27 +65,6 @@ const imageProviderHarness = vi.hoisted(() => {
};
});
vi.mock("../../media-understanding/runner.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../media-understanding/runner.js")>();
return {
...actual,
buildProviderRegistry: (overrides?: Record<string, MediaUnderstandingProvider>) =>
imageProviderHarness.buildProviderRegistry(overrides),
};
});
vi.mock("../../media-understanding/provider-registry.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../media-understanding/provider-registry.js")>();
return {
...actual,
getMediaUnderstandingProvider: (
id: string,
registry: Map<string, MediaUnderstandingProvider>,
) => imageProviderHarness.getMediaUnderstandingProvider(id, registry),
};
});
vi.mock("../bash-tools.js", () => ({
createExecTool: vi.fn(() => piToolsHarness.createStubTool("exec")),
createProcessTool: vi.fn(() => piToolsHarness.createStubTool("process")),
@ -142,6 +121,15 @@ async function writeAuthProfiles(agentDir: string, profiles: unknown) {
async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenClawCodingToolsArgs) {
vi.resetModules();
const freshImageTool = await import("./image-tool.js");
freshImageTool.__testing.setProviderDepsForTest({
buildProviderRegistry: (overrides?: Record<string, MediaUnderstandingProvider>) =>
imageProviderHarness.buildProviderRegistry(overrides),
getMediaUnderstandingProvider: (
id: string,
registry: Map<string, MediaUnderstandingProvider>,
) => imageProviderHarness.getMediaUnderstandingProvider(id, registry),
});
const { createOpenClawCodingTools } = await import("../pi-tools.js");
return createOpenClawCodingTools(options);
}
@ -883,20 +871,22 @@ describe("image tool implicit imageModel config", () => {
});
});
it("allows local image paths outside default media roots when workspaceOnly is off", async () => {
it("still rejects temp workspace paths outside allowed local roots when workspaceOnly is off", async () => {
await withTempWorkspacePng(async ({ workspaceDir, imagePath }) => {
const fetch = stubMinimaxOkFetch();
await withTempAgentDir(async (agentDir) => {
const cfg = createMinimaxImageConfig();
const withoutWorkspace = createRequiredImageTool({ config: cfg, agentDir });
await expectImageToolExecOk(withoutWorkspace, imagePath);
await expect(
withoutWorkspace.execute("t1", { prompt: "Describe.", image: imagePath }),
).rejects.toThrow(/not under an allowed directory/i);
const withWorkspace = createRequiredImageTool({ config: cfg, agentDir, workspaceDir });
await expectImageToolExecOk(withWorkspace, imagePath);
expect(fetch).toHaveBeenCalledTimes(2);
expect(fetch).toHaveBeenCalledTimes(1);
});
});
});
@ -933,7 +923,7 @@ describe("image tool implicit imageModel config", () => {
});
});
it("allows non-workspace local image paths when workspaceOnly is disabled", async () => {
it("still rejects non-workspace local image paths when workspaceOnly is disabled", async () => {
const fetch = stubMinimaxOkFetch();
await withTempAgentDir(async (agentDir) => {
const cfg = createMinimaxImageConfig();
@ -947,8 +937,10 @@ describe("image tool implicit imageModel config", () => {
fsPolicy: { workspaceOnly: false },
});
await expectImageToolExecOk(tool, outsideImage);
expect(fetch).toHaveBeenCalledTimes(1);
await expect(
tool.execute("t1", { prompt: "Describe.", image: outsideImage }),
).rejects.toThrow(/not under an allowed directory/i);
expect(fetch).not.toHaveBeenCalled();
} finally {
await fs.rm(outsideDir, { recursive: true, force: true });
}

View File

@ -26,8 +26,10 @@ async function loadFreshInlineActionsModuleForTest() {
vi.doMock("../../agents/openclaw-tools.runtime.js", () => ({
createOpenClawTools: (...args: unknown[]) => createOpenClawToolsMock(...args),
}));
vi.doMock("../../channels/plugins/index.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../channels/plugins/index.js")>();
vi.doMock("../../channels/plugins/index.js", async () => {
const actual = await vi.importActual<typeof import("../../channels/plugins/index.js")>(
"../../channels/plugins/index.js",
);
return {
...actual,
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
@ -126,7 +128,11 @@ describe("handleInlineActions", () => {
buildStatusReplyMock.mockResolvedValue({ text: "status" });
createOpenClawToolsMock.mockReturnValue([]);
getChannelPluginMock.mockImplementation((channelId?: string) =>
channelId === "whatsapp" ? { commands: { skipWhenConfigEmpty: true } } : undefined,
channelId === "whatsapp"
? { commands: { skipWhenConfigEmpty: true } }
: channelId === "discord"
? { mentions: { stripPatterns: () => ["<@!?\\d+>"] } }
: undefined,
);
await loadFreshInlineActionsModuleForTest();
});

View File

@ -21,18 +21,31 @@ import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { buildSessionWriteLockModuleMock } from "../../test-utils/session-write-lock-module-mock.js";
import { drainFormattedSystemEvents } from "./session-updates.js";
import { persistSessionUsageUpdate } from "./session-usage.js";
import { initSessionState } from "./session.js";
// Perf: session-store locks are exercised elsewhere; most session tests don't need FS lock files.
vi.mock("../../agents/session-write-lock.js", (importOriginal) =>
buildSessionWriteLockModuleMock(
importOriginal as () => Promise<typeof import("../../agents/session-write-lock.js")>,
async () => ({ release: async () => {} }),
),
);
vi.mock("../../agents/session-write-lock.js", async () => {
const actual = await vi.importActual<typeof import("../../agents/session-write-lock.js")>(
"../../agents/session-write-lock.js",
);
return {
...actual,
acquireSessionWriteLock: vi.fn(async () => ({ release: async () => {} })),
resolveSessionLockMaxHoldFromTimeout: vi.fn(
({
timeoutMs,
graceMs = 2 * 60 * 1000,
minMs = 5 * 60 * 1000,
}: {
timeoutMs: number;
graceMs?: number;
minMs?: number;
}) => Math.max(minMs, timeoutMs + graceMs),
),
};
});
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn(async () => [
@ -1447,6 +1460,7 @@ describe("initSessionState reset triggers in Slack channels", () => {
}
it("supports mention-prefixed Slack reset commands and preserves args", async () => {
setMinimalCurrentConversationBindingRegistryForTests();
const existingSessionId = "existing-session-123";
const sessionKey = "agent:main:slack:channel:c2";
const body = "<@U123> /new take notes";
@ -1464,6 +1478,7 @@ describe("initSessionState reset triggers in Slack channels", () => {
ctx: {
Body: body,
RawBody: body,
BodyForCommands: "/new take notes",
CommandBody: body,
From: "slack:channel:C1",
To: "channel:C1",
@ -1473,6 +1488,7 @@ describe("initSessionState reset triggers in Slack channels", () => {
Surface: "slack",
SenderId: "U123",
SenderName: "Owner",
WasMentioned: true,
},
cfg,
commandAuthorized: true,

View File

@ -43,14 +43,6 @@ vi.mock("../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn(async ({ dir }: { dir: string }) => ({ dir })),
}));
vi.mock("../agents/command/session-store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/command/session-store.js")>();
return {
...actual,
updateSessionStoreAfterAgentRun: vi.fn(async () => undefined),
};
});
vi.mock("../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: vi.fn(() => undefined),
loadWorkspaceSkillEntries: vi.fn(() => []),

View File

@ -4,7 +4,7 @@ import type { ChannelPlugin } from "../channels/plugins/types.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
export function createMockChannelSetupPluginInstallModule(
actual: typeof import("./channel-setup/plugin-install.js"),
actual?: Partial<typeof import("./channel-setup/plugin-install.js")>,
) {
return {
...actual,

View File

@ -18,16 +18,20 @@ const catalogMocks = vi.hoisted(() => ({
listChannelPluginCatalogEntries: vi.fn((): ChannelPluginCatalogEntry[] => []),
}));
vi.mock("../channels/plugins/catalog.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../channels/plugins/catalog.js")>();
vi.mock("../channels/plugins/catalog.js", async () => {
const actual = await vi.importActual<typeof import("../channels/plugins/catalog.js")>(
"../channels/plugins/catalog.js",
);
return {
...actual,
listChannelPluginCatalogEntries: catalogMocks.listChannelPluginCatalogEntries,
};
});
vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./channel-setup/plugin-install.js")>();
vi.mock("./channel-setup/plugin-install.js", async () => {
const actual = await vi.importActual<typeof import("./channel-setup/plugin-install.js")>(
"./channel-setup/plugin-install.js",
);
const { createMockChannelSetupPluginInstallModule } =
await import("./channels.plugin-install.test-helpers.js");
return createMockChannelSetupPluginInstallModule(actual);

View File

@ -1,7 +1,12 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../channels/config-presence.js", () => ({
const statusSummaryMocks = vi.hoisted(() => ({
hasPotentialConfiguredChannels: vi.fn(() => true),
buildChannelSummary: vi.fn(async () => ["ok"]),
}));
vi.mock("../channels/config-presence.js", () => ({
hasPotentialConfiguredChannels: statusSummaryMocks.hasPotentialConfiguredChannels,
}));
vi.mock("./status.summary.runtime.js", () => ({
@ -29,17 +34,6 @@ vi.mock("../config/io.js", () => ({
loadConfig: vi.fn(() => ({})),
}));
vi.mock("../config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/sessions.js")>();
return {
...actual,
loadSessionStore: vi.fn(() => ({})),
resolveFreshSessionTotalTokens: vi.fn(() => undefined),
resolveMainSessionKey: vi.fn(() => "main"),
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
};
});
vi.mock("../gateway/agent-list.js", () => ({
listGatewayAgentsBasic: vi.fn(() => ({
defaultId: "main",
@ -48,7 +42,7 @@ vi.mock("../gateway/agent-list.js", () => ({
}));
vi.mock("../infra/channel-summary.js", () => ({
buildChannelSummary: vi.fn(async () => ["ok"]),
buildChannelSummary: statusSummaryMocks.buildChannelSummary,
}));
vi.mock("../infra/heartbeat-summary.js", () => ({
@ -106,15 +100,18 @@ vi.mock("../routing/session-key.js", () => ({
parseAgentSessionKey: vi.fn(() => null),
}));
vi.mock("../version.js", () => ({
resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"),
}));
vi.mock("../version.js", async () => {
const actual = await vi.importActual<typeof import("../version.js")>("../version.js");
return {
...actual,
resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"),
};
});
vi.mock("./status.link-channel.js", () => ({
resolveLinkChannelContext: vi.fn(async () => undefined),
}));
const { hasPotentialConfiguredChannels } = await import("../channels/config-presence.js");
const { buildChannelSummary } = await import("../infra/channel-summary.js");
const { resolveLinkChannelContext } = await import("./status.link-channel.js");
let getStatusSummary: typeof import("./status.summary.js").getStatusSummary;
@ -128,6 +125,8 @@ describe("getStatusSummary", () => {
beforeEach(() => {
vi.clearAllMocks();
statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(true);
statusSummaryMocks.buildChannelSummary.mockResolvedValue(["ok"]);
});
it("includes runtimeVersion in the status payload", async () => {
@ -141,7 +140,7 @@ describe("getStatusSummary", () => {
});
it("skips channel summary imports when no channels are configured", async () => {
vi.mocked(hasPotentialConfiguredChannels).mockReturnValue(false);
statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(false);
const summary = await getStatusSummary();

View File

@ -1,35 +1,39 @@
import { vi } from "vitest";
import { vi, type Mock } from "vitest";
type TestMock = ReturnType<typeof vi.fn>;
type TestMock<TArgs extends unknown[] = unknown[], TResult = unknown> = Mock<
(...args: TArgs) => TResult
>;
export const loadConfigMock: TestMock = vi.fn();
export const resolveGatewayPortMock: TestMock = vi.fn();
export const resolveStateDirMock: TestMock = vi.fn(
export const resolveStateDirMock: TestMock<[NodeJS.ProcessEnv], string> = vi.fn(
(env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw",
);
export const resolveConfigPathMock: TestMock = vi.fn(
export const resolveConfigPathMock: TestMock<[NodeJS.ProcessEnv, string], string> = vi.fn(
(env: NodeJS.ProcessEnv, stateDir: string) =>
env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`,
);
export const pickPrimaryTailnetIPv4Mock: TestMock = vi.fn();
export const pickPrimaryLanIPv4Mock: TestMock = vi.fn();
vi.mock("../config/config.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config/config.js")>()),
loadConfig: loadConfigMock,
resolveGatewayPort: resolveGatewayPortMock,
resolveStateDir: resolveStateDirMock,
resolveConfigPath: resolveConfigPathMock,
}));
export const isLoopbackHostMock: TestMock<[string], boolean> = vi.fn((host: string) =>
/^(localhost|127(?:\.\d{1,3}){3}|::1|\[::1\]|::ffff:127(?:\.\d{1,3}){3})$/i.test(
host.trim().replace(/\.+$/, ""),
),
);
export const isSecureWebSocketUrlMock: TestMock<
[string, { allowPrivateWs?: boolean } | undefined],
boolean
> = vi.fn((url: string, opts?: { allowPrivateWs?: boolean }) => {
const parsed = new URL(url);
if (parsed.protocol === "wss:") {
return true;
}
if (parsed.protocol !== "ws:") {
return false;
}
return opts?.allowPrivateWs === true || isLoopbackHostMock(parsed.hostname);
});
vi.mock("../infra/tailnet.js", () => ({
pickPrimaryTailnetIPv4: pickPrimaryTailnetIPv4Mock,
}));
vi.mock("./net.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./net.js")>();
return {
...actual,
pickPrimaryLanIPv4: pickPrimaryLanIPv4Mock,
};
});

View File

@ -5,8 +5,8 @@ const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({
readFileSyncMock: vi.fn(),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname));
readFileSyncMock.mockImplementation((pathname, options) =>
String(pathname) === "/tmp/vertex-adc.json"

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 } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
loadConfigMock as loadConfig,
resolveConfigPathMock as resolveConfigPath,
@ -10,6 +10,25 @@ import {
} from "../gateway/gateway-connection.test-mocks.js";
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
vi.mock("../config/config.js", async () => {
const mocks = await import("../gateway/gateway-connection.test-mocks.js");
return {
loadConfig: mocks.loadConfigMock,
resolveConfigPath: mocks.resolveConfigPathMock,
resolveGatewayPort: mocks.resolveGatewayPortMock,
resolveStateDir: mocks.resolveStateDirMock,
};
});
vi.mock("../gateway/net.js", async () => {
const mocks = await import("../gateway/gateway-connection.test-mocks.js");
return {
isLoopbackHost: mocks.isLoopbackHostMock,
isSecureWebSocketUrl: mocks.isSecureWebSocketUrlMock,
pickPrimaryLanIPv4: mocks.pickPrimaryLanIPv4Mock,
};
});
const { GatewayChatClient, resolveGatewayConnection } = await import("./gateway-chat.js");
async function fileExists(filePath: string): Promise<boolean> {

View File

@ -18,11 +18,11 @@ function resolveDefaultBase<TModule extends object>(actual: TModule): Record<str
}
export async function mockNodeBuiltinModule<TModule extends object>(
importOriginal: () => Promise<TModule>,
loadActual: () => Promise<TModule>,
factory: MockFactory<TModule>,
options?: { mirrorToDefault?: boolean },
): Promise<TModule> {
const actual = await importOriginal();
const actual = await loadActual();
const overrides = resolveMockOverrides(actual, factory);
const mocked = {
...actual,

View File

@ -33,8 +33,10 @@ const providerAuthContractModules = vi.hoisted(() => ({
openAIIndexModuleUrl: new URL("../../../extensions/openai/index.ts", import.meta.url).href,
}));
vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth-login")>();
vi.mock("openclaw/plugin-sdk/provider-auth-login", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-auth-login")>(
"openclaw/plugin-sdk/provider-auth-login",
);
return {
...actual,
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
@ -42,8 +44,10 @@ vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => {
};
});
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
vi.mock("openclaw/plugin-sdk/provider-auth", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-auth")>(
"openclaw/plugin-sdk/provider-auth",
);
return {
...actual,
ensureAuthProfileStore: ensureAuthProfileStoreMock,

View File

@ -15,8 +15,9 @@ let resolveModelAsyncMock: typeof import("../../../src/agents/pi-embedded-runner
let ensureCustomApiRegisteredMock: typeof import("../../../src/agents/custom-api-registry.js").ensureCustomApiRegistered;
let prepareModelForSimpleCompletionMock: typeof import("../../../src/agents/simple-completion-transport.js").prepareModelForSimpleCompletion;
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
vi.mock("@mariozechner/pi-ai", async () => {
const original =
await vi.importActual<typeof import("@mariozechner/pi-ai")>("@mariozechner/pi-ai");
return {
...original,
completeSimple: vi.fn(),

View File

@ -1,7 +1,8 @@
import { vi } from "vitest";
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
vi.mock("@mariozechner/pi-ai", async () => {
const original =
await vi.importActual<typeof import("@mariozechner/pi-ai")>("@mariozechner/pi-ai");
return {
...original,
getOAuthApiKey: () => undefined,