mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 23:41:07 +00:00
fix: stabilize full gate
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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() {},
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] =>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user