fix: stabilize full gate

This commit is contained in:
Peter Steinberger 2026-03-17 06:53:29 +00:00
parent 026d8ea534
commit 5fb7a1363f
92 changed files with 1381 additions and 838 deletions

View File

@ -68,28 +68,59 @@ export function getUpsertChannelPairingRequestMock(): AnyAsyncMock {
return upsertChannelPairingRequest;
}
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
readChannelAllowFromStore,
upsertChannelPairingRequest,
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
readChannelAllowFromStore,
upsertChannelPairingRequest,
};
});
const skillCommandsHoisted = vi.hoisted(() => ({
listSkillCommandsForAgents: vi.fn(() => []),
replySpy: vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
await opts?.onReplyStart?.();
return undefined;
}) as MockFn<
(
ctx: MsgContext,
opts?: GetReplyOptions,
configOverride?: OpenClawConfig,
) => Promise<ReplyPayload | ReplyPayload[] | undefined>
>,
}));
export const listSkillCommandsForAgents = skillCommandsHoisted.listSkillCommandsForAgents;
export const replySpy = skillCommandsHoisted.replySpy;
vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
listSkillCommandsForAgents,
}));
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
return {
...actual,
listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents,
getReplyFromConfig: skillCommandsHoisted.replySpy,
__replySpy: skillCommandsHoisted.replySpy,
dispatchReplyWithBufferedBlockDispatcher: vi.fn(
async ({ ctx, replyOptions }: { ctx: MsgContext; replyOptions?: GetReplyOptions }) => {
await skillCommandsHoisted.replySpy(ctx, replyOptions);
return { queuedFinal: false };
},
),
};
});
const systemEventsHoisted = vi.hoisted(() => ({
enqueueSystemEventSpy: vi.fn(),
}));
export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy;
vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({
enqueueSystemEvent: enqueueSystemEventSpy,
}));
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
return {
...actual,
enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy,
};
});
const sentMessageCacheHoisted = vi.hoisted(() => ({
wasSentByBot: vi.fn(() => false),
@ -97,7 +128,7 @@ const sentMessageCacheHoisted = vi.hoisted(() => ({
export const wasSentByBot = sentMessageCacheHoisted.wasSentByBot;
vi.mock("./sent-message-cache.js", () => ({
wasSentByBot,
wasSentByBot: sentMessageCacheHoisted.wasSentByBot,
recordSentMessage: vi.fn(),
clearSentMessageCache: vi.fn(),
}));
@ -182,36 +213,24 @@ vi.mock("grammy", () => ({
InputFile: class {},
}));
const sequentializeMiddleware = vi.fn();
export const sequentializeSpy: AnyMock = vi.fn(() => sequentializeMiddleware);
const runnerHoisted = vi.hoisted(() => ({
sequentializeMiddleware: vi.fn(),
sequentializeSpy: vi.fn(),
throttlerSpy: vi.fn(() => "throttler"),
}));
export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy;
export let sequentializeKey: ((ctx: unknown) => string) | undefined;
vi.mock("@grammyjs/runner", () => ({
sequentialize: (keyFn: (ctx: unknown) => string) => {
sequentializeKey = keyFn;
return sequentializeSpy();
return runnerHoisted.sequentializeSpy();
},
}));
export const throttlerSpy: AnyMock = vi.fn(() => "throttler");
export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy;
vi.mock("@grammyjs/transformer-throttler", () => ({
apiThrottler: () => throttlerSpy(),
}));
export const replySpy: MockFn<
(
ctx: MsgContext,
opts?: GetReplyOptions,
configOverride?: OpenClawConfig,
) => Promise<ReplyPayload | ReplyPayload[] | undefined>
> = vi.fn(async (_ctx, opts) => {
await opts?.onReplyStart?.();
return undefined;
});
vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
getReplyFromConfig: replySpy,
__replySpy: replySpy,
apiThrottler: () => runnerHoisted.throttlerSpy(),
}));
export const getOnHandler = (event: string) => {

View File

@ -93,6 +93,19 @@ const unitIsolatedFilesRaw = [
"src/infra/git-commit.test.ts",
];
const unitIsolatedFiles = unitIsolatedFilesRaw.filter((file) => fs.existsSync(file));
const unitSingletonIsolatedFilesRaw = [];
const unitSingletonIsolatedFiles = unitSingletonIsolatedFilesRaw.filter((file) =>
fs.existsSync(file),
);
const unitVmForkSingletonFilesRaw = [
"src/channels/plugins/contracts/inbound.telegram.contract.test.ts",
];
const unitVmForkSingletonFiles = unitVmForkSingletonFilesRaw.filter((file) => fs.existsSync(file));
const groupedUnitIsolatedFiles = unitIsolatedFiles.filter(
(file) => !unitSingletonIsolatedFiles.includes(file),
);
const channelSingletonFilesRaw = [];
const channelSingletonFiles = channelSingletonFilesRaw.filter((file) => fs.existsSync(file));
const children = new Set();
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
@ -139,20 +152,55 @@ const runs = [
"vitest.unit.config.ts",
`--pool=${useVmForks ? "vmForks" : "forks"}`,
...(disableIsolation ? ["--isolate=false"] : []),
...unitIsolatedFiles.flatMap((file) => ["--exclude", file]),
...[
...unitIsolatedFiles,
...unitSingletonIsolatedFiles,
...unitVmForkSingletonFiles,
].flatMap((file) => ["--exclude", file]),
],
},
{
name: "unit-isolated",
...(groupedUnitIsolatedFiles.length > 0
? [
{
name: "unit-isolated",
args: [
"vitest",
"run",
"--config",
"vitest.unit.config.ts",
"--pool=forks",
...groupedUnitIsolatedFiles,
],
},
]
: []),
...unitSingletonIsolatedFiles.map((file) => ({
name: `${path.basename(file, ".test.ts")}-isolated`,
args: [
"vitest",
"run",
"--config",
"vitest.unit.config.ts",
"--pool=forks",
...unitIsolatedFiles,
`--pool=${useVmForks ? "vmForks" : "forks"}`,
file,
],
},
})),
...unitVmForkSingletonFiles.map((file) => ({
name: `${path.basename(file, ".test.ts")}-vmforks`,
args: [
"vitest",
"run",
"--config",
"vitest.unit.config.ts",
`--pool=${useVmForks ? "vmForks" : "forks"}`,
...(disableIsolation ? ["--isolate=false"] : []),
file,
],
})),
...channelSingletonFiles.map((file) => ({
name: `${path.basename(file, ".test.ts")}-channels-isolated`,
args: ["vitest", "run", "--config", "vitest.channels.config.ts", "--pool=forks", file],
})),
]
: [
{
@ -380,9 +428,24 @@ const resolveFilterMatches = (fileFilter) => {
}
return allKnownTestFiles.filter((file) => file.includes(normalizedFilter));
};
const isVmForkSingletonUnitFile = (fileFilter) => unitVmForkSingletonFiles.includes(fileFilter);
const createTargetedEntry = (owner, isolated, filters) => {
const name = isolated ? `${owner}-isolated` : owner;
const forceForks = isolated;
if (owner === "unit-vmforks") {
return {
name,
args: [
"vitest",
"run",
"--config",
"vitest.unit.config.ts",
`--pool=${useVmForks ? "vmForks" : "forks"}`,
...(disableIsolation ? ["--isolate=false"] : []),
...filters,
],
};
}
if (owner === "unit") {
return {
name,
@ -460,16 +523,19 @@ const targetedEntries = (() => {
const groups = passthroughFileFilters.reduce((acc, fileFilter) => {
const matchedFiles = resolveFilterMatches(fileFilter);
if (matchedFiles.length === 0) {
const target = inferTarget(normalizeRepoPath(fileFilter));
const key = `${target.owner}:${target.isolated ? "isolated" : "default"}`;
const normalizedFile = normalizeRepoPath(fileFilter);
const target = inferTarget(normalizedFile);
const owner = isVmForkSingletonUnitFile(normalizedFile) ? "unit-vmforks" : target.owner;
const key = `${owner}:${target.isolated ? "isolated" : "default"}`;
const files = acc.get(key) ?? [];
files.push(normalizeRepoPath(fileFilter));
files.push(normalizedFile);
acc.set(key, files);
return acc;
}
for (const matchedFile of matchedFiles) {
const target = inferTarget(matchedFile);
const key = `${target.owner}:${target.isolated ? "isolated" : "default"}`;
const owner = isVmForkSingletonUnitFile(matchedFile) ? "unit-vmforks" : target.owner;
const key = `${owner}:${target.isolated ? "isolated" : "default"}`;
const files = acc.get(key) ?? [];
files.push(matchedFile);
acc.set(key, files);

View File

@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
import { AcpRuntimeError } from "../runtime/errors.js";
import type { AcpRuntime, AcpRuntimeCapabilities } from "../runtime/types.js";
const hoisted = vi.hoisted(() => {
@ -32,7 +31,8 @@ vi.mock("../runtime/registry.js", async (importOriginal) => {
};
});
const { AcpSessionManager } = await import("./manager.js");
let AcpSessionManager: typeof import("./manager.js").AcpSessionManager;
let AcpRuntimeError: typeof import("../runtime/errors.js").AcpRuntimeError;
const baseCfg = {
acp: {
@ -146,7 +146,10 @@ function extractRuntimeOptionsFromUpserts(): Array<AcpSessionRuntimeOptions | un
}
describe("AcpSessionManager", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ AcpSessionManager } = await import("./manager.js"));
({ AcpRuntimeError } = await import("../runtime/errors.js"));
hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]);
hoisted.readAcpSessionEntryMock.mockReset();
hoisted.upsertAcpSessionMetaMock.mockReset().mockResolvedValue(null);

View File

@ -27,13 +27,13 @@ vi.mock("./runtime/session-meta.js", () => ({
readAcpSessionEntry: sessionMetaMocks.readAcpSessionEntry,
}));
import {
buildConfiguredAcpSessionKey,
ensureConfiguredAcpBindingSession,
resetAcpSessionInPlace,
resolveConfiguredAcpBindingRecord,
resolveConfiguredAcpBindingSpecBySessionKey,
} from "./persistent-bindings.js";
type PersistentBindingsModule = typeof import("./persistent-bindings.js");
let buildConfiguredAcpSessionKey: PersistentBindingsModule["buildConfiguredAcpSessionKey"];
let ensureConfiguredAcpBindingSession: PersistentBindingsModule["ensureConfiguredAcpBindingSession"];
let resetAcpSessionInPlace: PersistentBindingsModule["resetAcpSessionInPlace"];
let resolveConfiguredAcpBindingRecord: PersistentBindingsModule["resolveConfiguredAcpBindingRecord"];
let resolveConfiguredAcpBindingSpecBySessionKey: PersistentBindingsModule["resolveConfiguredAcpBindingSpecBySessionKey"];
type ConfiguredBinding = NonNullable<OpenClawConfig["bindings"]>[number];
type BindingRecordInput = Parameters<typeof resolveConfiguredAcpBindingRecord>[0];
@ -184,6 +184,17 @@ beforeEach(() => {
sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined);
});
beforeEach(async () => {
vi.resetModules();
({
buildConfiguredAcpSessionKey,
ensureConfiguredAcpBindingSession,
resetAcpSessionInPlace,
resolveConfiguredAcpBindingRecord,
resolveConfiguredAcpBindingSpecBySessionKey,
} = await import("./persistent-bindings.js"));
});
describe("resolveConfiguredAcpBindingRecord", () => {
it("resolves discord channel ACP binding from top-level typed bindings", () => {
const cfg = createCfgWithBindings([

View File

@ -22,10 +22,14 @@ vi.mock("../../config/sessions.js", async () => {
};
});
const { listAcpSessionEntries } = await import("./session-meta.js");
type SessionMetaModule = typeof import("./session-meta.js");
let listAcpSessionEntries: SessionMetaModule["listAcpSessionEntries"];
describe("listAcpSessionEntries", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ listAcpSessionEntries } = await import("./session-meta.js"));
vi.clearAllMocks();
});

View File

@ -24,11 +24,11 @@ vi.mock("../../../agents/tools/slack-actions.js", () => ({
handleSlackAction,
}));
const { discordMessageActions } = await import("./discord.js");
const { handleDiscordMessageAction } = await import("./discord/handle-action.js");
const { telegramMessageActions } = await import("./telegram.js");
const { signalMessageActions } = await import("./signal.js");
const { createSlackActions } = await import("../slack.actions.js");
let discordMessageActions: typeof import("./discord.js").discordMessageActions;
let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction;
let telegramMessageActions: typeof import("./telegram.js").telegramMessageActions;
let signalMessageActions: typeof import("./signal.js").signalMessageActions;
let createSlackActions: typeof import("../slack.actions.js").createSlackActions;
function telegramCfg(): OpenClawConfig {
return { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
@ -191,7 +191,13 @@ async function expectSlackSendRejected(params: Record<string, unknown>, error: R
expect(handleSlackAction).not.toHaveBeenCalled();
}
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ discordMessageActions } = await import("./discord.js"));
({ handleDiscordMessageAction } = await import("./discord/handle-action.js"));
({ telegramMessageActions } = await import("./telegram.js"));
({ signalMessageActions } = await import("./signal.js"));
({ createSlackActions } = await import("../slack.actions.js"));
vi.clearAllMocks();
});

View File

@ -1,18 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../config/sessions.js", () => ({
loadSessionStore: vi.fn(),
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
}));
vi.mock("../../pairing/pairing-store.js", () => ({
readChannelAllowFromStoreSync: vi.fn(() => []),
}));
import type { OpenClawConfig } from "../../config/config.js";
import { loadSessionStore } from "../../config/sessions.js";
import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js";
import { resolveWhatsAppHeartbeatRecipients } from "./whatsapp-heartbeat.js";
const loadSessionStoreMock = vi.hoisted(() => vi.fn());
const readChannelAllowFromStoreSyncMock = vi.hoisted(() => vi.fn<() => string[]>(() => []));
type WhatsAppHeartbeatModule = typeof import("./whatsapp-heartbeat.js");
let resolveWhatsAppHeartbeatRecipients: WhatsAppHeartbeatModule["resolveWhatsAppHeartbeatRecipients"];
function makeCfg(overrides?: Partial<OpenClawConfig>): OpenClawConfig {
return {
@ -23,12 +17,12 @@ function makeCfg(overrides?: Partial<OpenClawConfig>): OpenClawConfig {
}
describe("resolveWhatsAppHeartbeatRecipients", () => {
function setSessionStore(store: ReturnType<typeof loadSessionStore>) {
vi.mocked(loadSessionStore).mockReturnValue(store);
function setSessionStore(store: Record<string, unknown>) {
loadSessionStoreMock.mockReturnValue(store);
}
function setAllowFromStore(entries: string[]) {
vi.mocked(readChannelAllowFromStoreSync).mockReturnValue(entries);
readChannelAllowFromStoreSyncMock.mockReturnValue(entries);
}
function resolveWith(
@ -45,9 +39,18 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
setAllowFromStore(["+15550000001"]);
}
beforeEach(() => {
vi.mocked(loadSessionStore).mockClear();
vi.mocked(readChannelAllowFromStoreSync).mockClear();
beforeEach(async () => {
vi.resetModules();
loadSessionStoreMock.mockReset();
readChannelAllowFromStoreSyncMock.mockReset();
vi.doMock("../../config/sessions.js", () => ({
loadSessionStore: loadSessionStoreMock,
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
}));
vi.doMock("../../pairing/pairing-store.js", () => ({
readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock,
}));
({ resolveWhatsAppHeartbeatRecipients } = await import("./whatsapp-heartbeat.js"));
setAllowFromStore([]);
});

View File

@ -9,6 +9,10 @@ vi.mock("../config/sessions.js", () => ({
updateLastRoute: (args: unknown) => updateLastRouteMock(args),
}));
type SessionModule = typeof import("./session.js");
let recordInboundSession: SessionModule["recordInboundSession"];
describe("recordInboundSession", () => {
const ctx: MsgContext = {
Provider: "telegram",
@ -17,14 +21,14 @@ describe("recordInboundSession", () => {
OriginatingTo: "telegram:1234",
};
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ recordInboundSession } = await import("./session.js"));
recordSessionMetaFromInboundMock.mockClear();
updateLastRouteMock.mockClear();
});
it("does not pass ctx when updating a different session key", async () => {
const { recordInboundSession } = await import("./session.js");
await recordInboundSession({
storePath: "/tmp/openclaw-session-store.json",
sessionKey: "agent:main:telegram:1234:thread:42",
@ -50,8 +54,6 @@ describe("recordInboundSession", () => {
});
it("passes ctx when updating the same session key", async () => {
const { recordInboundSession } = await import("./session.js");
await recordInboundSession({
storePath: "/tmp/openclaw-session-store.json",
sessionKey: "agent:main:telegram:1234:thread:42",
@ -77,8 +79,6 @@ describe("recordInboundSession", () => {
});
it("normalizes mixed-case session keys before recording and route updates", async () => {
const { recordInboundSession } = await import("./session.js");
await recordInboundSession({
storePath: "/tmp/openclaw-session-store.json",
sessionKey: "Agent:Main:Telegram:1234:Thread:42",
@ -105,7 +105,6 @@ describe("recordInboundSession", () => {
});
it("skips last-route updates when main DM owner pin mismatches sender", async () => {
const { recordInboundSession } = await import("./session.js");
const onSkip = vi.fn();
await recordInboundSession({

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const callGateway = vi.fn();
@ -7,7 +7,13 @@ vi.mock("../gateway/call.js", () => ({
callGateway,
}));
const { resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js");
let resolveCommandSecretRefsViaGateway: typeof import("./command-secret-gateway.js").resolveCommandSecretRefsViaGateway;
beforeEach(async () => {
vi.resetModules();
callGateway.mockReset();
({ resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js"));
});
describe("resolveCommandSecretRefsViaGateway", () => {
function makeTalkApiKeySecretRefConfig(envKey: string): OpenClawConfig {

View File

@ -2,15 +2,17 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const getMemorySearchManager = vi.fn();
const loadConfig = vi.fn(() => ({}));
const resolveDefaultAgentId = vi.fn(() => "main");
const resolveCommandSecretRefsViaGateway = vi.fn(async ({ config }: { config: unknown }) => ({
resolvedConfig: config,
diagnostics: [] as string[],
}));
const getMemorySearchManager = vi.hoisted(() => vi.fn());
const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
const resolveDefaultAgentId = vi.hoisted(() => vi.fn(() => "main"));
const resolveCommandSecretRefsViaGateway = vi.hoisted(() =>
vi.fn(async ({ config }: { config: unknown }) => ({
resolvedConfig: config,
diagnostics: [] as string[],
})),
);
vi.mock("../memory/index.js", () => ({
getMemorySearchManager,
@ -33,7 +35,8 @@ let defaultRuntime: typeof import("../runtime.js").defaultRuntime;
let isVerbose: typeof import("../globals.js").isVerbose;
let setVerbose: typeof import("../globals.js").setVerbose;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ registerMemoryCli } = await import("./memory-cli.js"));
({ defaultRuntime } = await import("../runtime.js"));
({ isVerbose, setVerbose } = await import("../globals.js"));

View File

@ -1,5 +1,5 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const listChannelPairingRequests = vi.fn();
const approveChannelPairingCode = vi.fn();
@ -47,11 +47,9 @@ vi.mock("../config/config.js", () => ({
describe("pairing cli", () => {
let registerPairingCli: typeof import("./pairing-cli.js").registerPairingCli;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ registerPairingCli } = await import("./pairing-cli.js"));
});
beforeEach(() => {
listChannelPairingRequests.mockClear();
listChannelPairingRequests.mockResolvedValue([]);
approveChannelPairingCode.mockClear();

View File

@ -1,12 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { isYes, setVerbose, setYes } from "../globals.js";
vi.mock("node:readline/promises", () => {
const question = vi.fn(async () => "");
const close = vi.fn();
const createInterface = vi.fn(() => ({ question, close }));
return { default: { createInterface } };
});
import { beforeEach, describe, expect, it, vi } from "vitest";
type ReadlineMock = {
default: {
@ -17,8 +9,27 @@ type ReadlineMock = {
};
};
const { promptYesNo } = await import("./prompt.js");
const readline = (await import("node:readline/promises")) as unknown as ReadlineMock;
type PromptModule = typeof import("./prompt.js");
type GlobalsModule = typeof import("../globals.js");
let promptYesNo: PromptModule["promptYesNo"];
let readline: ReadlineMock;
let isYes: GlobalsModule["isYes"];
let setVerbose: GlobalsModule["setVerbose"];
let setYes: GlobalsModule["setYes"];
beforeEach(async () => {
vi.resetModules();
vi.doMock("node:readline/promises", () => {
const question = vi.fn(async () => "");
const close = vi.fn();
const createInterface = vi.fn(() => ({ question, close }));
return { default: { createInterface } };
});
({ promptYesNo } = await import("./prompt.js"));
({ isYes, setVerbose, setYes } = await import("../globals.js"));
readline = (await import("node:readline/promises")) as unknown as ReadlineMock;
});
describe("promptYesNo", () => {
it("returns true when global --yes is set", async () => {

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
createConfigIO: vi.fn().mockReturnValue({
@ -10,7 +10,13 @@ vi.mock("./io.js", () => ({
createConfigIO: mocks.createConfigIO,
}));
import { formatConfigPath, logConfigUpdated } from "./logging.js";
let formatConfigPath: typeof import("./logging.js").formatConfigPath;
let logConfigUpdated: typeof import("./logging.js").logConfigUpdated;
beforeEach(async () => {
vi.resetModules();
({ formatConfigPath, logConfigUpdated } = await import("./logging.js"));
});
describe("config logging", () => {
it("formats the live config path when no explicit path is provided", () => {

View File

@ -17,7 +17,8 @@ vi.mock("./store.js", () => ({
loadSessionStore: () => storeState.store,
}));
import { extractDeliveryInfo, parseSessionThreadInfo } from "./delivery-info.js";
let extractDeliveryInfo: typeof import("./delivery-info.js").extractDeliveryInfo;
let parseSessionThreadInfo: typeof import("./delivery-info.js").parseSessionThreadInfo;
const buildEntry = (deliveryContext: SessionEntry["deliveryContext"]): SessionEntry => ({
sessionId: "session-1",
@ -25,8 +26,10 @@ const buildEntry = (deliveryContext: SessionEntry["deliveryContext"]): SessionEn
deliveryContext,
});
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
storeState.store = {};
({ extractDeliveryInfo, parseSessionThreadInfo } = await import("./delivery-info.js"));
});
describe("extractDeliveryInfo", () => {

View File

@ -3,15 +3,19 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore } from "./store.js";
import type { SessionEntry } from "./types.js";
// Keep integration tests deterministic: never read a real openclaw.json.
vi.mock("../config.js", () => ({
loadConfig: vi.fn().mockReturnValue({}),
}));
const { loadConfig } = await import("../config.js");
const mockLoadConfig = vi.mocked(loadConfig) as ReturnType<typeof vi.fn>;
type StoreModule = typeof import("./store.js");
let clearSessionStoreCacheForTest: StoreModule["clearSessionStoreCacheForTest"];
let loadSessionStore: StoreModule["loadSessionStore"];
let saveSessionStore: StoreModule["saveSessionStore"];
let mockLoadConfig: ReturnType<typeof vi.fn>;
const DAY_MS = 24 * 60 * 60 * 1000;
@ -77,6 +81,11 @@ describe("Integration: saveSessionStore with pruning", () => {
});
beforeEach(async () => {
vi.resetModules();
({ clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore } =
await import("./store.js"));
const { loadConfig } = await import("../config.js");
mockLoadConfig = vi.mocked(loadConfig) as ReturnType<typeof vi.fn>;
testDir = await createCaseDir("pruning-integ");
storePath = path.join(testDir, "sessions.json");
savedCacheTtl = process.env.OPENCLAW_SESSION_CACHE_TTL_MS;

View File

@ -1,44 +1,51 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { parseTelegramTarget } from "../../extensions/telegram/src/targets.js";
import type { OpenClawConfig } from "../config/config.js";
// Mock session store so we can control what entries exist.
const mockStore: Record<string, Record<string, unknown>> = {};
vi.mock("../config/sessions.js", () => ({
loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}),
resolveAgentMainSessionKey: vi.fn(({ agentId }: { agentId: string }) => `agent:${agentId}:main`),
resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"),
}));
type DeliveryTargetModule = typeof import("./isolated-agent/delivery-target.js");
// Mock channel-selection to avoid real config resolution.
vi.mock("../infra/outbound/channel-selection.js", () => ({
resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })),
}));
let resolveDeliveryTarget: DeliveryTargetModule["resolveDeliveryTarget"];
// Minimal mock for channel plugins (Telegram resolveTarget is an identity).
vi.mock("../channels/plugins/index.js", () => ({
getChannelPlugin: vi.fn(() => ({
meta: { label: "Telegram" },
config: {},
messaging: {
parseExplicitTarget: ({ raw }: { raw: string }) => {
const target = parseTelegramTarget(raw);
return {
to: target.chatId,
threadId: target.messageThreadId,
chatType: target.chatType === "unknown" ? undefined : target.chatType,
};
beforeEach(async () => {
vi.resetModules();
for (const key of Object.keys(mockStore)) {
delete mockStore[key];
}
vi.doMock("../config/sessions.js", () => ({
loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}),
resolveAgentMainSessionKey: vi.fn(
({ agentId }: { agentId: string }) => `agent:${agentId}:main`,
),
resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"),
}));
vi.doMock("../infra/outbound/channel-selection.js", () => ({
resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })),
}));
vi.doMock("../channels/plugins/index.js", () => ({
getChannelPlugin: vi.fn(() => ({
meta: { label: "Telegram" },
config: {},
messaging: {
parseExplicitTarget: ({ raw }: { raw: string }) => {
const target = parseTelegramTarget(raw);
return {
to: target.chatId,
threadId: target.messageThreadId,
chatType: target.chatType === "unknown" ? undefined : target.chatType,
};
},
},
},
outbound: {
resolveTarget: ({ to }: { to?: string }) =>
to ? { ok: true, to } : { ok: false, error: new Error("missing") },
},
})),
normalizeChannelId: vi.fn((id: string) => id),
}));
const { resolveDeliveryTarget } = await import("./isolated-agent/delivery-target.js");
outbound: {
resolveTarget: ({ to }: { to?: string }) =>
to ? { ok: true, to } : { ok: false, error: new Error("missing") },
},
})),
normalizeChannelId: vi.fn((id: string) => id),
}));
({ resolveDeliveryTarget } = await import("./isolated-agent/delivery-target.js"));
});
describe("resolveDeliveryTarget thread session lookup", () => {
const cfg: OpenClawConfig = {};

View File

@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearFastTestEnv,
loadRunCronIsolatedAgentTurn,
@ -8,8 +8,11 @@ import {
runWithModelFallbackMock,
} from "./run.test-harness.js";
const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
const { resolveSandboxConfigForAgent } = await import("../../agents/sandbox/config.js");
type RunModule = typeof import("./run.js");
type SandboxConfigModule = typeof import("../../agents/sandbox/config.js");
let runCronIsolatedAgentTurn: RunModule["runCronIsolatedAgentTurn"];
let resolveSandboxConfigForAgent: SandboxConfigModule["resolveSandboxConfigForAgent"];
function makeJob(overrides?: Record<string, unknown>) {
return {
@ -82,7 +85,10 @@ function expectDefaultSandboxPreserved(
describe("runCronIsolatedAgentTurn sandbox config preserved", () => {
let previousFastTestEnv: string | undefined;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
({ resolveSandboxConfigForAgent } = await import("../../agents/sandbox/config.js"));
previousFastTestEnv = clearFastTestEnv();
resetRunCronIsolatedAgentTurnHarness();
});

View File

@ -4,7 +4,6 @@ import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
import type { CronEvent, CronServiceDeps } from "./service.js";
import { CronService } from "./service.js";
import { createDeferred, createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
import { loadCronStore } from "./store.js";
const noopLogger = createNoopLogger();
installCronTestHooks({ logger: noopLogger });
@ -60,10 +59,6 @@ async function makeStorePath() {
return { storePath, cleanup: async () => {} };
}
function writeStoreFile(storePath: string, payload: unknown) {
setFile(storePath, JSON.stringify(payload, null, 2));
}
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
const pathMod = await import("node:path");
@ -415,14 +410,6 @@ async function createMainOneShotJobHarness(params: { name: string; deleteAfterRu
return { ...harness, atMs, job };
}
async function loadLegacyDeliveryMigrationByPayload(params: {
id: string;
payload: { provider?: string; channel?: string };
}) {
const rawJob = createLegacyDeliveryMigrationJob(params);
return loadLegacyDeliveryMigration(rawJob);
}
async function expectNoMainSummaryForIsolatedRun(params: {
runIsolatedAgentJob: CronServiceDeps["runIsolatedAgentJob"];
name: string;
@ -439,43 +426,6 @@ async function expectNoMainSummaryForIsolatedRun(params: {
await stopCronAndCleanup(cron, store);
}
function createLegacyDeliveryMigrationJob(options: {
id: string;
payload: { provider?: string; channel?: string };
}) {
return {
id: options.id,
name: "legacy",
enabled: true,
createdAtMs: Date.now(),
updatedAtMs: Date.now(),
schedule: { kind: "cron", expr: "* * * * *" },
sessionTarget: "isolated",
wakeMode: "now",
payload: {
kind: "agentTurn",
message: "hi",
deliver: true,
...options.payload,
to: "7200373102",
},
state: {},
};
}
async function loadLegacyDeliveryMigration(rawJob: Record<string, unknown>) {
ensureDir(fixturesRoot);
const store = await makeStorePath();
writeStoreFile(store.storePath, { version: 1, jobs: [rawJob] });
const cron = createStartedCronService(store.storePath);
await cron.start();
cron.stop();
const loaded = await loadCronStore(store.storePath);
const job = loaded.jobs.find((j) => j.id === rawJob.id);
return { store, cron, job };
}
describe("CronService", () => {
it("runs a one-shot main job and disables it after success when requested", async () => {
const { store, cron, enqueueSystemEvent, requestHeartbeatNow, events, atMs, job } =
@ -658,33 +608,6 @@ describe("CronService", () => {
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1);
});
it("migrates legacy payload.provider to payload.channel on load", async () => {
const { store, cron, job } = await loadLegacyDeliveryMigrationByPayload({
id: "legacy-1",
payload: { provider: " TeLeGrAm " },
});
// Legacy delivery fields are migrated to the top-level delivery object
const delivery = job?.delivery as unknown as Record<string, unknown>;
expect(delivery?.channel).toBe("telegram");
const payload = job?.payload as unknown as Record<string, unknown>;
expect("provider" in payload).toBe(false);
expect("channel" in payload).toBe(false);
await stopCronAndCleanup(cron, store);
});
it("canonicalizes payload.channel casing on load", async () => {
const { store, cron, job } = await loadLegacyDeliveryMigrationByPayload({
id: "legacy-2",
payload: { channel: "Telegram" },
});
// Legacy delivery fields are migrated to the top-level delivery object
const delivery = job?.delivery as unknown as Record<string, unknown>;
expect(delivery?.channel).toBe("telegram");
await stopCronAndCleanup(cron, store);
});
it("does not post a fallback main summary when an isolated job errors", async () => {
const runIsolatedAgentJob = vi.fn(async () => ({
status: "error" as const,
@ -764,60 +687,4 @@ describe("CronService", () => {
cron.stop();
await store.cleanup();
});
it("skips invalid main jobs with agentTurn payloads from disk", async () => {
ensureDir(fixturesRoot);
const store = await makeStorePath();
const enqueueSystemEvent = vi.fn();
const requestHeartbeatNow = vi.fn();
const events = createCronEventHarness();
const atMs = Date.parse("2025-12-13T00:00:01.000Z");
writeStoreFile(store.storePath, {
version: 1,
jobs: [
{
id: "job-1",
enabled: true,
createdAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
updatedAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
schedule: { kind: "at", at: new Date(atMs).toISOString() },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "agentTurn", message: "bad" },
state: {},
},
],
});
const cron = new CronService({
storePath: store.storePath,
cronEnabled: true,
log: noopLogger,
enqueueSystemEvent,
requestHeartbeatNow,
runIsolatedAgentJob: vi.fn(async (_params: { job: unknown; message: string }) => ({
status: "ok",
})) as unknown as CronServiceDeps["runIsolatedAgentJob"],
onEvent: events.onEvent,
});
await cron.start();
vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z"));
await vi.runOnlyPendingTimersAsync();
await events.waitFor(
(evt) => evt.jobId === "job-1" && evt.action === "finished" && evt.status === "skipped",
);
expect(enqueueSystemEvent).not.toHaveBeenCalled();
expect(requestHeartbeatNow).not.toHaveBeenCalled();
const jobs = await cron.list({ includeDisabled: true });
expect(jobs[0]?.state.lastStatus).toBe("skipped");
expect(jobs[0]?.state.lastError).toMatch(/main job requires/i);
cron.stop();
await store.cleanup();
});
});

View File

@ -14,11 +14,15 @@ vi.mock("./safe-open-sync.js", () => ({
openVerifiedFileSync: (...args: unknown[]) => openVerifiedFileSyncMock(...args),
}));
const { canUseBoundaryFileOpen, openBoundaryFile, openBoundaryFileSync } =
await import("./boundary-file-read.js");
let canUseBoundaryFileOpen: typeof import("./boundary-file-read.js").canUseBoundaryFileOpen;
let openBoundaryFile: typeof import("./boundary-file-read.js").openBoundaryFile;
let openBoundaryFileSync: typeof import("./boundary-file-read.js").openBoundaryFileSync;
describe("boundary-file-read", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ canUseBoundaryFileOpen, openBoundaryFile, openBoundaryFileSync } =
await import("./boundary-file-read.js"));
resolveBoundaryPathSyncMock.mockReset();
resolveBoundaryPathMock.mockReset();
openVerifiedFileSyncMock.mockReset();

View File

@ -1,12 +1,18 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.js";
vi.mock("../channels/plugins/index.js", () => ({
listChannelPlugins: vi.fn(),
}));
const { buildChannelSummary } = await import("./channel-summary.js");
const { listChannelPlugins } = await import("../channels/plugins/index.js");
let buildChannelSummary: typeof import("./channel-summary.js").buildChannelSummary;
let listChannelPlugins: typeof import("../channels/plugins/index.js").listChannelPlugins;
beforeEach(async () => {
vi.resetModules();
({ buildChannelSummary } = await import("./channel-summary.js"));
({ listChannelPlugins } = await import("../channels/plugins/index.js"));
});
function makeSlackHttpSummaryPlugin(): ChannelPlugin {
return {

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withEnv } from "../test-utils/env.js";
const loggerMocks = vi.hoisted(() => ({
@ -11,7 +11,18 @@ vi.mock("../logging/subsystem.js", () => ({
}),
}));
import { isTruthyEnvValue, logAcceptedEnvOption, normalizeEnv, normalizeZaiEnv } from "./env.js";
type EnvModule = typeof import("./env.js");
let isTruthyEnvValue: EnvModule["isTruthyEnvValue"];
let logAcceptedEnvOption: EnvModule["logAcceptedEnvOption"];
let normalizeEnv: EnvModule["normalizeEnv"];
let normalizeZaiEnv: EnvModule["normalizeZaiEnv"];
beforeEach(async () => {
vi.resetModules();
({ isTruthyEnvValue, logAcceptedEnvOption, normalizeEnv, normalizeZaiEnv } =
await import("./env.js"));
});
describe("normalizeZaiEnv", () => {
it("copies Z_AI_API_KEY to ZAI_API_KEY when missing", () => {

View File

@ -5,51 +5,14 @@ const getChannelPluginMock = vi.hoisted(() => vi.fn());
const listChannelPluginsMock = vi.hoisted(() => vi.fn());
const normalizeMessageChannelMock = vi.hoisted(() => vi.fn());
vi.mock("../config/config.js", () => ({
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
}));
type ExecApprovalSurfaceModule = typeof import("./exec-approval-surface.js");
vi.mock("../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args),
}));
vi.mock("../../extensions/discord/src/channel.js", () => ({
discordPlugin: {},
}));
vi.mock("../../extensions/telegram/src/channel.js", () => ({
telegramPlugin: {},
}));
vi.mock("../../extensions/slack/src/channel.js", () => ({
slackPlugin: {},
}));
vi.mock("../../extensions/whatsapp/src/channel.js", () => ({
whatsappPlugin: {},
}));
vi.mock("../../extensions/signal/src/channel.js", () => ({
signalPlugin: {},
}));
vi.mock("../../extensions/imessage/src/channel.js", () => ({
imessagePlugin: {},
}));
vi.mock("../utils/message-channel.js", () => ({
INTERNAL_MESSAGE_CHANNEL: "web",
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),
}));
import {
hasConfiguredExecApprovalDmRoute,
resolveExecApprovalInitiatingSurfaceState,
} from "./exec-approval-surface.js";
let hasConfiguredExecApprovalDmRoute: ExecApprovalSurfaceModule["hasConfiguredExecApprovalDmRoute"];
let resolveExecApprovalInitiatingSurfaceState: ExecApprovalSurfaceModule["resolveExecApprovalInitiatingSurfaceState"];
describe("resolveExecApprovalInitiatingSurfaceState", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
loadConfigMock.mockReset();
getChannelPluginMock.mockReset();
listChannelPluginsMock.mockReset();
@ -57,6 +20,37 @@ describe("resolveExecApprovalInitiatingSurfaceState", () => {
normalizeMessageChannelMock.mockImplementation((value?: string | null) =>
typeof value === "string" ? value.trim().toLowerCase() : undefined,
);
vi.doMock("../config/config.js", () => ({
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
}));
vi.doMock("../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args),
}));
vi.doMock("../../extensions/discord/src/channel.js", () => ({
discordPlugin: {},
}));
vi.doMock("../../extensions/telegram/src/channel.js", () => ({
telegramPlugin: {},
}));
vi.doMock("../../extensions/slack/src/channel.js", () => ({
slackPlugin: {},
}));
vi.doMock("../../extensions/whatsapp/src/channel.js", () => ({
whatsappPlugin: {},
}));
vi.doMock("../../extensions/signal/src/channel.js", () => ({
signalPlugin: {},
}));
vi.doMock("../../extensions/imessage/src/channel.js", () => ({
imessagePlugin: {},
}));
vi.doMock("../utils/message-channel.js", () => ({
INTERNAL_MESSAGE_CHANNEL: "web",
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),
}));
({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } =
await import("./exec-approval-surface.js"));
});
it("treats web UI, terminal UI, and missing channels as enabled", () => {
@ -154,8 +148,46 @@ describe("resolveExecApprovalInitiatingSurfaceState", () => {
});
describe("hasConfiguredExecApprovalDmRoute", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
loadConfigMock.mockReset();
getChannelPluginMock.mockReset();
listChannelPluginsMock.mockReset();
normalizeMessageChannelMock.mockReset();
normalizeMessageChannelMock.mockImplementation((value?: string | null) =>
typeof value === "string" ? value.trim().toLowerCase() : undefined,
);
vi.doMock("../config/config.js", () => ({
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
}));
vi.doMock("../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args),
}));
vi.doMock("../../extensions/discord/src/channel.js", () => ({
discordPlugin: {},
}));
vi.doMock("../../extensions/telegram/src/channel.js", () => ({
telegramPlugin: {},
}));
vi.doMock("../../extensions/slack/src/channel.js", () => ({
slackPlugin: {},
}));
vi.doMock("../../extensions/whatsapp/src/channel.js", () => ({
whatsappPlugin: {},
}));
vi.doMock("../../extensions/signal/src/channel.js", () => ({
signalPlugin: {},
}));
vi.doMock("../../extensions/imessage/src/channel.js", () => ({
imessagePlugin: {},
}));
vi.doMock("../utils/message-channel.js", () => ({
INTERNAL_MESSAGE_CHANNEL: "web",
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),
}));
({ hasConfiguredExecApprovalDmRoute, resolveExecApprovalInitiatingSurfaceState } =
await import("./exec-approval-surface.js"));
});
it("returns true when any enabled account routes approvals to DM or both", () => {

View File

@ -9,23 +9,36 @@ vi.mock("./jsonl-socket.js", () => ({
requestJsonlSocket: (...args: unknown[]) => requestJsonlSocketMock(...args),
}));
import {
addAllowlistEntry,
ensureExecApprovals,
mergeExecApprovalsSocketDefaults,
normalizeExecApprovals,
readExecApprovalsSnapshot,
recordAllowlistUse,
requestExecApprovalViaSocket,
resolveExecApprovalsPath,
resolveExecApprovalsSocketPath,
type ExecApprovalsFile,
} from "./exec-approvals.js";
import type { ExecApprovalsFile } from "./exec-approvals.js";
type ExecApprovalsModule = typeof import("./exec-approvals.js");
let addAllowlistEntry: ExecApprovalsModule["addAllowlistEntry"];
let ensureExecApprovals: ExecApprovalsModule["ensureExecApprovals"];
let mergeExecApprovalsSocketDefaults: ExecApprovalsModule["mergeExecApprovalsSocketDefaults"];
let normalizeExecApprovals: ExecApprovalsModule["normalizeExecApprovals"];
let readExecApprovalsSnapshot: ExecApprovalsModule["readExecApprovalsSnapshot"];
let recordAllowlistUse: ExecApprovalsModule["recordAllowlistUse"];
let requestExecApprovalViaSocket: ExecApprovalsModule["requestExecApprovalViaSocket"];
let resolveExecApprovalsPath: ExecApprovalsModule["resolveExecApprovalsPath"];
let resolveExecApprovalsSocketPath: ExecApprovalsModule["resolveExecApprovalsSocketPath"];
const tempDirs: string[] = [];
const originalOpenClawHome = process.env.OPENCLAW_HOME;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({
addAllowlistEntry,
ensureExecApprovals,
mergeExecApprovalsSocketDefaults,
normalizeExecApprovals,
readExecApprovalsSnapshot,
recordAllowlistUse,
requestExecApprovalViaSocket,
resolveExecApprovalsPath,
resolveExecApprovalsSocketPath,
} = await import("./exec-approvals.js"));
requestJsonlSocketMock.mockReset();
});

View File

@ -51,12 +51,10 @@ vi.mock("undici", () => ({
fetch: undiciFetch,
}));
import {
getProxyUrlFromFetch,
makeProxyFetch,
PROXY_FETCH_PROXY_URL,
resolveProxyFetchFromEnv,
} from "./proxy-fetch.js";
let getProxyUrlFromFetch: typeof import("./proxy-fetch.js").getProxyUrlFromFetch;
let makeProxyFetch: typeof import("./proxy-fetch.js").makeProxyFetch;
let PROXY_FETCH_PROXY_URL: typeof import("./proxy-fetch.js").PROXY_FETCH_PROXY_URL;
let resolveProxyFetchFromEnv: typeof import("./proxy-fetch.js").resolveProxyFetchFromEnv;
function clearProxyEnv(): void {
for (const key of PROXY_ENV_KEYS) {
@ -75,7 +73,12 @@ function restoreProxyEnv(): void {
}
describe("makeProxyFetch", () => {
beforeEach(() => vi.clearAllMocks());
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ getProxyUrlFromFetch, makeProxyFetch, PROXY_FETCH_PROXY_URL, resolveProxyFetchFromEnv } =
await import("./proxy-fetch.js"));
});
it("uses undici fetch with ProxyAgent dispatcher", async () => {
const proxyUrl = "http://proxy.test:8080";

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { agentCtor, envHttpProxyAgentCtor, proxyAgentCtor } = vi.hoisted(() => ({
agentCtor: vi.fn(function MockAgent(this: { options: unknown }, options: unknown) {
@ -21,7 +21,14 @@ vi.mock("undici", () => ({
ProxyAgent: proxyAgentCtor,
}));
import { createPinnedDispatcher, type PinnedHostname } from "./ssrf.js";
import type { PinnedHostname } from "./ssrf.js";
let createPinnedDispatcher: typeof import("./ssrf.js").createPinnedDispatcher;
beforeEach(async () => {
vi.resetModules();
({ createPinnedDispatcher } = await import("./ssrf.js"));
});
describe("createPinnedDispatcher", () => {
it("uses pinned lookup without overriding global family policy", () => {

View File

@ -62,15 +62,20 @@ vi.mock("./proxy-env.js", () => ({
}));
import { hasEnvHttpProxyConfigured } from "./proxy-env.js";
import {
DEFAULT_UNDICI_STREAM_TIMEOUT_MS,
ensureGlobalUndiciEnvProxyDispatcher,
ensureGlobalUndiciStreamTimeouts,
resetGlobalUndiciStreamTimeoutsForTests,
} from "./undici-global-dispatcher.js";
let DEFAULT_UNDICI_STREAM_TIMEOUT_MS: typeof import("./undici-global-dispatcher.js").DEFAULT_UNDICI_STREAM_TIMEOUT_MS;
let ensureGlobalUndiciEnvProxyDispatcher: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciEnvProxyDispatcher;
let ensureGlobalUndiciStreamTimeouts: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciStreamTimeouts;
let resetGlobalUndiciStreamTimeoutsForTests: typeof import("./undici-global-dispatcher.js").resetGlobalUndiciStreamTimeoutsForTests;
describe("ensureGlobalUndiciStreamTimeouts", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({
DEFAULT_UNDICI_STREAM_TIMEOUT_MS,
ensureGlobalUndiciEnvProxyDispatcher,
ensureGlobalUndiciStreamTimeouts,
resetGlobalUndiciStreamTimeoutsForTests,
} = await import("./undici-global-dispatcher.js"));
vi.clearAllMocks();
resetGlobalUndiciStreamTimeoutsForTests();
setCurrentDispatcher(new Agent());

View File

@ -1,6 +1,6 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
@ -93,12 +93,10 @@ describe("resolveOpenClawPackageRoot", () => {
let resolveOpenClawPackageRoot: typeof import("./openclaw-root.js").resolveOpenClawPackageRoot;
let resolveOpenClawPackageRootSync: typeof import("./openclaw-root.js").resolveOpenClawPackageRootSync;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } =
await import("./openclaw-root.js"));
});
beforeEach(() => {
state.entries.clear();
state.realpaths.clear();
state.realpathErrors.clear();

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
resolveOutboundTarget: vi.fn(() => ({ ok: true as const, to: "+1999" })),
@ -13,7 +13,15 @@ vi.mock("./targets.js", async () => {
});
import type { OpenClawConfig } from "../../config/config.js";
import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget } from "./agent-delivery.js";
type AgentDeliveryModule = typeof import("./agent-delivery.js");
let resolveAgentDeliveryPlan: AgentDeliveryModule["resolveAgentDeliveryPlan"];
let resolveAgentOutboundTarget: AgentDeliveryModule["resolveAgentOutboundTarget"];
beforeEach(async () => {
vi.resetModules();
({ resolveAgentDeliveryPlan, resolveAgentOutboundTarget } = await import("./agent-delivery.js"));
});
describe("agent delivery helpers", () => {
it("builds a delivery plan from session delivery context", () => {

View File

@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { defaultRuntime } from "../../runtime.js";
const mocks = vi.hoisted(() => ({
listChannelPlugins: vi.fn(),
@ -14,11 +13,20 @@ vi.mock("./channel-resolution.js", () => ({
resolveOutboundChannelPlugin: mocks.resolveOutboundChannelPlugin,
}));
import {
__testing,
listConfiguredMessageChannels,
resolveMessageChannelSelection,
} from "./channel-selection.js";
type ChannelSelectionModule = typeof import("./channel-selection.js");
type RuntimeModule = typeof import("../../runtime.js");
let __testing: ChannelSelectionModule["__testing"];
let listConfiguredMessageChannels: ChannelSelectionModule["listConfiguredMessageChannels"];
let resolveMessageChannelSelection: ChannelSelectionModule["resolveMessageChannelSelection"];
let runtimeModule: RuntimeModule;
beforeEach(async () => {
vi.resetModules();
runtimeModule = await import("../../runtime.js");
({ __testing, listConfiguredMessageChannels, resolveMessageChannelSelection } =
await import("./channel-selection.js"));
});
function makePlugin(params: {
id: string;
@ -40,9 +48,10 @@ function makePlugin(params: {
}
describe("listConfiguredMessageChannels", () => {
const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => undefined);
let errorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
errorSpy = vi.spyOn(runtimeModule.defaultRuntime, "error").mockImplementation(() => undefined);
mocks.listChannelPlugins.mockReset();
mocks.listChannelPlugins.mockReturnValue([]);
mocks.resolveOutboundChannelPlugin.mockReset();

View File

@ -15,10 +15,12 @@ import {
whatsappChunkConfig,
} from "./deliver.test-helpers.js";
const { deliverOutboundPayloads } = await import("./deliver.js");
type DeliverModule = typeof import("./deliver.js");
let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"];
async function runChunkedWhatsAppDelivery(params?: {
mirror?: Parameters<typeof deliverOutboundPayloads>[0]["mirror"];
mirror?: Parameters<DeliverModule["deliverOutboundPayloads"]>[0]["mirror"];
}) {
return await runChunkedWhatsAppDeliveryHelper({
deliverOutboundPayloads,
@ -75,7 +77,9 @@ function expectSuccessfulWhatsAppInternalHookPayload(
}
describe("deliverOutboundPayloads lifecycle", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ deliverOutboundPayloads } = await import("./deliver.js"));
resetDeliverTestState();
resetDeliverTestMocks({ includeSessionMocks: true });
});

View File

@ -80,7 +80,10 @@ vi.mock("../../logging/subsystem.js", () => ({
},
}));
const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js");
type DeliverModule = typeof import("./deliver.js");
let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"];
let normalizeOutboundPayloads: DeliverModule["normalizeOutboundPayloads"];
const telegramChunkConfig: OpenClawConfig = {
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
@ -90,13 +93,13 @@ const whatsappChunkConfig: OpenClawConfig = {
channels: { whatsapp: { textChunkLimit: 4000 } },
};
type DeliverOutboundArgs = Parameters<typeof deliverOutboundPayloads>[0];
type DeliverOutboundArgs = Parameters<DeliverModule["deliverOutboundPayloads"]>[0];
type DeliverOutboundPayload = DeliverOutboundArgs["payloads"][number];
type DeliverSession = DeliverOutboundArgs["session"];
async function deliverWhatsAppPayload(params: {
sendWhatsApp: NonNullable<
NonNullable<Parameters<typeof deliverOutboundPayloads>[0]["deps"]>["sendWhatsApp"]
NonNullable<Parameters<DeliverModule["deliverOutboundPayloads"]>[0]["deps"]>["sendWhatsApp"]
>;
payload: DeliverOutboundPayload;
cfg?: OpenClawConfig;
@ -198,7 +201,9 @@ function expectSuccessfulWhatsAppInternalHookPayload(
}
describe("deliverOutboundPayloads", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js"));
setActivePluginRegistry(defaultRegistry);
mocks.appendAssistantMessageToSessionTranscript.mockClear();
hookMocks.runner.hasHooks.mockClear();

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveAgentIdentityMock = vi.hoisted(() => vi.fn());
const resolveAgentAvatarMock = vi.hoisted(() => vi.fn());
@ -11,7 +11,15 @@ vi.mock("../../agents/identity-avatar.js", () => ({
resolveAgentAvatar: (...args: unknown[]) => resolveAgentAvatarMock(...args),
}));
import { normalizeOutboundIdentity, resolveAgentOutboundIdentity } from "./identity.js";
type IdentityModule = typeof import("./identity.js");
let normalizeOutboundIdentity: IdentityModule["normalizeOutboundIdentity"];
let resolveAgentOutboundIdentity: IdentityModule["resolveAgentOutboundIdentity"];
beforeEach(async () => {
vi.resetModules();
({ normalizeOutboundIdentity, resolveAgentOutboundIdentity } = await import("./identity.js"));
});
describe("normalizeOutboundIdentity", () => {
it("trims fields and drops empty identities", () => {

View File

@ -1,16 +1,13 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
import { loadWebMedia } from "../../../extensions/whatsapp/src/media.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { jsonResult } from "../../agents/tools/common.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js";
import { runMessageAction } from "./message-action-runner.js";
vi.mock("../../../extensions/whatsapp/src/media.js", async () => {
const actual = await vi.importActual<typeof import("../../../extensions/whatsapp/src/media.js")>(
@ -79,8 +76,17 @@ async function expectSandboxMediaRewrite(params: {
);
}
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
type MessageActionRunnerModule = typeof import("./message-action-runner.js");
type WhatsAppMediaModule = typeof import("../../../extensions/whatsapp/src/media.js");
type SlackChannelModule = typeof import("../../../extensions/slack/src/channel.js");
type RuntimeIndexModule = typeof import("../../plugins/runtime/index.js");
type SlackRuntimeModule = typeof import("../../../extensions/slack/src/runtime.js");
let runMessageAction: MessageActionRunnerModule["runMessageAction"];
let loadWebMedia: WhatsAppMediaModule["loadWebMedia"];
let slackPlugin: SlackChannelModule["slackPlugin"];
let createPluginRuntime: RuntimeIndexModule["createPluginRuntime"];
let setSlackRuntime: SlackRuntimeModule["setSlackRuntime"];
function installSlackRuntime() {
const runtime = createPluginRuntime();
@ -88,7 +94,11 @@ function installSlackRuntime() {
}
describe("runMessageAction media behavior", () => {
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ runMessageAction } = await import("./message-action-runner.js"));
({ loadWebMedia } = await import("../../../extensions/whatsapp/src/media.js"));
({ slackPlugin } = await import("../../../extensions/slack/src/channel.js"));
({ createPluginRuntime } = await import("../../plugins/runtime/index.js"));
({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js"));
});

View File

@ -1,11 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
installMessageActionRunnerTestRegistry,
resetMessageActionRunnerTestRegistry,
slackConfig,
telegramConfig,
} from "./message-action-runner.test-helpers.js";
const mocks = vi.hoisted(() => ({
executePollAction: vi.fn(),
}));
@ -20,10 +13,18 @@ vi.mock("./outbound-send-service.js", async () => {
};
});
import { runMessageAction } from "./message-action-runner.js";
type MessageActionRunnerModule = typeof import("./message-action-runner.js");
type MessageActionRunnerTestHelpersModule =
typeof import("./message-action-runner.test-helpers.js");
let runMessageAction: MessageActionRunnerModule["runMessageAction"];
let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"];
let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"];
let slackConfig: MessageActionRunnerTestHelpersModule["slackConfig"];
let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"];
async function runPollAction(params: {
cfg: typeof slackConfig;
cfg: MessageActionRunnerTestHelpersModule["slackConfig"];
actionParams: Record<string, unknown>;
toolContext?: Record<string, unknown>;
}) {
@ -44,7 +45,15 @@ async function runPollAction(params: {
| undefined;
}
describe("runMessageAction poll handling", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ runMessageAction } = await import("./message-action-runner.js"));
({
installMessageActionRunnerTestRegistry,
resetMessageActionRunnerTestRegistry,
slackConfig,
telegramConfig,
} = await import("./message-action-runner.test-helpers.js"));
installMessageActionRunnerTestRegistry();
mocks.executePollAction.mockResolvedValue({
handledBy: "core",
@ -54,14 +63,14 @@ describe("runMessageAction poll handling", () => {
});
afterEach(() => {
resetMessageActionRunnerTestRegistry();
resetMessageActionRunnerTestRegistry?.();
mocks.executePollAction.mockReset();
});
it.each([
{
name: "requires at least two poll options",
cfg: telegramConfig,
getCfg: () => telegramConfig,
actionParams: {
channel: "telegram",
target: "telegram:123",
@ -72,7 +81,7 @@ describe("runMessageAction poll handling", () => {
},
{
name: "rejects durationSeconds outside telegram",
cfg: slackConfig,
getCfg: () => slackConfig,
actionParams: {
channel: "slack",
target: "#C12345678",
@ -84,7 +93,7 @@ describe("runMessageAction poll handling", () => {
},
{
name: "rejects poll visibility outside telegram",
cfg: slackConfig,
getCfg: () => slackConfig,
actionParams: {
channel: "slack",
target: "#C12345678",
@ -94,8 +103,8 @@ describe("runMessageAction poll handling", () => {
},
message: /pollAnonymous\/pollPublic are only supported for Telegram polls/i,
},
])("$name", async ({ cfg, actionParams, message }) => {
await expect(runPollAction({ cfg, actionParams })).rejects.toThrow(message);
])("$name", async ({ getCfg, actionParams, message }) => {
await expect(runPollAction({ cfg: getCfg(), actionParams })).rejects.toThrow(message);
expect(mocks.executePollAction).not.toHaveBeenCalled();
});

View File

@ -1,11 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
installMessageActionRunnerTestRegistry,
resetMessageActionRunnerTestRegistry,
slackConfig,
telegramConfig,
} from "./message-action-runner.test-helpers.js";
const mocks = vi.hoisted(() => ({
executeSendAction: vi.fn(),
recordSessionMetaFromInbound: vi.fn(async () => ({ ok: true })),
@ -31,10 +24,18 @@ vi.mock("../../config/sessions.js", async () => {
};
});
import { runMessageAction } from "./message-action-runner.js";
type MessageActionRunnerModule = typeof import("./message-action-runner.js");
type MessageActionRunnerTestHelpersModule =
typeof import("./message-action-runner.test-helpers.js");
let runMessageAction: MessageActionRunnerModule["runMessageAction"];
let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"];
let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"];
let slackConfig: MessageActionRunnerTestHelpersModule["slackConfig"];
let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"];
async function runThreadingAction(params: {
cfg: typeof slackConfig;
cfg: MessageActionRunnerTestHelpersModule["slackConfig"];
actionParams: Record<string, unknown>;
toolContext?: Record<string, unknown>;
}) {
@ -65,12 +66,20 @@ const defaultTelegramToolContext = {
} as const;
describe("runMessageAction threading auto-injection", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ runMessageAction } = await import("./message-action-runner.js"));
({
installMessageActionRunnerTestRegistry,
resetMessageActionRunnerTestRegistry,
slackConfig,
telegramConfig,
} = await import("./message-action-runner.test-helpers.js"));
installMessageActionRunnerTestRegistry();
});
afterEach(() => {
resetMessageActionRunnerTestRegistry();
resetMessageActionRunnerTestRegistry?.();
mocks.executeSendAction.mockClear();
mocks.recordSessionMetaFromInbound.mockClear();
});

View File

@ -4,7 +4,6 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createMSTeamsTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { sendMessage, sendPoll } from "./message.js";
const setRegistry = (registry: ReturnType<typeof createTestRegistry>) => {
setActivePluginRegistry(registry);
@ -17,7 +16,12 @@ vi.mock("../../gateway/call.js", () => ({
randomIdempotencyKey: () => "idem-1",
}));
beforeEach(() => {
let sendMessage: typeof import("./message.js").sendMessage;
let sendPoll: typeof import("./message.js").sendPoll;
beforeEach(async () => {
vi.resetModules();
({ sendMessage, sendPoll } = await import("./message.js"));
callGatewayMock.mockClear();
setRegistry(emptyRegistry);
});

View File

@ -46,10 +46,13 @@ vi.mock("./deliver.js", () => ({
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { sendMessage } from "./message.js";
let sendMessage: typeof import("./message.js").sendMessage;
describe("sendMessage", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ sendMessage } = await import("./message.js"));
setActivePluginRegistry(createTestRegistry([]));
mocks.getChannelPlugin.mockClear();
mocks.resolveOutboundTarget.mockClear();

View File

@ -32,7 +32,10 @@ vi.mock("../../config/sessions.js", () => ({
appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript,
}));
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
type OutboundSendServiceModule = typeof import("./outbound-send-service.js");
let executePollAction: OutboundSendServiceModule["executePollAction"];
let executeSendAction: OutboundSendServiceModule["executeSendAction"];
describe("executeSendAction", () => {
function pluginActionResult(messageId: string) {
@ -88,7 +91,9 @@ describe("executeSendAction", () => {
});
}
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ executePollAction, executeSendAction } = await import("./outbound-send-service.js"));
mocks.dispatchChannelMessageAction.mockClear();
mocks.sendMessage.mockClear();
mocks.sendPoll.mockClear();

View File

@ -1,12 +1,19 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn());
vi.mock("../../agents/agent-scope.js", () => ({
resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args),
}));
type SessionContextModule = typeof import("./session-context.js");
import { buildOutboundSessionContext } from "./session-context.js";
let buildOutboundSessionContext: SessionContextModule["buildOutboundSessionContext"];
beforeEach(async () => {
vi.resetModules();
resolveSessionAgentIdMock.mockReset();
vi.doMock("../../agents/agent-scope.js", () => ({
resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args),
}));
({ buildOutboundSessionContext } = await import("./session-context.js"));
});
describe("buildOutboundSessionContext", () => {
it("returns undefined when both session key and agent id are blank", () => {

View File

@ -4,33 +4,51 @@ const normalizeChannelIdMock = vi.hoisted(() => vi.fn());
const getChannelPluginMock = vi.hoisted(() => vi.fn());
const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn());
vi.mock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args),
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
type TargetNormalizationModule = typeof import("./target-normalization.js");
vi.mock("../../plugins/runtime.js", () => ({
getActivePluginRegistryVersion: (...args: unknown[]) =>
getActivePluginRegistryVersionMock(...args),
}));
import {
buildTargetResolverSignature,
normalizeChannelTargetInput,
normalizeTargetForProvider,
} from "./target-normalization.js";
let buildTargetResolverSignature: TargetNormalizationModule["buildTargetResolverSignature"];
let normalizeChannelTargetInput: TargetNormalizationModule["normalizeChannelTargetInput"];
let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForProvider"];
describe("normalizeChannelTargetInput", () => {
beforeEach(async () => {
vi.resetModules();
normalizeChannelIdMock.mockReset();
getChannelPluginMock.mockReset();
getActivePluginRegistryVersionMock.mockReset();
vi.doMock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args),
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.doMock("../../plugins/runtime.js", () => ({
getActivePluginRegistryVersion: (...args: unknown[]) =>
getActivePluginRegistryVersionMock(...args),
}));
({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } =
await import("./target-normalization.js"));
});
it("trims raw target input", () => {
expect(normalizeChannelTargetInput(" channel:C1 ")).toBe("channel:C1");
});
});
describe("normalizeTargetForProvider", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
normalizeChannelIdMock.mockReset();
getChannelPluginMock.mockReset();
getActivePluginRegistryVersionMock.mockReset();
vi.doMock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args),
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.doMock("../../plugins/runtime.js", () => ({
getActivePluginRegistryVersion: (...args: unknown[]) =>
getActivePluginRegistryVersionMock(...args),
}));
({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } =
await import("./target-normalization.js"));
});
it("returns undefined for missing or blank raw input", () => {
@ -87,8 +105,21 @@ describe("normalizeTargetForProvider", () => {
});
describe("buildTargetResolverSignature", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
normalizeChannelIdMock.mockReset();
getChannelPluginMock.mockReset();
getActivePluginRegistryVersionMock.mockReset();
vi.doMock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args),
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.doMock("../../plugins/runtime.js", () => ({
getActivePluginRegistryVersion: (...args: unknown[]) =>
getActivePluginRegistryVersionMock(...args),
}));
({ buildTargetResolverSignature, normalizeChannelTargetInput, normalizeTargetForProvider } =
await import("./target-normalization.js"));
});
it("builds stable signatures from resolver hint and looksLikeId source", () => {

View File

@ -1,28 +1,41 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelDirectoryEntry } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resetDirectoryCache, resolveMessagingTarget } from "./target-resolver.js";
type TargetResolverModule = typeof import("./target-resolver.js");
let resetDirectoryCache: TargetResolverModule["resetDirectoryCache"];
let resolveMessagingTarget: TargetResolverModule["resolveMessagingTarget"];
const mocks = vi.hoisted(() => ({
listGroups: vi.fn(),
listGroupsLive: vi.fn(),
resolveTarget: vi.fn(),
getChannelPlugin: vi.fn(),
getActivePluginRegistryVersion: vi.fn(() => 1),
}));
vi.mock("../../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
normalizeChannelId: (value: string) => value,
}));
beforeEach(async () => {
vi.resetModules();
mocks.listGroups.mockReset();
mocks.listGroupsLive.mockReset();
mocks.resolveTarget.mockReset();
mocks.getChannelPlugin.mockReset();
mocks.getActivePluginRegistryVersion.mockReset();
mocks.getActivePluginRegistryVersion.mockReturnValue(1);
vi.doMock("../../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
normalizeChannelId: (value: string) => value,
}));
vi.doMock("../../plugins/runtime.js", () => ({
getActivePluginRegistryVersion: () => mocks.getActivePluginRegistryVersion(),
}));
({ resetDirectoryCache, resolveMessagingTarget } = await import("./target-resolver.js"));
});
describe("resolveMessagingTarget (directory fallback)", () => {
const cfg = {} as OpenClawConfig;
beforeEach(() => {
mocks.listGroups.mockClear();
mocks.listGroupsLive.mockClear();
mocks.resolveTarget.mockClear();
mocks.getChannelPlugin.mockClear();
resetDirectoryCache();
mocks.getChannelPlugin.mockReturnValue({
directory: {

View File

@ -48,7 +48,8 @@ vi.mock("../../config/plugin-auto-enable.js", () => ({
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { resolveOutboundTarget } from "./targets.js";
let resolveOutboundTarget: typeof import("./targets.js").resolveOutboundTarget;
describe("resolveOutboundTarget channel resolution", () => {
let registrySeq = 0;
@ -60,7 +61,9 @@ describe("resolveOutboundTarget channel resolution", () => {
mode: "explicit",
});
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ resolveOutboundTarget } = await import("./targets.js"));
registrySeq += 1;
setActivePluginRegistry(createTestRegistry([]), `targets-test-${registrySeq}`);
mocks.getChannelPlugin.mockReset();

View File

@ -1,5 +1,5 @@
import { Buffer } from "node:buffer";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const randomBytesMock = vi.hoisted(() => vi.fn());
@ -11,7 +11,17 @@ vi.mock("node:crypto", async () => {
};
});
import { generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } from "./pairing-token.js";
type PairingTokenModule = typeof import("./pairing-token.js");
let generatePairingToken: PairingTokenModule["generatePairingToken"];
let PAIRING_TOKEN_BYTES: PairingTokenModule["PAIRING_TOKEN_BYTES"];
let verifyPairingToken: PairingTokenModule["verifyPairingToken"];
beforeEach(async () => {
vi.resetModules();
({ generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } =
await import("./pairing-token.js"));
});
describe("generatePairingToken", () => {
it("uses the configured byte count and returns a base64url token", () => {

View File

@ -7,11 +7,20 @@ const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
import { inspectPortUsage } from "./ports-inspect.js";
import { ensurePortAvailable, handlePortError, PortInUseError } from "./ports.js";
let inspectPortUsage: typeof import("./ports-inspect.js").inspectPortUsage;
let ensurePortAvailable: typeof import("./ports.js").ensurePortAvailable;
let handlePortError: typeof import("./ports.js").handlePortError;
let PortInUseError: typeof import("./ports.js").PortInUseError;
const describeUnix = process.platform === "win32" ? describe.skip : describe;
beforeEach(async () => {
vi.resetModules();
({ inspectPortUsage } = await import("./ports-inspect.js"));
({ ensurePortAvailable, handlePortError, PortInUseError } = await import("./ports.js"));
});
describe("ports helpers", () => {
it("ensurePortAvailable rejects when port busy", async () => {
const server = net.createServer();

View File

@ -7,12 +7,14 @@ vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderUsageAuthWithPluginMock(...args),
}));
import { resolveProviderAuths } from "./provider-usage.auth.js";
let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths;
describe("resolveProviderAuths plugin seam", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
resolveProviderUsageAuthWithPluginMock.mockReset();
resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null);
({ resolveProviderAuths } = await import("./provider-usage.auth.js"));
});
it("prefers plugin-owned usage auth when available", async () => {

View File

@ -12,14 +12,16 @@ vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderUsageSnapshotWithPluginMock(...args),
}));
import { loadProviderUsageSummary } from "./provider-usage.load.js";
let loadProviderUsageSummary: typeof import("./provider-usage.load.js").loadProviderUsageSummary;
const usageNow = Date.UTC(2026, 0, 7, 0, 0, 0);
describe("provider-usage.load plugin seam", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
resolveProviderUsageSnapshotWithPluginMock.mockReset();
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValue(null);
({ loadProviderUsageSummary } = await import("./provider-usage.load.js"));
});
it("prefers plugin-owned usage snapshots", async () => {

View File

@ -32,11 +32,9 @@ vi.mock("../logging/subsystem.js", () => ({
}));
import { resolveLsofCommandSync } from "./ports-lsof.js";
import {
__testing,
cleanStaleGatewayProcessesSync,
findGatewayPidsOnPortSync,
} from "./restart-stale-pids.js";
let __testing: typeof import("./restart-stale-pids.js").__testing;
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync;
function lsofOutput(entries: Array<{ pid: number; cmd: string }>): string {
return entries.map(({ pid, cmd }) => `p${pid}\nc${cmd}`).join("\n") + "\n";
@ -89,6 +87,12 @@ function installInitialBusyPoll(
describe.skipIf(isWindows)("restart-stale-pids", () => {
beforeEach(() => {
vi.resetModules();
});
beforeEach(async () => {
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import("./restart-stale-pids.js"));
mockSpawnSync.mockReset();
mockResolveGatewayPort.mockReset();
mockRestartWarn.mockReset();

View File

@ -16,13 +16,14 @@ vi.mock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
}));
import {
__testing,
cleanStaleGatewayProcessesSync,
findGatewayPidsOnPortSync,
} from "./restart-stale-pids.js";
let __testing: typeof import("./restart-stale-pids.js").__testing;
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import("./restart-stale-pids.js"));
spawnSyncMock.mockReset();
resolveLsofCommandSyncMock.mockReset();
resolveGatewayPortMock.mockReset();

View File

@ -1,5 +1,5 @@
import { Buffer } from "node:buffer";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const cryptoMocks = vi.hoisted(() => ({
randomBytes: vi.fn((bytes: number) => Buffer.alloc(bytes, 0xab)),
@ -11,7 +11,13 @@ vi.mock("node:crypto", () => ({
randomUUID: cryptoMocks.randomUUID,
}));
import { generateSecureToken, generateSecureUuid } from "./secure-random.js";
let generateSecureToken: typeof import("./secure-random.js").generateSecureToken;
let generateSecureUuid: typeof import("./secure-random.js").generateSecureUuid;
beforeEach(async () => {
vi.resetModules();
({ generateSecureToken, generateSecureUuid } = await import("./secure-random.js"));
});
describe("secure-random", () => {
it("delegates UUID generation to crypto.randomUUID", () => {

View File

@ -15,28 +15,9 @@ const mocks = vi.hoisted(() => ({
enqueueSystemEvent: vi.fn(),
}));
vi.mock("../agents/agent-scope.js", () => ({
resolveSessionAgentId: mocks.resolveSessionAgentId,
}));
type SessionMaintenanceWarningModule = typeof import("./session-maintenance-warning.js");
vi.mock("../utils/message-channel.js", () => ({
normalizeMessageChannel: mocks.normalizeMessageChannel,
isDeliverableMessageChannel: mocks.isDeliverableMessageChannel,
}));
vi.mock("./outbound/targets.js", () => ({
resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget,
}));
vi.mock("./outbound/deliver.js", () => ({
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
}));
vi.mock("./system-events.js", () => ({
enqueueSystemEvent: mocks.enqueueSystemEvent,
}));
const { deliverSessionMaintenanceWarning } = await import("./session-maintenance-warning.js");
let deliverSessionMaintenanceWarning: SessionMaintenanceWarningModule["deliverSessionMaintenanceWarning"];
function createParams(
overrides: Partial<Parameters<typeof deliverSessionMaintenanceWarning>[0]> = {},
@ -62,17 +43,35 @@ describe("deliverSessionMaintenanceWarning", () => {
let prevVitest: string | undefined;
let prevNodeEnv: string | undefined;
beforeEach(() => {
beforeEach(async () => {
prevVitest = process.env.VITEST;
prevNodeEnv = process.env.NODE_ENV;
delete process.env.VITEST;
process.env.NODE_ENV = "development";
vi.resetModules();
mocks.resolveSessionAgentId.mockClear();
mocks.resolveSessionDeliveryTarget.mockClear();
mocks.normalizeMessageChannel.mockClear();
mocks.isDeliverableMessageChannel.mockClear();
mocks.deliverOutboundPayloads.mockClear();
mocks.enqueueSystemEvent.mockClear();
vi.doMock("../agents/agent-scope.js", () => ({
resolveSessionAgentId: mocks.resolveSessionAgentId,
}));
vi.doMock("../utils/message-channel.js", () => ({
normalizeMessageChannel: mocks.normalizeMessageChannel,
isDeliverableMessageChannel: mocks.isDeliverableMessageChannel,
}));
vi.doMock("./outbound/targets.js", () => ({
resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget,
}));
vi.doMock("./outbound/deliver.js", () => ({
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
}));
vi.doMock("./system-events.js", () => ({
enqueueSystemEvent: mocks.enqueueSystemEvent,
}));
({ deliverSessionMaintenanceWarning } = await import("./session-maintenance-warning.js"));
});
afterEach(() => {

View File

@ -1,43 +1,45 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { waitForTransportReady } from "./transport-ready.js";
let injectedSleepError: Error | null = null;
// Perf: `sleepWithAbort` uses `node:timers/promises` which isn't controlled by fake timers.
// Route sleeps through global `setTimeout` so tests can advance time deterministically.
vi.mock("./backoff.js", () => ({
sleepWithAbort: async (ms: number, signal?: AbortSignal) => {
if (injectedSleepError) {
throw injectedSleepError;
}
if (signal?.aborted) {
throw new Error("aborted");
}
if (ms <= 0) {
return;
}
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
reject(new Error("aborted"));
};
signal?.addEventListener("abort", onAbort, { once: true });
});
},
}));
type TransportReadyModule = typeof import("./transport-ready.js");
let waitForTransportReady: TransportReadyModule["waitForTransportReady"];
function createRuntime() {
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
}
describe("waitForTransportReady", () => {
beforeEach(() => {
beforeEach(async () => {
vi.useFakeTimers();
vi.resetModules();
// Perf: `sleepWithAbort` uses `node:timers/promises` which isn't controlled by fake timers.
// Route sleeps through global `setTimeout` so tests can advance time deterministically.
vi.doMock("./backoff.js", () => ({
sleepWithAbort: async (ms: number, signal?: AbortSignal) => {
if (injectedSleepError) {
throw injectedSleepError;
}
if (signal?.aborted) {
throw new Error("aborted");
}
if (ms <= 0) {
return;
}
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
reject(new Error("aborted"));
};
signal?.addEventListener("abort", onAbort, { once: true });
});
},
}));
({ waitForTransportReady } = await import("./transport-ready.js"));
});
afterEach(() => {

View File

@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { captureFullEnv } from "../test-utils/env.js";
const spawnMock = vi.hoisted(() => vi.fn());
@ -14,7 +14,9 @@ vi.mock("./tmp-openclaw-dir.js", () => ({
resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(),
}));
import { relaunchGatewayScheduledTask } from "./windows-task-restart.js";
type WindowsTaskRestartModule = typeof import("./windows-task-restart.js");
let relaunchGatewayScheduledTask: WindowsTaskRestartModule["relaunchGatewayScheduledTask"];
const envSnapshot = captureFullEnv();
const createdScriptPaths = new Set<string>();
@ -51,6 +53,11 @@ afterEach(() => {
});
describe("relaunchGatewayScheduledTask", () => {
beforeEach(async () => {
vi.resetModules();
({ relaunchGatewayScheduledTask } = await import("./windows-task-restart.js"));
});
it("writes a detached schtasks relaunch helper", () => {
const unref = vi.fn();
let seenCommandArg = "";

View File

@ -14,7 +14,11 @@ vi.mock("node:fs/promises", () => ({
},
}));
const { isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js");
let isWSLEnv: typeof import("./wsl.js").isWSLEnv;
let isWSLSync: typeof import("./wsl.js").isWSLSync;
let isWSL2Sync: typeof import("./wsl.js").isWSL2Sync;
let isWSL: typeof import("./wsl.js").isWSL;
let resetWSLStateForTests: typeof import("./wsl.js").resetWSLStateForTests;
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
@ -29,13 +33,18 @@ describe("wsl detection", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
beforeEach(() => {
vi.resetModules();
envSnapshot = captureEnv(["WSL_INTEROP", "WSL_DISTRO_NAME", "WSLENV"]);
readFileSyncMock.mockReset();
readFileMock.mockReset();
resetWSLStateForTests();
setPlatform("linux");
});
beforeEach(async () => {
({ isWSLEnv, isWSLSync, isWSL2Sync, isWSL, resetWSLStateForTests } = await import("./wsl.js"));
resetWSLStateForTests();
});
afterEach(() => {
envSnapshot.restore();
resetWSLStateForTests();

View File

@ -2,51 +2,35 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
import type { MsgContext } from "../auto-reply/templating.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { fetchRemoteMedia } from "../media/fetch.js";
import { runExec } from "../process/exec.js";
import { withEnvAsync } from "../test-utils/env.js";
import { clearMediaUnderstandingBinaryCacheForTests } from "./runner.js";
import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js";
type ResolveApiKeyForProvider = typeof import("../agents/model-auth.js").resolveApiKeyForProvider;
const resolveApiKeyForProviderMock = vi.hoisted(() =>
vi.fn<typeof resolveApiKeyForProvider>(async () => ({
vi.fn<ResolveApiKeyForProvider>(async () => ({
apiKey: "test-key", // pragma: allowlist secret
source: "test",
mode: "api-key",
})),
);
const hasAvailableAuthForProviderMock = vi.hoisted(() =>
vi.fn(async (...args: Parameters<typeof resolveApiKeyForProvider>) => {
vi.fn(async (...args: Parameters<ResolveApiKeyForProvider>) => {
const resolved = await resolveApiKeyForProviderMock(...args);
return Boolean(resolved?.apiKey);
}),
);
vi.mock("../agents/model-auth.js", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
hasAvailableAuthForProvider: hasAvailableAuthForProviderMock,
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
if (auth?.apiKey) {
return auth.apiKey;
}
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`);
},
}));
vi.mock("../media/fetch.js", () => ({
fetchRemoteMedia: vi.fn(),
}));
vi.mock("../process/exec.js", () => ({
runExec: vi.fn(),
}));
const fetchRemoteMediaMock = vi.hoisted(() => vi.fn());
const runExecMock = vi.hoisted(() => vi.fn());
let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding;
const mockedRunExec = vi.mocked(runExec);
let clearMediaUnderstandingBinaryCacheForTests: typeof import("./runner.js").clearMediaUnderstandingBinaryCacheForTests;
const mockedResolveApiKey = resolveApiKeyForProviderMock;
const mockedFetchRemoteMedia = fetchRemoteMediaMock;
const mockedRunExec = runExecMock;
const TEMP_MEDIA_PREFIX = "openclaw-media-";
let suiteTempMediaRootDir = "";
@ -241,14 +225,32 @@ function expectFileNotApplied(params: {
}
describe("applyMediaUnderstanding", () => {
const mockedResolveApiKey = vi.mocked(resolveApiKeyForProvider);
const mockedFetchRemoteMedia = vi.mocked(fetchRemoteMedia);
beforeAll(async () => {
vi.resetModules();
vi.doMock("../agents/model-auth.js", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
hasAvailableAuthForProvider: hasAvailableAuthForProviderMock,
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
if (auth?.apiKey) {
return auth.apiKey;
}
throw new Error(
`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`,
);
},
}));
vi.doMock("../media/fetch.js", () => ({
fetchRemoteMedia: fetchRemoteMediaMock,
}));
vi.doMock("../process/exec.js", () => ({
runExec: runExecMock,
}));
({ applyMediaUnderstanding } = await import("./apply.js"));
({ clearMediaUnderstandingBinaryCacheForTests } = await import("./runner.js"));
const baseDir = resolvePreferredOpenClawTmpDir();
await fs.mkdir(baseDir, { recursive: true });
suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX));
({ applyMediaUnderstanding } = await import("./apply.js"));
});
beforeEach(() => {

View File

@ -16,49 +16,43 @@ const resolveApiKeyForProviderMock = vi.fn(async () => ({
const requireApiKeyMock = vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? "");
const setRuntimeApiKeyMock = vi.fn();
const discoverModelsMock = vi.fn();
let imageImportSeq = 0;
type ImageModule = typeof import("./image.js");
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
vi.mock("../../agents/minimax-vlm.js", () => ({
isMinimaxVlmProvider: (provider: string) =>
provider === "minimax" || provider === "minimax-portal",
isMinimaxVlmModel: (provider: string, modelId: string) =>
(provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01",
minimaxUnderstandImage: minimaxUnderstandImageMock,
}));
vi.mock("../../agents/models-config.js", () => ({
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
}));
vi.mock("../../agents/model-auth.js", () => ({
getApiKeyForModel: getApiKeyForModelMock,
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
requireApiKey: requireApiKeyMock,
}));
vi.mock("../../agents/pi-model-discovery-runtime.js", () => ({
discoverAuthStorage: () => ({
setRuntimeApiKey: setRuntimeApiKeyMock,
}),
discoverModels: discoverModelsMock,
}));
async function importImageModule() {
imageImportSeq += 1;
return await import(/* @vite-ignore */ `./image.js?case=${imageImportSeq}`);
}
let describeImageWithModel: ImageModule["describeImageWithModel"];
describe("describeImageWithModel", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
vi.doMock("@mariozechner/pi-ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
vi.doMock("../../agents/minimax-vlm.js", () => ({
isMinimaxVlmProvider: (provider: string) =>
provider === "minimax" || provider === "minimax-portal",
isMinimaxVlmModel: (provider: string, modelId: string) =>
(provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01",
minimaxUnderstandImage: minimaxUnderstandImageMock,
}));
vi.doMock("../../agents/models-config.js", () => ({
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
}));
vi.doMock("../../agents/model-auth.js", () => ({
getApiKeyForModel: getApiKeyForModelMock,
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
requireApiKey: requireApiKeyMock,
}));
vi.doMock("../../agents/pi-model-discovery-runtime.js", () => ({
discoverAuthStorage: () => ({
setRuntimeApiKey: setRuntimeApiKeyMock,
}),
discoverModels: discoverModelsMock,
}));
({ describeImageWithModel } = await import("./image.js"));
minimaxUnderstandImageMock.mockResolvedValue("portal ok");
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
@ -71,8 +65,6 @@ describe("describeImageWithModel", () => {
});
it("routes minimax-portal image models through the MiniMax VLM endpoint", async () => {
const { describeImageWithModel } = await importImageModule();
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
@ -121,8 +113,6 @@ describe("describeImageWithModel", () => {
content: [{ type: "text", text: "generic ok" }],
});
const { describeImageWithModel } = await importImageModule();
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
@ -165,8 +155,6 @@ describe("describeImageWithModel", () => {
content: [{ type: "text", text: "flash ok" }],
});
const { describeImageWithModel } = await importImageModule();
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
@ -215,8 +203,6 @@ describe("describeImageWithModel", () => {
content: [{ type: "text", text: "flash lite ok" }],
});
const { describeImageWithModel } = await importImageModule();
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",

View File

@ -1,9 +1,4 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
resolveTelegramTransport,
shouldRetryTelegramIpv4Fallback,
} from "../../extensions/telegram/src/fetch.js";
import { fetchRemoteMedia } from "./fetch.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const undiciMocks = vi.hoisted(() => {
const createDispatcherCtor = <T extends Record<string, unknown> | string>() =>
@ -26,9 +21,20 @@ vi.mock("undici", () => ({
fetch: undiciMocks.fetch,
}));
let resolveTelegramTransport: typeof import("../../extensions/telegram/src/fetch.js").resolveTelegramTransport;
let shouldRetryTelegramIpv4Fallback: typeof import("../../extensions/telegram/src/fetch.js").shouldRetryTelegramIpv4Fallback;
let fetchRemoteMedia: typeof import("./fetch.js").fetchRemoteMedia;
describe("fetchRemoteMedia telegram network policy", () => {
type LookupFn = NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
beforeEach(async () => {
vi.resetModules();
({ resolveTelegramTransport, shouldRetryTelegramIpv4Fallback } =
await import("../../extensions/telegram/src/fetch.js"));
({ fetchRemoteMedia } = await import("./fetch.js"));
});
function createTelegramFetchFailedError(code: string): Error {
return Object.assign(new TypeError("fetch failed"), {
cause: { code },

View File

@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const fetchWithSsrFGuardMock = vi.fn();
const convertHeicToJpegMock = vi.fn();
@ -24,15 +24,13 @@ let fetchWithGuard: typeof import("./input-files.js").fetchWithGuard;
let extractImageContentFromSource: typeof import("./input-files.js").extractImageContentFromSource;
let extractFileContentFromSource: typeof import("./input-files.js").extractFileContentFromSource;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ fetchWithGuard, extractImageContentFromSource, extractFileContentFromSource } =
await import("./input-files.js"));
});
beforeEach(() => {
vi.clearAllMocks();
});
describe("HEIC input image normalization", () => {
it("converts base64 HEIC images to JPEG before returning them", async () => {
const normalized = Buffer.from("jpeg-normalized");

View File

@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
const mocks = vi.hoisted(() => ({
@ -15,13 +15,22 @@ vi.mock("../infra/fs-safe.js", async (importOriginal) => {
};
});
const { saveMediaSource } = await import("./store.js");
const { SafeOpenError } = await import("../infra/fs-safe.js");
type StoreModule = typeof import("./store.js");
type FsSafeModule = typeof import("../infra/fs-safe.js");
let saveMediaSource: StoreModule["saveMediaSource"];
let SafeOpenError: FsSafeModule["SafeOpenError"];
describe("media store outside-workspace mapping", () => {
let tempHome: TempHomeEnv;
let home = "";
beforeEach(async () => {
vi.resetModules();
({ saveMediaSource } = await import("./store.js"));
({ SafeOpenError } = await import("../infra/fs-safe.js"));
});
beforeAll(async () => {
tempHome = await createTempHomeEnv("openclaw-media-store-test-home-");
home = tempHome.home;

View File

@ -1,7 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { retryAsync } from "../infra/retry.js";
import { postJsonWithRetry } from "./batch-http.js";
import { postJson } from "./post-json.js";
vi.mock("../infra/retry.js", () => ({
retryAsync: vi.fn(async (run: () => Promise<unknown>) => await run()),
@ -12,11 +9,18 @@ vi.mock("./post-json.js", () => ({
}));
describe("postJsonWithRetry", () => {
const retryAsyncMock = vi.mocked(retryAsync);
const postJsonMock = vi.mocked(postJson);
let retryAsyncMock: ReturnType<typeof vi.mocked<typeof import("../infra/retry.js").retryAsync>>;
let postJsonMock: ReturnType<typeof vi.mocked<typeof import("./post-json.js").postJson>>;
let postJsonWithRetry: typeof import("./batch-http.js").postJsonWithRetry;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ postJsonWithRetry } = await import("./batch-http.js"));
const retryModule = await import("../infra/retry.js");
const postJsonModule = await import("./post-json.js");
retryAsyncMock = vi.mocked(retryModule.retryAsync);
postJsonMock = vi.mocked(postJsonModule.postJson);
});
it("posts JSON and returns parsed response payload", async () => {

View File

@ -1,14 +1,12 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, expect } from "vitest";
import { afterAll, beforeAll, beforeEach, expect, vi, type Mock } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { getEmbedBatchMock, resetEmbeddingMocks } from "./embedding.test-mocks.js";
import {
getMemorySearchManager,
type MemoryIndexManager,
type MemorySearchManager,
} from "./index.js";
import type { MemoryIndexManager, MemorySearchManager } from "./index.js";
type EmbeddingTestMocksModule = typeof import("./embedding.test-mocks.js");
type MemoryIndexModule = typeof import("./index.js");
export function installEmbeddingManagerFixture(opts: {
fixturePrefix: string;
@ -21,7 +19,6 @@ export function installEmbeddingManagerFixture(opts: {
}) => OpenClawConfig;
resetIndexEachTest?: boolean;
}) {
const embedBatch = getEmbedBatchMock();
const resetIndexEachTest = opts.resetIndexEachTest ?? true;
let fixtureRoot: string | undefined;
@ -29,6 +26,9 @@ export function installEmbeddingManagerFixture(opts: {
let memoryDir: string | undefined;
let managerLarge: MemoryIndexManager | undefined;
let managerSmall: MemoryIndexManager | undefined;
let embedBatch: Mock<(texts: string[]) => Promise<number[][]>> | undefined;
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
let resetEmbeddingMocks: EmbeddingTestMocksModule["resetEmbeddingMocks"];
const resetManager = (manager: MemoryIndexManager) => {
(manager as unknown as { resetIndex: () => void }).resetIndex();
@ -56,6 +56,12 @@ export function installEmbeddingManagerFixture(opts: {
};
beforeAll(async () => {
vi.resetModules();
await import("./embedding.test-mocks.js");
const embeddingMocks = await import("./embedding.test-mocks.js");
embedBatch = embeddingMocks.getEmbedBatchMock();
resetEmbeddingMocks = embeddingMocks.resetEmbeddingMocks;
({ getMemorySearchManager } = await import("./index.js"));
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), opts.fixturePrefix));
workspaceDir = path.join(fixtureRoot, "workspace");
memoryDir = path.join(workspaceDir, "memory");
@ -116,7 +122,9 @@ export function installEmbeddingManagerFixture(opts: {
});
return {
embedBatch,
get embedBatch() {
return requireValue(embedBatch, "embedBatch");
},
getFixtureRoot: () => requireValue(fixtureRoot, "fixtureRoot"),
getWorkspaceDir: () => requireValue(workspaceDir, "workspaceDir"),
getMemoryDir: () => requireValue(memoryDir, "memoryDir"),

View File

@ -1,15 +1,20 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js";
import { postJson } from "./post-json.js";
vi.mock("./post-json.js", () => ({
postJson: vi.fn(),
}));
type EmbeddingsRemoteFetchModule = typeof import("./embeddings-remote-fetch.js");
let fetchRemoteEmbeddingVectors: EmbeddingsRemoteFetchModule["fetchRemoteEmbeddingVectors"];
describe("fetchRemoteEmbeddingVectors", () => {
const postJsonMock = vi.mocked(postJson);
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ fetchRemoteEmbeddingVectors } = await import("./embeddings-remote-fetch.js"));
vi.clearAllMocks();
});

View File

@ -1,7 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import * as authModule from "../agents/model-auth.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type FetchMock, withFetchPreconnect } from "../test-utils/fetch-mock.js";
import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../agents/model-auth.js", async () => {
@ -20,6 +18,17 @@ const createFetchMock = () => {
return withFetchPreconnect(fetchMock);
};
let authModule: typeof import("../agents/model-auth.js");
let createVoyageEmbeddingProvider: typeof import("./embeddings-voyage.js").createVoyageEmbeddingProvider;
let normalizeVoyageModel: typeof import("./embeddings-voyage.js").normalizeVoyageModel;
beforeEach(async () => {
vi.resetModules();
authModule = await import("../agents/model-auth.js");
({ createVoyageEmbeddingProvider, normalizeVoyageModel } =
await import("./embeddings-voyage.js"));
});
function mockVoyageApiKey() {
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
apiKey: "voyage-key-123",

View File

@ -1,7 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import * as authModule from "../agents/model-auth.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
import { createEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../agents/model-auth.js", async () => {
@ -33,12 +31,25 @@ function readFirstFetchRequest(fetchMock: { mock: { calls: unknown[][] } }) {
return { url, init: init as RequestInit | undefined };
}
type EmbeddingsModule = typeof import("./embeddings.js");
type AuthModule = typeof import("../agents/model-auth.js");
let authModule: AuthModule;
let createEmbeddingProvider: EmbeddingsModule["createEmbeddingProvider"];
let DEFAULT_LOCAL_MODEL: EmbeddingsModule["DEFAULT_LOCAL_MODEL"];
beforeEach(async () => {
vi.resetModules();
authModule = await import("../agents/model-auth.js");
({ createEmbeddingProvider, DEFAULT_LOCAL_MODEL } = await import("./embeddings.js"));
});
afterEach(() => {
vi.resetAllMocks();
vi.unstubAllGlobals();
});
function requireProvider(result: Awaited<ReturnType<typeof createEmbeddingProvider>>) {
function requireProvider(result: Awaited<ReturnType<EmbeddingsModule["createEmbeddingProvider"]>>) {
if (!result.provider) {
throw new Error("Expected embedding provider");
}
@ -71,7 +82,7 @@ function createLocalProvider(options?: { fallback?: "none" | "openai" }) {
}
function expectAutoSelectedProvider(
result: Awaited<ReturnType<typeof createEmbeddingProvider>>,
result: Awaited<ReturnType<EmbeddingsModule["createEmbeddingProvider"]>>,
expectedId: "openai" | "gemini" | "mistral",
) {
expect(result.requestedProvider).toBe("auto");

View File

@ -3,8 +3,12 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import "./test-runtime-mocks.js";
import type { MemoryIndexManager } from "./index.js";
type MemoryIndexModule = typeof import("./index.js");
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
let embedBatchCalls = 0;
let embedBatchInputCalls = 0;
@ -151,6 +155,9 @@ describe("memory index", () => {
});
beforeEach(async () => {
vi.resetModules();
await import("./test-runtime-mocks.js");
({ getMemorySearchManager } = await import("./index.js"));
// Perf: most suites don't need atomic swap behavior for full reindexes.
// Keep atomic reindex tests on the safe path.
vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "1");

View File

@ -3,25 +3,33 @@ import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { getEmbedBatchMock, resetEmbeddingMocks } from "./embedding.test-mocks.js";
import type { MemoryIndexManager } from "./index.js";
import { getRequiredMemoryIndexManager } from "./test-manager-helpers.js";
let shouldFail = false;
type EmbeddingTestMocksModule = typeof import("./embedding.test-mocks.js");
type TestManagerHelpersModule = typeof import("./test-manager-helpers.js");
describe("memory manager atomic reindex", () => {
let fixtureRoot = "";
let caseId = 0;
let workspaceDir: string;
let indexPath: string;
let manager: MemoryIndexManager | null = null;
const embedBatch = getEmbedBatchMock();
let embedBatch: ReturnType<EmbeddingTestMocksModule["getEmbedBatchMock"]>;
let resetEmbeddingMocks: EmbeddingTestMocksModule["resetEmbeddingMocks"];
let getRequiredMemoryIndexManager: TestManagerHelpersModule["getRequiredMemoryIndexManager"];
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-atomic-"));
});
beforeEach(async () => {
vi.resetModules();
const embeddingMocks = await import("./embedding.test-mocks.js");
embedBatch = embeddingMocks.getEmbedBatchMock();
resetEmbeddingMocks = embeddingMocks.resetEmbeddingMocks;
({ getRequiredMemoryIndexManager } = await import("./test-manager-helpers.js"));
vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "0");
resetEmbeddingMocks();
shouldFail = false;

View File

@ -4,21 +4,15 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { useFastShortTimeouts } from "../../test/helpers/fast-short-timeouts.js";
import type { OpenClawConfig } from "../config/config.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import { createOpenAIEmbeddingProviderMock } from "./test-embeddings-mock.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
import "./test-runtime-mocks.js";
type MemoryIndexManager = import("./index.js").MemoryIndexManager;
type MemoryIndexModule = typeof import("./index.js");
const embedBatch = vi.fn(async (_texts: string[]) => [] as number[][]);
const embedQuery = vi.fn(async () => [0.5, 0.5, 0.5]);
vi.mock("./embeddings.js", () => ({
createEmbeddingProvider: async () =>
createOpenAIEmbeddingProviderMock({
embedQuery,
embedBatch,
}),
}));
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
describe("memory indexing with OpenAI batches", () => {
let fixtureRoot: string;
@ -118,6 +112,17 @@ describe("memory indexing with OpenAI batches", () => {
}
beforeAll(async () => {
vi.resetModules();
vi.doMock("./embeddings.js", () => ({
createEmbeddingProvider: async () =>
createOpenAIEmbeddingProviderMock({
embedQuery,
embedBatch,
}),
}));
await import("./test-runtime-mocks.js");
({ getMemorySearchManager } = await import("./index.js"));
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-batch-"));
workspaceDir = path.join(fixtureRoot, "workspace");
memoryDir = path.join(workspaceDir, "memory");

View File

@ -25,7 +25,6 @@ const fx = installEmbeddingManagerFixture({
},
}),
});
const { embedBatch } = fx;
describe("memory embedding batches", () => {
async function expectSyncWithFastTimeouts(manager: {
@ -55,13 +54,13 @@ describe("memory embedding batches", () => {
});
const status = managerLarge.status();
const totalTexts = embedBatch.mock.calls.reduce(
const totalTexts = fx.embedBatch.mock.calls.reduce(
(sum: number, call: unknown[]) => sum + ((call[0] as string[] | undefined)?.length ?? 0),
0,
);
expect(totalTexts).toBe(status.chunks);
expect(embedBatch.mock.calls.length).toBeGreaterThan(1);
const inputs: string[] = embedBatch.mock.calls.flatMap(
expect(fx.embedBatch.mock.calls.length).toBeGreaterThan(1);
const inputs: string[] = fx.embedBatch.mock.calls.flatMap(
(call: unknown[]) => (call[0] as string[] | undefined) ?? [],
);
expect(inputs.every((text) => Buffer.byteLength(text, "utf8") <= 8000)).toBe(true);
@ -80,7 +79,7 @@ describe("memory embedding batches", () => {
await fs.writeFile(path.join(memoryDir, "2026-01-04.md"), content);
await managerSmall.sync({ reason: "test" });
expect(embedBatch.mock.calls.length).toBe(1);
expect(fx.embedBatch.mock.calls.length).toBe(1);
});
it("retries embeddings on transient rate limit and 5xx errors", async () => {
@ -95,7 +94,7 @@ describe("memory embedding batches", () => {
"openai embeddings failed: 502 Bad Gateway (cloudflare)",
];
let calls = 0;
embedBatch.mockImplementation(async (texts: string[]) => {
fx.embedBatch.mockImplementation(async (texts: string[]) => {
calls += 1;
const transient = transientErrors[calls - 1];
if (transient) {
@ -117,7 +116,7 @@ describe("memory embedding batches", () => {
await fs.writeFile(path.join(memoryDir, "2026-01-08.md"), content);
let calls = 0;
embedBatch.mockImplementation(async (texts: string[]) => {
fx.embedBatch.mockImplementation(async (texts: string[]) => {
calls += 1;
if (calls === 1) {
throw new Error("AWS Bedrock embeddings failed: Too many tokens per day");
@ -136,7 +135,9 @@ describe("memory embedding batches", () => {
await fs.writeFile(path.join(memoryDir, "2026-01-07.md"), "\n\n\n");
await managerSmall.sync({ reason: "test" });
const inputs = embedBatch.mock.calls.flatMap((call: unknown[]) => (call[0] as string[]) ?? []);
const inputs = fx.embedBatch.mock.calls.flatMap(
(call: unknown[]) => (call[0] as string[]) ?? [],
);
expect(inputs).not.toContain("");
});
});

View File

@ -3,12 +3,11 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import {
closeAllMemoryIndexManagers,
MemoryIndexManager as RawMemoryIndexManager,
} from "./manager.js";
import "./test-runtime-mocks.js";
import type { MemoryIndexManager } from "./index.js";
type MemoryIndexModule = typeof import("./index.js");
type ManagerModule = typeof import("./manager.js");
const hoisted = vi.hoisted(() => ({
providerCreateCalls: 0,
@ -34,10 +33,19 @@ vi.mock("./embeddings.js", () => ({
},
}));
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
let closeAllMemoryIndexManagers: ManagerModule["closeAllMemoryIndexManagers"];
let RawMemoryIndexManager: ManagerModule["MemoryIndexManager"];
describe("memory manager cache hydration", () => {
let workspaceDir = "";
beforeEach(async () => {
vi.resetModules();
await import("./test-runtime-mocks.js");
({ getMemorySearchManager } = await import("./index.js"));
({ closeAllMemoryIndexManagers, MemoryIndexManager: RawMemoryIndexManager } =
await import("./manager.js"));
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-concurrent-"));
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello memory.");

View File

@ -11,7 +11,7 @@ import type {
OllamaEmbeddingClient,
OpenAiEmbeddingClient,
} from "./embeddings.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import type { MemoryIndexManager } from "./index.js";
const { createEmbeddingProviderMock } = vi.hoisted(() => ({
createEmbeddingProviderMock: vi.fn(),
@ -25,6 +25,10 @@ vi.mock("./sqlite-vec.js", () => ({
loadSqliteVecExtension: async () => ({ ok: false, error: "sqlite-vec disabled in tests" }),
}));
type MemoryIndexModule = typeof import("./index.js");
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
function createProvider(id: string): EmbeddingProvider {
return {
id,
@ -64,6 +68,8 @@ describe("memory manager mistral provider wiring", () => {
let manager: MemoryIndexManager | null = null;
beforeEach(async () => {
vi.resetModules();
({ getMemorySearchManager } = await import("./index.js"));
createEmbeddingProviderMock.mockReset();
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-mistral-"));
indexPath = path.join(workspaceDir, "index.sqlite");

View File

@ -4,8 +4,6 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { MemoryIndexManager } from "./index.js";
import { buildFileEntry } from "./internal.js";
import { createMemoryManagerOrThrow } from "./test-manager.js";
vi.mock("./embeddings.js", () => {
return {
@ -21,6 +19,12 @@ vi.mock("./embeddings.js", () => {
};
});
type MemoryInternalModule = typeof import("./internal.js");
type TestManagerModule = typeof import("./test-manager.js");
let buildFileEntry: MemoryInternalModule["buildFileEntry"];
let createMemoryManagerOrThrow: TestManagerModule["createMemoryManagerOrThrow"];
describe("memory vector dedupe", () => {
let workspaceDir: string;
let indexPath: string;
@ -40,6 +44,9 @@ describe("memory vector dedupe", () => {
}
beforeEach(async () => {
vi.resetModules();
({ buildFileEntry } = await import("./internal.js"));
({ createMemoryManagerOrThrow } = await import("./test-manager.js"));
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-"));
indexPath = path.join(workspaceDir, "index.sqlite");
await seedMemoryWorkspace(workspaceDir);

View File

@ -1,10 +1,10 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { MemorySearchConfig } from "../config/types.tools.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import type { MemoryIndexManager } from "./index.js";
const { watchMock } = vi.hoisted(() => ({
watchMock: vi.fn(() => ({
@ -34,11 +34,20 @@ vi.mock("./embeddings.js", () => ({
}),
}));
type MemoryIndexModule = typeof import("./index.js");
let getMemorySearchManager: MemoryIndexModule["getMemorySearchManager"];
describe("memory watcher config", () => {
let manager: MemoryIndexManager | null = null;
let workspaceDir = "";
let extraDir = "";
beforeEach(async () => {
vi.resetModules();
({ getMemorySearchManager } = await import("./index.js"));
});
afterEach(async () => {
watchMock.mockClear();
if (manager) {

View File

@ -1,16 +1,21 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { postJson } from "./post-json.js";
import { withRemoteHttpResponse } from "./remote-http.js";
vi.mock("./remote-http.js", () => ({
withRemoteHttpResponse: vi.fn(),
}));
describe("postJson", () => {
const remoteHttpMock = vi.mocked(withRemoteHttpResponse);
let postJson: typeof import("./post-json.js").postJson;
let withRemoteHttpResponse: typeof import("./remote-http.js").withRemoteHttpResponse;
beforeEach(() => {
describe("postJson", () => {
let remoteHttpMock: ReturnType<typeof vi.mocked<typeof withRemoteHttpResponse>>;
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
({ postJson } = await import("./post-json.js"));
({ withRemoteHttpResponse } = await import("./remote-http.js"));
remoteHttpMock = vi.mocked(withRemoteHttpResponse);
});
it("parses JSON payload on successful response", async () => {

View File

@ -1,10 +1,12 @@
import type { OpenClawConfig } from "../config/config.js";
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
import type { MemoryIndexManager } from "./index.js";
export async function getRequiredMemoryIndexManager(params: {
cfg: OpenClawConfig;
agentId?: string;
}): Promise<MemoryIndexManager> {
await import("./embedding.test-mocks.js");
const { getMemorySearchManager } = await import("./index.js");
const result = await getMemorySearchManager({
cfg: params.cfg,
agentId: params.agentId ?? "main",

View File

@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SecretInput } from "../config/types.secrets.js";
import { encodePairingSetupCode, resolvePairingSetupFromConfig } from "./setup-code.js";
vi.mock("../infra/device-bootstrap.js", () => ({
issueDeviceBootstrapToken: vi.fn(async () => ({
@ -9,6 +8,9 @@ vi.mock("../infra/device-bootstrap.js", () => ({
})),
}));
let encodePairingSetupCode: typeof import("./setup-code.js").encodePairingSetupCode;
let resolvePairingSetupFromConfig: typeof import("./setup-code.js").resolvePairingSetupFromConfig;
describe("pairing setup code", () => {
type ResolvedSetup = Awaited<ReturnType<typeof resolvePairingSetupFromConfig>>;
const defaultEnvSecretProviderConfig = {
@ -68,10 +70,17 @@ describe("pairing setup code", () => {
}
beforeEach(() => {
vi.resetModules();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "");
vi.stubEnv("CLAWDBOT_GATEWAY_PASSWORD", "");
vi.stubEnv("OPENCLAW_GATEWAY_PORT", "");
vi.stubEnv("CLAWDBOT_GATEWAY_PORT", "");
});
beforeEach(async () => {
({ encodePairingSetupCode, resolvePairingSetupFromConfig } = await import("./setup-code.js"));
});
afterEach(() => {

View File

@ -1,5 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { loadOutboundMediaFromUrl } from "./outbound-media.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadWebMediaMock = vi.hoisted(() => vi.fn());
@ -7,7 +6,17 @@ vi.mock("../../extensions/whatsapp/src/media.js", () => ({
loadWebMedia: loadWebMediaMock,
}));
type OutboundMediaModule = typeof import("./outbound-media.js");
let loadOutboundMediaFromUrl: OutboundMediaModule["loadOutboundMediaFromUrl"];
describe("loadOutboundMediaFromUrl", () => {
beforeEach(async () => {
vi.resetModules();
({ loadOutboundMediaFromUrl } = await import("./outbound-media.js"));
loadWebMediaMock.mockReset();
});
it("forwards maxBytes and mediaLocalRoots to loadWebMedia", async () => {
loadWebMediaMock.mockResolvedValueOnce({
buffer: Buffer.from("x"),

View File

@ -4,43 +4,61 @@ import {
replaceRuntimeAuthProfileStoreSnapshots,
} from "../../agents/auth-profiles/store.js";
import { createNonExitingRuntime } from "../../runtime.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import type {
WizardMultiSelectParams,
WizardPrompter,
WizardProgress,
WizardSelectParams,
} from "../../wizard/prompts.js";
import { registerProviders, requireProvider } from "./testkit.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
type LoginOpenAICodexOAuth =
(typeof import("../../plugins/provider-openai-codex-oauth.js"))["loginOpenAICodexOAuth"];
(typeof import("openclaw/plugin-sdk/provider-auth"))["loginOpenAICodexOAuth"];
type LoginQwenPortalOAuth =
(typeof import("../../../extensions/qwen-portal-auth/oauth.js"))["loginQwenPortalOAuth"];
type GithubCopilotLoginCommand =
(typeof import("../../providers/github-copilot-auth.js"))["githubCopilotLoginCommand"];
(typeof import("openclaw/plugin-sdk/provider-auth"))["githubCopilotLoginCommand"];
type CreateVpsAwareHandlers =
(typeof import("../../plugins/provider-oauth-flow.js"))["createVpsAwareOAuthHandlers"];
(typeof import("../../commands/oauth-flow.js"))["createVpsAwareOAuthHandlers"];
const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn<LoginOpenAICodexOAuth>());
const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn<LoginQwenPortalOAuth>());
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn<GithubCopilotLoginCommand>());
vi.mock("../../plugins/provider-openai-codex-oauth.js", () => ({
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
}));
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
return {
...actual,
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
};
});
vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({
loginQwenPortalOAuth: loginQwenPortalOAuthMock,
}));
vi.mock("../../providers/github-copilot-auth.js", () => ({
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
}));
const openAIPlugin = (await import("../../../extensions/openai/index.js")).default;
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default;
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
function buildPrompter(): WizardPrompter {
const progress: WizardProgress = {
update() {},

View File

@ -23,30 +23,28 @@ vi.mock("./providers.js", () => ({
resolveOwningPluginIdsForProviderMock(params as never),
}));
import {
augmentModelCatalogWithProviderPlugins,
buildProviderAuthDoctorHintWithPlugin,
buildProviderMissingAuthMessageWithPlugin,
formatProviderAuthProfileApiKeyWithPlugin,
prepareProviderExtraParams,
resolveProviderCacheTtlEligibility,
resolveProviderBinaryThinking,
resolveProviderBuiltInModelSuppression,
resolveProviderDefaultThinkingLevel,
resolveProviderModernModelRef,
resolveProviderUsageSnapshotWithPlugin,
resolveProviderCapabilitiesWithPlugin,
resolveProviderUsageAuthWithPlugin,
resolveProviderXHighThinking,
normalizeProviderResolvedModelWithPlugin,
prepareProviderDynamicModel,
prepareProviderRuntimeAuth,
resetProviderRuntimeHookCacheForTest,
refreshProviderOAuthCredentialWithPlugin,
resolveProviderRuntimePlugin,
runProviderDynamicModel,
wrapProviderStreamFn,
} from "./provider-runtime.js";
let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins;
let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js").buildProviderAuthDoctorHintWithPlugin;
let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin;
let formatProviderAuthProfileApiKeyWithPlugin: typeof import("./provider-runtime.js").formatProviderAuthProfileApiKeyWithPlugin;
let prepareProviderExtraParams: typeof import("./provider-runtime.js").prepareProviderExtraParams;
let resolveProviderCacheTtlEligibility: typeof import("./provider-runtime.js").resolveProviderCacheTtlEligibility;
let resolveProviderBinaryThinking: typeof import("./provider-runtime.js").resolveProviderBinaryThinking;
let resolveProviderBuiltInModelSuppression: typeof import("./provider-runtime.js").resolveProviderBuiltInModelSuppression;
let resolveProviderDefaultThinkingLevel: typeof import("./provider-runtime.js").resolveProviderDefaultThinkingLevel;
let resolveProviderModernModelRef: typeof import("./provider-runtime.js").resolveProviderModernModelRef;
let resolveProviderUsageSnapshotWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageSnapshotWithPlugin;
let resolveProviderCapabilitiesWithPlugin: typeof import("./provider-runtime.js").resolveProviderCapabilitiesWithPlugin;
let resolveProviderUsageAuthWithPlugin: typeof import("./provider-runtime.js").resolveProviderUsageAuthWithPlugin;
let resolveProviderXHighThinking: typeof import("./provider-runtime.js").resolveProviderXHighThinking;
let normalizeProviderResolvedModelWithPlugin: typeof import("./provider-runtime.js").normalizeProviderResolvedModelWithPlugin;
let prepareProviderDynamicModel: typeof import("./provider-runtime.js").prepareProviderDynamicModel;
let prepareProviderRuntimeAuth: typeof import("./provider-runtime.js").prepareProviderRuntimeAuth;
let resetProviderRuntimeHookCacheForTest: typeof import("./provider-runtime.js").resetProviderRuntimeHookCacheForTest;
let refreshProviderOAuthCredentialWithPlugin: typeof import("./provider-runtime.js").refreshProviderOAuthCredentialWithPlugin;
let resolveProviderRuntimePlugin: typeof import("./provider-runtime.js").resolveProviderRuntimePlugin;
let runProviderDynamicModel: typeof import("./provider-runtime.js").runProviderDynamicModel;
let wrapProviderStreamFn: typeof import("./provider-runtime.js").wrapProviderStreamFn;
const MODEL: ProviderRuntimeModel = {
id: "demo-model",
@ -62,7 +60,32 @@ const MODEL: ProviderRuntimeModel = {
};
describe("provider-runtime", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({
augmentModelCatalogWithProviderPlugins,
buildProviderAuthDoctorHintWithPlugin,
buildProviderMissingAuthMessageWithPlugin,
formatProviderAuthProfileApiKeyWithPlugin,
prepareProviderExtraParams,
resolveProviderCacheTtlEligibility,
resolveProviderBinaryThinking,
resolveProviderBuiltInModelSuppression,
resolveProviderDefaultThinkingLevel,
resolveProviderModernModelRef,
resolveProviderUsageSnapshotWithPlugin,
resolveProviderCapabilitiesWithPlugin,
resolveProviderUsageAuthWithPlugin,
resolveProviderXHighThinking,
normalizeProviderResolvedModelWithPlugin,
prepareProviderDynamicModel,
prepareProviderRuntimeAuth,
resetProviderRuntimeHookCacheForTest,
refreshProviderOAuthCredentialWithPlugin,
resolveProviderRuntimePlugin,
runProviderDynamicModel,
wrapProviderStreamFn,
} = await import("./provider-runtime.js"));
resetProviderRuntimeHookCacheForTest();
resolvePluginProvidersMock.mockReset();
resolvePluginProvidersMock.mockReturnValue([]);

View File

@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
const loadOpenClawPluginsMock = vi.fn();
const loadPluginManifestRegistryMock = vi.fn();
@ -12,8 +11,12 @@ vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistry: (...args: unknown[]) => loadPluginManifestRegistryMock(...args),
}));
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
let resolvePluginProviders: typeof import("./providers.js").resolvePluginProviders;
describe("resolvePluginProviders", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock.mockReturnValue({
providers: [{ pluginId: "google", provider: { id: "demo-provider" } }],
@ -29,6 +32,8 @@ describe("resolvePluginProviders", () => {
],
diagnostics: [],
});
({ resolveOwningPluginIdsForProvider, resolvePluginProviders } =
await import("./providers.js"));
});
it("forwards an explicit env to plugin loading", () => {

View File

@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolvePluginTools } from "./tools.js";
type MockRegistryToolEntry = {
pluginId: string;
@ -14,6 +13,8 @@ vi.mock("./loader.js", () => ({
loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params),
}));
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
function makeTool(name: string) {
return {
name,
@ -90,8 +91,10 @@ function resolveOptionalDemoTools(toolAllowlist?: string[]) {
}
describe("resolvePluginTools optional tools", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
loadOpenClawPluginsMock.mockClear();
({ resolvePluginTools } = await import("./tools.js"));
});
it("skips optional tools without explicit allowlist", () => {

View File

@ -1,9 +1,8 @@
/**
* Test: before_compaction & after_compaction hook wiring
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { makeZeroUsageSnapshot } from "../agents/usage.js";
import { emitAgentEvent } from "../infra/agent-events.js";
const hookMocks = vi.hoisted(() => ({
runner: {
@ -11,13 +10,6 @@ const hookMocks = vi.hoisted(() => ({
runBeforeCompaction: vi.fn(async () => {}),
runAfterCompaction: vi.fn(async () => {}),
},
}));
vi.mock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => hookMocks.runner,
}));
vi.mock("../infra/agent-events.js", () => ({
emitAgentEvent: vi.fn(),
}));
@ -25,19 +17,23 @@ describe("compaction hook wiring", () => {
let handleAutoCompactionStart: typeof import("../agents/pi-embedded-subscribe.handlers.compaction.js").handleAutoCompactionStart;
let handleAutoCompactionEnd: typeof import("../agents/pi-embedded-subscribe.handlers.compaction.js").handleAutoCompactionEnd;
beforeAll(async () => {
({ handleAutoCompactionStart, handleAutoCompactionEnd } =
await import("../agents/pi-embedded-subscribe.handlers.compaction.js"));
});
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
hookMocks.runner.hasHooks.mockClear();
hookMocks.runner.hasHooks.mockReturnValue(false);
hookMocks.runner.runBeforeCompaction.mockClear();
hookMocks.runner.runBeforeCompaction.mockResolvedValue(undefined);
hookMocks.runner.runAfterCompaction.mockClear();
hookMocks.runner.runAfterCompaction.mockResolvedValue(undefined);
vi.mocked(emitAgentEvent).mockClear();
hookMocks.emitAgentEvent.mockClear();
vi.doMock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => hookMocks.runner,
}));
vi.doMock("../infra/agent-events.js", () => ({
emitAgentEvent: hookMocks.emitAgentEvent,
}));
({ handleAutoCompactionStart, handleAutoCompactionEnd } =
await import("../agents/pi-embedded-subscribe.handlers.compaction.js"));
});
function createCompactionEndCtx(params: {
@ -94,7 +90,7 @@ describe("compaction hook wiring", () => {
const hookCtx = beforeCalls[0]?.[1] as { sessionKey?: string } | undefined;
expect(hookCtx?.sessionKey).toBe("agent:main:web-abc123");
expect(ctx.ensureCompactionPromise).toHaveBeenCalledTimes(1);
expect(emitAgentEvent).toHaveBeenCalledWith({
expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({
runId: "r1",
stream: "compaction",
data: { phase: "start" },
@ -135,7 +131,7 @@ describe("compaction hook wiring", () => {
expect(event?.compactedCount).toBe(1);
expect(ctx.incrementCompactionCount).toHaveBeenCalledTimes(1);
expect(ctx.maybeResolveCompactionWait).toHaveBeenCalledTimes(1);
expect(emitAgentEvent).toHaveBeenCalledWith({
expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({
runId: "r2",
stream: "compaction",
data: { phase: "end", willRetry: false, completed: true },
@ -166,7 +162,7 @@ describe("compaction hook wiring", () => {
expect(ctx.noteCompactionRetry).toHaveBeenCalledTimes(1);
expect(ctx.resetForCompactionRetry).toHaveBeenCalledTimes(1);
expect(ctx.maybeResolveCompactionWait).not.toHaveBeenCalled();
expect(emitAgentEvent).toHaveBeenCalledWith({
expect(hookMocks.emitAgentEvent).toHaveBeenCalledWith({
runId: "r3",
stream: "compaction",
data: { phase: "end", willRetry: true, completed: true },

View File

@ -17,19 +17,19 @@ vi.mock("../logging/diagnostic.js", () => ({
diagnosticLogger: diagnosticMocks.diag,
}));
import {
clearCommandLane,
CommandLaneClearedError,
enqueueCommand,
enqueueCommandInLane,
GatewayDrainingError,
getActiveTaskCount,
getQueueSize,
markGatewayDraining,
resetAllLanes,
setCommandLaneConcurrency,
waitForActiveTasks,
} from "./command-queue.js";
type CommandQueueModule = typeof import("./command-queue.js");
let clearCommandLane: CommandQueueModule["clearCommandLane"];
let CommandLaneClearedError: CommandQueueModule["CommandLaneClearedError"];
let enqueueCommand: CommandQueueModule["enqueueCommand"];
let enqueueCommandInLane: CommandQueueModule["enqueueCommandInLane"];
let GatewayDrainingError: CommandQueueModule["GatewayDrainingError"];
let getActiveTaskCount: CommandQueueModule["getActiveTaskCount"];
let getQueueSize: CommandQueueModule["getQueueSize"];
let markGatewayDraining: CommandQueueModule["markGatewayDraining"];
let resetAllLanes: CommandQueueModule["resetAllLanes"];
let setCommandLaneConcurrency: CommandQueueModule["setCommandLaneConcurrency"];
let waitForActiveTasks: CommandQueueModule["waitForActiveTasks"];
function createDeferred(): { promise: Promise<void>; resolve: () => void } {
let resolve!: () => void;
@ -54,7 +54,21 @@ function enqueueBlockedMainTask<T = void>(
}
describe("command queue", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({
clearCommandLane,
CommandLaneClearedError,
enqueueCommand,
enqueueCommandInLane,
GatewayDrainingError,
getActiveTaskCount,
getQueueSize,
markGatewayDraining,
resetAllLanes,
setCommandLaneConcurrency,
waitForActiveTasks,
} = await import("./command-queue.js"));
resetAllLanes();
diagnosticMocks.logLaneEnqueue.mockClear();
diagnosticMocks.logLaneDequeue.mockClear();

View File

@ -1,6 +1,6 @@
import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn());
@ -12,7 +12,9 @@ vi.mock("node:child_process", async () => {
};
});
import { runCommandWithTimeout } from "./exec.js";
type ExecModule = typeof import("./exec.js");
let runCommandWithTimeout: ExecModule["runCommandWithTimeout"];
function createFakeSpawnedChild() {
const child = new EventEmitter() as EventEmitter & ChildProcess;
@ -39,6 +41,11 @@ function createFakeSpawnedChild() {
}
describe("runCommandWithTimeout no-output timer", () => {
beforeEach(async () => {
vi.resetModules();
({ runCommandWithTimeout } = await import("./exec.js"));
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();

View File

@ -1,5 +1,5 @@
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn());
const execFileMock = vi.hoisted(() => vi.fn());
@ -13,7 +13,8 @@ vi.mock("node:child_process", async (importOriginal) => {
};
});
import { runCommandWithTimeout, runExec } from "./exec.js";
let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout;
let runExec: typeof import("./exec.js").runExec;
type MockChild = EventEmitter & {
stdout: EventEmitter;
@ -64,6 +65,11 @@ function expectCmdWrappedInvocation(params: {
}
describe("windows command wrapper behavior", () => {
beforeEach(async () => {
vi.resetModules();
({ runCommandWithTimeout, runExec } = await import("./exec.js"));
});
afterEach(() => {
spawnMock.mockReset();
execFileMock.mockReset();

View File

@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { killProcessTree } from "./kill-tree.js";
const { spawnMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
@ -9,6 +8,8 @@ vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
let killProcessTree: typeof import("./kill-tree.js").killProcessTree;
async function withPlatform<T>(platform: NodeJS.Platform, run: () => Promise<T> | T): Promise<T> {
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: platform, configurable: true });
@ -24,7 +25,9 @@ async function withPlatform<T>(platform: NodeJS.Platform, run: () => Promise<T>
describe("killProcessTree", () => {
let killSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
({ killProcessTree } = await import("./kill-tree.js"));
spawnMock.mockClear();
killSpy = vi.spyOn(process, "kill");
vi.useFakeTimers();

View File

@ -1,7 +1,7 @@
import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
const { spawnWithFallbackMock, killProcessTreeMock } = vi.hoisted(() => ({
spawnWithFallbackMock: vi.fn(),
@ -51,11 +51,9 @@ async function createAdapterHarness(params?: {
describe("createChildAdapter", () => {
const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ createChildAdapter } = await import("./child.js"));
});
beforeEach(() => {
spawnWithFallbackMock.mockClear();
killProcessTreeMock.mockClear();
delete process.env.OPENCLAW_SERVICE_MARKER;

View File

@ -1,4 +1,4 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { spawnMock, ptyKillMock, killProcessTreeMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
@ -39,11 +39,9 @@ function expectSpawnEnv() {
describe("createPtyAdapter", () => {
let createPtyAdapter: typeof import("./pty.js").createPtyAdapter;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ createPtyAdapter } = await import("./pty.js"));
});
beforeEach(() => {
spawnMock.mockClear();
ptyKillMock.mockClear();
killProcessTreeMock.mockClear();

View File

@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { createPtyAdapterMock } = vi.hoisted(() => ({
createPtyAdapterMock: vi.fn(),
@ -35,11 +35,9 @@ function createStubPtyAdapter() {
describe("process supervisor PTY command contract", () => {
let createProcessSupervisor: typeof import("./supervisor.js").createProcessSupervisor;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ createProcessSupervisor } = await import("./supervisor.js"));
});
beforeEach(() => {
createPtyAdapterMock.mockClear();
});

View File

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js";
const MOCK_USERNAME = "MockUser";
@ -8,15 +8,26 @@ vi.mock("node:os", () => ({
userInfo: () => ({ username: MOCK_USERNAME }),
}));
const {
createIcaclsResetCommand,
formatIcaclsResetCommand,
formatWindowsAclSummary,
inspectWindowsAcl,
parseIcaclsOutput,
resolveWindowsUserPrincipal,
summarizeWindowsAcl,
} = await import("./windows-acl.js");
let createIcaclsResetCommand: typeof import("./windows-acl.js").createIcaclsResetCommand;
let formatIcaclsResetCommand: typeof import("./windows-acl.js").formatIcaclsResetCommand;
let formatWindowsAclSummary: typeof import("./windows-acl.js").formatWindowsAclSummary;
let inspectWindowsAcl: typeof import("./windows-acl.js").inspectWindowsAcl;
let parseIcaclsOutput: typeof import("./windows-acl.js").parseIcaclsOutput;
let resolveWindowsUserPrincipal: typeof import("./windows-acl.js").resolveWindowsUserPrincipal;
let summarizeWindowsAcl: typeof import("./windows-acl.js").summarizeWindowsAcl;
beforeEach(async () => {
vi.resetModules();
({
createIcaclsResetCommand,
formatIcaclsResetCommand,
formatWindowsAclSummary,
inspectWindowsAcl,
parseIcaclsOutput,
resolveWindowsUserPrincipal,
summarizeWindowsAcl,
} = await import("./windows-acl.js"));
});
function aclEntry(params: {
principal: string;

View File

@ -1,7 +1,7 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let mockTtsPromise = vi.fn<(text: string, filePath: string) => Promise<void>>();
@ -13,7 +13,9 @@ vi.mock("node-edge-tts", () => ({
},
}));
const { edgeTTS } = await import("./tts-core.js");
type TtsCoreModule = typeof import("./tts-core.js");
let edgeTTS: TtsCoreModule["edgeTTS"];
const baseEdgeConfig = {
enabled: true,
@ -27,6 +29,11 @@ const baseEdgeConfig = {
describe("edgeTTS empty audio validation", () => {
let tempDir: string;
beforeEach(async () => {
vi.resetModules();
({ edgeTTS } = await import("./tts-core.js"));
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});

View File

@ -362,20 +362,43 @@ describe("tts", () => {
});
describe("summarizeText", () => {
let summarizeTextForTest: typeof summarizeText;
let resolveTtsConfigForTest: typeof resolveTtsConfig;
let completeSimpleForTest: typeof completeSimple;
let getApiKeyForModelForTest: typeof getApiKeyForModel;
let resolveModelAsyncForTest: typeof resolveModelAsync;
let ensureCustomApiRegisteredForTest: typeof ensureCustomApiRegistered;
const baseCfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } },
messages: { tts: {} },
};
const baseConfig = resolveTtsConfig(baseCfg);
beforeEach(async () => {
vi.resetModules();
({ completeSimple: completeSimpleForTest } = await import("@mariozechner/pi-ai"));
({ getApiKeyForModel: getApiKeyForModelForTest } = await import("../agents/model-auth.js"));
({ resolveModelAsync: resolveModelAsyncForTest } =
await import("../agents/pi-embedded-runner/model.js"));
({ ensureCustomApiRegistered: ensureCustomApiRegisteredForTest } =
await import("../agents/custom-api-registry.js"));
const ttsModule = await import("./tts.js");
summarizeTextForTest = ttsModule._test.summarizeText;
resolveTtsConfigForTest = ttsModule.resolveTtsConfig;
vi.mocked(completeSimpleForTest).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: "Summary" }]),
);
});
it("summarizes text and returns result with metrics", async () => {
const mockSummary = "This is a summarized version of the text.";
vi.mocked(completeSimple).mockResolvedValue(
const baseConfig = resolveTtsConfigForTest(baseCfg);
vi.mocked(completeSimpleForTest).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: mockSummary }]),
);
const longText = "A".repeat(2000);
const result = await summarizeText({
const result = await summarizeTextForTest({
text: longText,
targetLength: 1500,
cfg: baseCfg,
@ -387,11 +410,12 @@ describe("tts", () => {
expect(result.inputLength).toBe(2000);
expect(result.outputLength).toBe(mockSummary.length);
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
expect(completeSimple).toHaveBeenCalledTimes(1);
expect(completeSimpleForTest).toHaveBeenCalledTimes(1);
});
it("calls the summary model with the expected parameters", async () => {
await summarizeText({
const baseConfig = resolveTtsConfigForTest(baseCfg);
await summarizeTextForTest({
text: "Long text to summarize",
targetLength: 500,
cfg: baseCfg,
@ -399,11 +423,11 @@ describe("tts", () => {
timeoutMs: 30_000,
});
const callArgs = vi.mocked(completeSimple).mock.calls[0];
const callArgs = vi.mocked(completeSimpleForTest).mock.calls[0];
expect(callArgs?.[1]?.messages?.[0]?.role).toBe("user");
expect(callArgs?.[2]?.maxTokens).toBe(250);
expect(callArgs?.[2]?.temperature).toBe(0.3);
expect(getApiKeyForModel).toHaveBeenCalledTimes(1);
expect(getApiKeyForModelForTest).toHaveBeenCalledTimes(1);
});
it("uses summaryModel override when configured", async () => {
@ -411,8 +435,8 @@ describe("tts", () => {
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
messages: { tts: { summaryModel: "openai/gpt-4.1-mini" } },
};
const config = resolveTtsConfig(cfg);
await summarizeText({
const config = resolveTtsConfigForTest(cfg);
await summarizeTextForTest({
text: "Long text to summarize",
targetLength: 500,
cfg,
@ -420,11 +444,17 @@ describe("tts", () => {
timeoutMs: 30_000,
});
expect(resolveModelAsync).toHaveBeenCalledWith("openai", "gpt-4.1-mini", undefined, cfg);
expect(resolveModelAsyncForTest).toHaveBeenCalledWith(
"openai",
"gpt-4.1-mini",
undefined,
cfg,
);
});
it("registers the Ollama api before direct summarization", async () => {
vi.mocked(resolveModelAsync).mockResolvedValue({
const baseConfig = resolveTtsConfigForTest(baseCfg);
vi.mocked(resolveModelAsyncForTest).mockResolvedValue({
...createResolvedModel("ollama", "qwen3:8b", "ollama"),
model: {
...createResolvedModel("ollama", "qwen3:8b", "ollama").model,
@ -432,7 +462,7 @@ describe("tts", () => {
},
} as never);
await summarizeText({
await summarizeTextForTest({
text: "Long text to summarize",
targetLength: 500,
cfg: baseCfg,
@ -440,10 +470,11 @@ describe("tts", () => {
timeoutMs: 30_000,
});
expect(ensureCustomApiRegistered).toHaveBeenCalledWith("ollama", expect.any(Function));
expect(ensureCustomApiRegisteredForTest).toHaveBeenCalledWith("ollama", expect.any(Function));
});
it("validates targetLength bounds", async () => {
const baseConfig = resolveTtsConfigForTest(baseCfg);
const cases = [
{ targetLength: 99, shouldThrow: true },
{ targetLength: 100, shouldThrow: false },
@ -451,7 +482,7 @@ describe("tts", () => {
{ targetLength: 10001, shouldThrow: true },
] as const;
for (const testCase of cases) {
const call = summarizeText({
const call = summarizeTextForTest({
text: "text",
targetLength: testCase.targetLength,
cfg: baseCfg,
@ -469,6 +500,7 @@ describe("tts", () => {
});
it("throws when summary output is missing or empty", async () => {
const baseConfig = resolveTtsConfigForTest(baseCfg);
const cases = [
{ name: "no summary blocks", message: mockAssistantMessage([]) },
{
@ -477,9 +509,9 @@ describe("tts", () => {
},
] as const;
for (const testCase of cases) {
vi.mocked(completeSimple).mockResolvedValue(testCase.message);
vi.mocked(completeSimpleForTest).mockResolvedValue(testCase.message);
await expect(
summarizeText({
summarizeTextForTest({
text: "text",
targetLength: 500,
cfg: baseCfg,

View File

@ -12,10 +12,23 @@ import {
normalizeGatewayClientMode,
normalizeGatewayClientName,
} from "../gateway/protocol/client-info.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const;
export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL;
const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
type PluginRegistryStateLike = {
registry?: {
channels?: Array<{
plugin: {
id: string;
meta: {
aliases?: string[];
};
};
}>;
} | null;
};
const MARKDOWN_CAPABLE_CHANNELS = new Set<string>([
"slack",
@ -64,8 +77,13 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined
if (builtIn) {
return builtIn;
}
const registry = getActivePluginRegistry();
const pluginMatch = registry?.channels.find((entry) => {
const channels =
(
globalThis as typeof globalThis & {
[REGISTRY_STATE]?: PluginRegistryStateLike;
}
)[REGISTRY_STATE]?.registry?.channels ?? [];
const pluginMatch = channels.find((entry) => {
if (entry.plugin.id.toLowerCase() === normalized) {
return true;
}
@ -77,19 +95,23 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined
}
const listPluginChannelIds = (): string[] => {
const registry = getActivePluginRegistry();
if (!registry) {
return [];
}
return registry.channels.map((entry) => entry.plugin.id);
const channels =
(
globalThis as typeof globalThis & {
[REGISTRY_STATE]?: PluginRegistryStateLike;
}
)[REGISTRY_STATE]?.registry?.channels ?? [];
return channels.map((entry) => entry.plugin.id);
};
const listPluginChannelAliases = (): string[] => {
const registry = getActivePluginRegistry();
if (!registry) {
return [];
}
return registry.channels.flatMap((entry) => entry.plugin.meta.aliases ?? []);
const channels =
(
globalThis as typeof globalThis & {
[REGISTRY_STATE]?: PluginRegistryStateLike;
}
)[REGISTRY_STATE]?.registry?.channels ?? [];
return channels.flatMap((entry) => entry.plugin.meta.aliases ?? []);
};
export const listDeliverableMessageChannels = (): ChannelId[] =>

View File

@ -1,12 +1,13 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import * as normalize from "./normalize.js";
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
vi.mock("./normalize.js");
vi.mock("../infra/outbound/target-errors.js", () => ({
missingTargetError: (platform: string, format: string) => new Error(`${platform}: ${format}`),
}));
let resolveWhatsAppOutboundTarget: typeof import("./resolve-outbound-target.js").resolveWhatsAppOutboundTarget;
type ResolveParams = Parameters<typeof resolveWhatsAppOutboundTarget>[0];
const PRIMARY_TARGET = "+11234567890";
const SECONDARY_TARGET = "+19876543210";
@ -62,8 +63,10 @@ function expectDeniedForTarget(params: {
}
describe("resolveWhatsAppOutboundTarget", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.resetAllMocks();
({ resolveWhatsAppOutboundTarget } = await import("./resolve-outbound-target.js"));
});
describe("empty/missing to parameter", () => {