mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 20:51:10 +00:00
perf: optimize messaging plugin tests
This commit is contained in:
123
extensions/browser/src/doctor-browser.test.ts
Normal file
123
extensions/browser/src/doctor-browser.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { noteChromeMcpBrowserReadiness } from "./doctor-browser.js";
|
||||
|
||||
describe("browser doctor readiness", () => {
|
||||
it("does nothing when Chrome MCP is not configured", async () => {
|
||||
const noteFn = vi.fn();
|
||||
await noteChromeMcpBrowserReadiness(
|
||||
{
|
||||
browser: {
|
||||
profiles: {
|
||||
openclaw: { color: "#FF4500" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noteFn,
|
||||
},
|
||||
);
|
||||
expect(noteFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns when Chrome MCP is configured but Chrome is missing", async () => {
|
||||
const noteFn = vi.fn();
|
||||
await noteChromeMcpBrowserReadiness(
|
||||
{
|
||||
browser: {
|
||||
defaultProfile: "user",
|
||||
},
|
||||
},
|
||||
{
|
||||
noteFn,
|
||||
platform: "darwin",
|
||||
resolveChromeExecutable: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteFn).toHaveBeenCalledTimes(1);
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("Google Chrome was not found");
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("brave://inspect/#remote-debugging");
|
||||
});
|
||||
|
||||
it("warns when detected Chrome is too old for Chrome MCP", async () => {
|
||||
const noteFn = vi.fn();
|
||||
await noteChromeMcpBrowserReadiness(
|
||||
{
|
||||
browser: {
|
||||
profiles: {
|
||||
chromeLive: {
|
||||
driver: "existing-session",
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noteFn,
|
||||
platform: "linux",
|
||||
resolveChromeExecutable: () => ({ path: "/usr/bin/google-chrome" }),
|
||||
readVersion: () => "Google Chrome 143.0.7499.4",
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteFn).toHaveBeenCalledTimes(1);
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("too old");
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("Chrome 144+");
|
||||
});
|
||||
|
||||
it("reports the detected Chrome version for existing-session profiles", async () => {
|
||||
const noteFn = vi.fn();
|
||||
await noteChromeMcpBrowserReadiness(
|
||||
{
|
||||
browser: {
|
||||
profiles: {
|
||||
chromeLive: {
|
||||
driver: "existing-session",
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noteFn,
|
||||
platform: "win32",
|
||||
resolveChromeExecutable: () => ({
|
||||
path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
}),
|
||||
readVersion: () => "Google Chrome 144.0.7534.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteFn).toHaveBeenCalledTimes(1);
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain(
|
||||
"Detected Chrome Google Chrome 144.0.7534.0",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips Chrome auto-detection when profiles use explicit userDataDir", async () => {
|
||||
const noteFn = vi.fn();
|
||||
await noteChromeMcpBrowserReadiness(
|
||||
{
|
||||
browser: {
|
||||
profiles: {
|
||||
braveLive: {
|
||||
driver: "existing-session",
|
||||
userDataDir: "/Users/test/Library/Application Support/BraveSoftware/Brave-Browser",
|
||||
color: "#FB542B",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noteFn,
|
||||
resolveChromeExecutable: () => {
|
||||
throw new Error("should not look up Chrome");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(noteFn).toHaveBeenCalledTimes(1);
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("explicit Chromium user data directory");
|
||||
expect(String(noteFn.mock.calls[0]?.[0])).toContain("brave://inspect/#remote-debugging");
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,8 @@ const cliMocks = vi.hoisted(() => ({
|
||||
registerMatrixCli: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./src/cli.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./src/cli.js")>("./src/cli.js");
|
||||
vi.mock("./src/cli.js", () => {
|
||||
return {
|
||||
...actual,
|
||||
registerMatrixCli: cliMocks.registerMatrixCli,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,8 +3,10 @@ import { createNonExitingRuntimeEnv } from "../../../test/helpers/plugins/runtim
|
||||
|
||||
const resolveMatrixTargetsMock = vi.hoisted(() => vi.fn(async () => []));
|
||||
|
||||
vi.mock("./resolve-targets.js", () => ({
|
||||
resolveMatrixTargets: resolveMatrixTargetsMock,
|
||||
vi.mock("./resolver.runtime.js", () => ({
|
||||
matrixResolverRuntime: {
|
||||
resolveMatrixTargets: resolveMatrixTargetsMock,
|
||||
},
|
||||
}));
|
||||
|
||||
import { matrixResolverAdapter } from "./resolver.js";
|
||||
|
||||
@@ -29,38 +29,15 @@ function writeFixtureFile(fixtureRoot: string, relativePath: string, value: stri
|
||||
fs.writeFileSync(fullPath, value, "utf8");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0, tempDirs.length)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("loads the plugin-entry runtime wrapper through native ESM import", async () => {
|
||||
const wrapperPath = path.join(
|
||||
process.cwd(),
|
||||
"extensions",
|
||||
"matrix",
|
||||
"src",
|
||||
"plugin-entry.runtime.js",
|
||||
);
|
||||
const wrapperUrl = pathToFileURL(wrapperPath);
|
||||
const mod = await import(wrapperUrl.href);
|
||||
|
||||
expect(mod).toMatchObject({
|
||||
ensureMatrixCryptoRuntime: expect.any(Function),
|
||||
handleVerifyRecoveryKey: expect.any(Function),
|
||||
handleVerificationBootstrap: expect.any(Function),
|
||||
handleVerificationStatus: expect.any(Function),
|
||||
});
|
||||
}, 240_000);
|
||||
|
||||
it("loads the packaged runtime wrapper without recursing through the stable root alias", async () => {
|
||||
const fixtureRoot = makeFixtureRoot(".tmp-matrix-runtime-");
|
||||
const wrapperSource = fs.readFileSync(
|
||||
path.join(REPO_ROOT, "extensions", "matrix", "src", "plugin-entry.runtime.js"),
|
||||
"utf8",
|
||||
function writeJitiFixture(fixtureRoot: string) {
|
||||
writeFixtureFile(
|
||||
fixtureRoot,
|
||||
"node_modules/jiti/index.js",
|
||||
`module.exports = require(${JSON.stringify(JITI_ENTRY_PATH)});\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function writeOpenClawPackageFixture(fixtureRoot: string) {
|
||||
writeFixtureFile(
|
||||
fixtureRoot,
|
||||
"package.json",
|
||||
@@ -77,11 +54,52 @@ it("loads the packaged runtime wrapper without recursing through the stable root
|
||||
) + "\n",
|
||||
);
|
||||
writeFixtureFile(fixtureRoot, "dist/plugin-sdk/index.js", "export {};\n");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0, tempDirs.length)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("loads the source-checkout runtime wrapper through native ESM import", async () => {
|
||||
const fixtureRoot = makeFixtureRoot(".tmp-matrix-source-runtime-");
|
||||
const wrapperSource = fs.readFileSync(
|
||||
path.join(REPO_ROOT, "extensions", "matrix", "src", "plugin-entry.runtime.js"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
writeOpenClawPackageFixture(fixtureRoot);
|
||||
writeJitiFixture(fixtureRoot);
|
||||
writeFixtureFile(fixtureRoot, "extensions/matrix/src/plugin-entry.runtime.js", wrapperSource);
|
||||
writeFixtureFile(
|
||||
fixtureRoot,
|
||||
"node_modules/jiti/index.js",
|
||||
`module.exports = require(${JSON.stringify(JITI_ENTRY_PATH)});\n`,
|
||||
"extensions/matrix/plugin-entry.handlers.runtime.js",
|
||||
PACKAGED_RUNTIME_STUB,
|
||||
);
|
||||
|
||||
const wrapperUrl = pathToFileURL(
|
||||
path.join(fixtureRoot, "extensions", "matrix", "src", "plugin-entry.runtime.js"),
|
||||
);
|
||||
const mod = await import(`${wrapperUrl.href}?t=${Date.now()}`);
|
||||
|
||||
expect(mod).toMatchObject({
|
||||
ensureMatrixCryptoRuntime: expect.any(Function),
|
||||
handleVerifyRecoveryKey: expect.any(Function),
|
||||
handleVerificationBootstrap: expect.any(Function),
|
||||
handleVerificationStatus: expect.any(Function),
|
||||
});
|
||||
}, 240_000);
|
||||
|
||||
it("loads the packaged runtime wrapper without recursing through the stable root alias", async () => {
|
||||
const fixtureRoot = makeFixtureRoot(".tmp-matrix-runtime-");
|
||||
const wrapperSource = fs.readFileSync(
|
||||
path.join(REPO_ROOT, "extensions", "matrix", "src", "plugin-entry.runtime.js"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
writeOpenClawPackageFixture(fixtureRoot);
|
||||
writeJitiFixture(fixtureRoot);
|
||||
writeFixtureFile(fixtureRoot, "dist/plugin-entry.runtime-C88YIa_v.js", wrapperSource);
|
||||
writeFixtureFile(
|
||||
fixtureRoot,
|
||||
|
||||
5
extensions/matrix/src/resolver.runtime.ts
Normal file
5
extensions/matrix/src/resolver.runtime.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { resolveMatrixTargets } from "./resolve-targets.js";
|
||||
|
||||
export const matrixResolverRuntime = {
|
||||
resolveMatrixTargets,
|
||||
};
|
||||
@@ -3,8 +3,8 @@ import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { ResolvedMatrixAccount } from "./matrix/accounts.js";
|
||||
|
||||
const loadMatrixChannelRuntime = createLazyRuntimeNamedExport(
|
||||
() => import("./channel.runtime.js"),
|
||||
"matrixChannelRuntime",
|
||||
() => import("./resolver.runtime.js"),
|
||||
"matrixResolverRuntime",
|
||||
);
|
||||
|
||||
type MatrixResolver = NonNullable<ChannelPlugin<ResolvedMatrixAccount>["resolver"]>;
|
||||
|
||||
@@ -4,25 +4,6 @@ const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => vi.fn());
|
||||
const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
const resolveTelegramConversationRouteMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./bot-message-context.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./bot-message-context.runtime.js")>(
|
||||
"./bot-message-context.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
ensureConfiguredBindingRouteReady: (...args: unknown[]) =>
|
||||
ensureConfiguredBindingRouteReadyMock(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("./bot-message-context.session.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
|
||||
"./bot-message-context.session.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("./conversation-route.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./conversation-route.js")>("./conversation-route.js");
|
||||
@@ -35,6 +16,19 @@ vi.mock("./conversation-route.js", async () => {
|
||||
|
||||
let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
|
||||
|
||||
const configuredBindingRuntime = {
|
||||
ensureConfiguredBindingRouteReady: (...args: unknown[]) =>
|
||||
ensureConfiguredBindingRouteReadyMock(...args),
|
||||
} as NonNullable<
|
||||
import("./bot-message-context.types.js").BuildTelegramMessageContextParams["runtime"]
|
||||
>;
|
||||
|
||||
const configuredBindingSessionRuntime = {
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
} as NonNullable<
|
||||
import("./bot-message-context.types.js").BuildTelegramMessageContextParams["sessionRuntime"]
|
||||
>;
|
||||
|
||||
function createConfiguredTelegramBinding() {
|
||||
return {
|
||||
spec: {
|
||||
@@ -162,6 +156,8 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
it("treats configured topic bindings as explicit route matches on non-default accounts", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
accountId: "work",
|
||||
runtime: configuredBindingRuntime,
|
||||
sessionRuntime: configuredBindingSessionRuntime,
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "OpenClaw", is_forum: true },
|
||||
message_thread_id: 42,
|
||||
@@ -182,6 +178,8 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
it("skips ACP session initialization when topic access is denied", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
accountId: "work",
|
||||
runtime: configuredBindingRuntime,
|
||||
sessionRuntime: configuredBindingSessionRuntime,
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "OpenClaw", is_forum: true },
|
||||
message_thread_id: 42,
|
||||
@@ -201,6 +199,8 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
it("defers ACP session initialization for unauthorized control commands", async () => {
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
accountId: "work",
|
||||
runtime: configuredBindingRuntime,
|
||||
sessionRuntime: configuredBindingSessionRuntime,
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "OpenClaw", is_forum: true },
|
||||
message_thread_id: 42,
|
||||
@@ -229,6 +229,8 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
accountId: "work",
|
||||
runtime: configuredBindingRuntime,
|
||||
sessionRuntime: configuredBindingSessionRuntime,
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "OpenClaw", is_forum: true },
|
||||
message_thread_id: 42,
|
||||
|
||||
@@ -1,25 +1,56 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
|
||||
type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise<unknown>>;
|
||||
type BuildTelegramMessageContextForTest =
|
||||
typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
|
||||
type BuildTelegramMessageContextForTestParams = Parameters<BuildTelegramMessageContextForTest>[0];
|
||||
type TelegramTestSessionRuntime = NonNullable<
|
||||
import("./bot-message-context.types.js").BuildTelegramMessageContextParams["sessionRuntime"]
|
||||
>;
|
||||
|
||||
const hoisted = vi.hoisted((): { recordInboundSessionMock: AsyncUnknownMock } => ({
|
||||
recordInboundSessionMock: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
export const recordInboundSessionMock: AsyncUnknownMock = hoisted.recordInboundSessionMock;
|
||||
const finalizeInboundContextForTest = ((ctx) => {
|
||||
const next = ctx as Record<string, unknown>;
|
||||
const body = typeof next.Body === "string" ? next.Body : "";
|
||||
next.Body = body;
|
||||
next.BodyForAgent =
|
||||
typeof next.BodyForAgent === "string"
|
||||
? next.BodyForAgent
|
||||
: typeof next.RawBody === "string"
|
||||
? next.RawBody
|
||||
: body;
|
||||
next.BodyForCommands =
|
||||
typeof next.BodyForCommands === "string"
|
||||
? next.BodyForCommands
|
||||
: typeof next.CommandBody === "string"
|
||||
? next.CommandBody
|
||||
: typeof next.RawBody === "string"
|
||||
? next.RawBody
|
||||
: body;
|
||||
next.CommandAuthorized = Boolean(next.CommandAuthorized);
|
||||
return next;
|
||||
}) as NonNullable<TelegramTestSessionRuntime["finalizeInboundContext"]>;
|
||||
const recordInboundSessionForTest: NonNullable<
|
||||
TelegramTestSessionRuntime["recordInboundSession"]
|
||||
> = async (params) => {
|
||||
await recordInboundSessionMock(params);
|
||||
};
|
||||
|
||||
vi.mock("./bot-message-context.session.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
|
||||
"./bot-message-context.session.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
};
|
||||
});
|
||||
export const telegramRouteTestSessionRuntime = {
|
||||
finalizeInboundContext: finalizeInboundContextForTest,
|
||||
readSessionUpdatedAt: () => undefined,
|
||||
recordInboundSession: recordInboundSessionForTest,
|
||||
resolveInboundLastRouteSessionKey: ({ route, sessionKey }) =>
|
||||
route.lastRoutePolicy === "main" ? route.mainSessionKey : sessionKey,
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => null,
|
||||
resolveStorePath: () => "/tmp/openclaw/session-store.json",
|
||||
} satisfies TelegramTestSessionRuntime;
|
||||
|
||||
export async function loadTelegramMessageContextRouteHarness() {
|
||||
vi.resetModules();
|
||||
const [
|
||||
{ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot },
|
||||
{ buildTelegramMessageContextForTest },
|
||||
@@ -27,10 +58,20 @@ export async function loadTelegramMessageContextRouteHarness() {
|
||||
import("../../../src/config/config.js"),
|
||||
import("./bot-message-context.test-harness.js"),
|
||||
]);
|
||||
const buildTelegramMessageContextForRouteTest = (
|
||||
params: BuildTelegramMessageContextForTestParams,
|
||||
) =>
|
||||
buildTelegramMessageContextForTest({
|
||||
...params,
|
||||
sessionRuntime: {
|
||||
...telegramRouteTestSessionRuntime,
|
||||
...params.sessionRuntime,
|
||||
},
|
||||
});
|
||||
return {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
buildTelegramMessageContextForTest,
|
||||
buildTelegramMessageContextForTest: buildTelegramMessageContextForRouteTest,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import type {
|
||||
TelegramMediaRef,
|
||||
TelegramMessageContextOptions,
|
||||
TelegramMessageContextSessionRuntimeOverrides,
|
||||
} from "./bot-message-context.types.js";
|
||||
import {
|
||||
buildGroupLabel,
|
||||
@@ -43,6 +44,38 @@ type FinalizedTelegramInboundContext = ReturnType<
|
||||
typeof import("./bot-message-context.session.runtime.js").finalizeInboundContext
|
||||
>;
|
||||
|
||||
type TelegramMessageContextSessionRuntime =
|
||||
typeof import("./bot-message-context.session.runtime.js");
|
||||
|
||||
const sessionRuntimeMethods = [
|
||||
"finalizeInboundContext",
|
||||
"readSessionUpdatedAt",
|
||||
"recordInboundSession",
|
||||
"resolveInboundLastRouteSessionKey",
|
||||
"resolvePinnedMainDmOwnerFromAllowlist",
|
||||
"resolveStorePath",
|
||||
] as const satisfies readonly (keyof TelegramMessageContextSessionRuntime)[];
|
||||
|
||||
function hasCompleteSessionRuntime(
|
||||
runtime: TelegramMessageContextSessionRuntimeOverrides | undefined,
|
||||
): runtime is TelegramMessageContextSessionRuntime {
|
||||
return Boolean(
|
||||
runtime && sessionRuntimeMethods.every((method) => typeof runtime[method] === "function"),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadTelegramMessageContextSessionRuntime(
|
||||
runtime: TelegramMessageContextSessionRuntimeOverrides | undefined,
|
||||
): Promise<TelegramMessageContextSessionRuntime> {
|
||||
if (hasCompleteSessionRuntime(runtime)) {
|
||||
return runtime;
|
||||
}
|
||||
return {
|
||||
...(await import("./bot-message-context.session.runtime.js")),
|
||||
...runtime,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildTelegramInboundContextPayload(params: {
|
||||
cfg: OpenClawConfig;
|
||||
primaryCtx: TelegramContext;
|
||||
@@ -72,6 +105,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
options?: TelegramMessageContextOptions;
|
||||
dmAllowFrom?: Array<string | number>;
|
||||
effectiveGroupAllow?: NormalizedAllowFrom;
|
||||
sessionRuntime?: TelegramMessageContextSessionRuntimeOverrides;
|
||||
}): Promise<{
|
||||
ctxPayload: FinalizedTelegramInboundContext;
|
||||
skillFilter: string[] | undefined;
|
||||
@@ -105,6 +139,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
options,
|
||||
dmAllowFrom,
|
||||
effectiveGroupAllow,
|
||||
sessionRuntime: sessionRuntimeOverride,
|
||||
} = params;
|
||||
const replyTarget = describeReplyTarget(msg);
|
||||
const forwardOrigin = normalizeForwardedContext(msg);
|
||||
@@ -194,7 +229,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
const conversationLabel = isGroup
|
||||
? (groupLabel ?? `group:${chatId}`)
|
||||
: buildSenderLabel(msg, senderId || chatId);
|
||||
const sessionRuntime = await import("./bot-message-context.session.runtime.js");
|
||||
const sessionRuntime = await loadTelegramMessageContextSessionRuntime(sessionRuntimeOverride);
|
||||
const storePath = sessionRuntime.resolveStorePath(cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
|
||||
@@ -15,13 +15,14 @@ const internalHookMocks = vi.hoisted(() => ({
|
||||
triggerInternalHook: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/hook-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/hook-runtime")>(
|
||||
"openclaw/plugin-sdk/hook-runtime",
|
||||
);
|
||||
vi.mock("openclaw/plugin-sdk/hook-runtime", () => {
|
||||
return {
|
||||
...actual,
|
||||
createInternalHookEvent: internalHookMocks.createInternalHookEvent,
|
||||
fireAndForgetHook: (task: Promise<unknown>) => void task,
|
||||
toInternalMessageReceivedContext: (context: Record<string, unknown>) => ({
|
||||
...context,
|
||||
metadata: { to: context.to },
|
||||
}),
|
||||
triggerInternalHook: internalHookMocks.triggerInternalHook,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,17 +6,52 @@ export const baseTelegramMessageContextConfig = {
|
||||
messages: { groupChat: { mentionPatterns: [] } },
|
||||
} as never;
|
||||
|
||||
type TelegramTestSessionRuntime = NonNullable<BuildTelegramMessageContextParams["sessionRuntime"]>;
|
||||
const finalizeInboundContextForTest = ((ctx) => {
|
||||
const next = ctx as Record<string, unknown>;
|
||||
const body = typeof next.Body === "string" ? next.Body : "";
|
||||
next.Body = body;
|
||||
next.BodyForAgent =
|
||||
typeof next.BodyForAgent === "string"
|
||||
? next.BodyForAgent
|
||||
: typeof next.RawBody === "string"
|
||||
? next.RawBody
|
||||
: body;
|
||||
next.BodyForCommands =
|
||||
typeof next.BodyForCommands === "string"
|
||||
? next.BodyForCommands
|
||||
: typeof next.CommandBody === "string"
|
||||
? next.CommandBody
|
||||
: typeof next.RawBody === "string"
|
||||
? next.RawBody
|
||||
: body;
|
||||
next.CommandAuthorized = Boolean(next.CommandAuthorized);
|
||||
return next;
|
||||
}) as NonNullable<TelegramTestSessionRuntime["finalizeInboundContext"]>;
|
||||
|
||||
type BuildTelegramMessageContextForTestParams = {
|
||||
message: Record<string, unknown>;
|
||||
allMedia?: TelegramMediaRef[];
|
||||
options?: BuildTelegramMessageContextParams["options"];
|
||||
cfg?: Record<string, unknown>;
|
||||
accountId?: string;
|
||||
runtime?: BuildTelegramMessageContextParams["runtime"];
|
||||
sessionRuntime?: BuildTelegramMessageContextParams["sessionRuntime"];
|
||||
resolveGroupActivation?: BuildTelegramMessageContextParams["resolveGroupActivation"];
|
||||
resolveGroupRequireMention?: BuildTelegramMessageContextParams["resolveGroupRequireMention"];
|
||||
resolveTelegramGroupConfig?: BuildTelegramMessageContextParams["resolveTelegramGroupConfig"];
|
||||
};
|
||||
|
||||
const telegramMessageContextSessionRuntimeForTest = {
|
||||
finalizeInboundContext: finalizeInboundContextForTest,
|
||||
readSessionUpdatedAt: () => undefined,
|
||||
recordInboundSession: async () => undefined,
|
||||
resolveInboundLastRouteSessionKey: ({ route, sessionKey }) =>
|
||||
route.lastRoutePolicy === "main" ? route.mainSessionKey : sessionKey,
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => null,
|
||||
resolveStorePath: () => "/tmp/openclaw/session-store.json",
|
||||
} satisfies NonNullable<BuildTelegramMessageContextParams["sessionRuntime"]>;
|
||||
|
||||
export async function buildTelegramMessageContextForTest(
|
||||
params: BuildTelegramMessageContextForTestParams,
|
||||
): Promise<
|
||||
@@ -46,6 +81,14 @@ export async function buildTelegramMessageContextForTest(
|
||||
} as never,
|
||||
cfg: (params.cfg ?? baseTelegramMessageContextConfig) as never,
|
||||
loadFreshConfig: () => (params.cfg ?? baseTelegramMessageContextConfig) as never,
|
||||
runtime: {
|
||||
recordChannelActivity: () => undefined,
|
||||
...params.runtime,
|
||||
},
|
||||
sessionRuntime: {
|
||||
...telegramMessageContextSessionRuntimeForTest,
|
||||
...params.sessionRuntime,
|
||||
},
|
||||
account: { accountId: params.accountId ?? "default" } as never,
|
||||
historyLimit: 0,
|
||||
groupHistories: new Map(),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { telegramRouteTestSessionRuntime } from "./bot-message-context.route-test-support.js";
|
||||
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
|
||||
|
||||
const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
const resolveTelegramConversationRouteMock = vi.hoisted(() => vi.fn());
|
||||
type TelegramTestSessionRuntime = NonNullable<
|
||||
import("./bot-message-context.types.js").BuildTelegramMessageContextParams["sessionRuntime"]
|
||||
>;
|
||||
const recordInboundSessionForThreadBindingTest: NonNullable<
|
||||
TelegramTestSessionRuntime["recordInboundSession"]
|
||||
> = async (params) => {
|
||||
await recordInboundSessionMock(params);
|
||||
};
|
||||
|
||||
vi.mock("./bot-message-context.session.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
|
||||
"./bot-message-context.session.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("./conversation-route.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./conversation-route.js")>("./conversation-route.js");
|
||||
@@ -23,6 +23,11 @@ vi.mock("./conversation-route.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const threadBindingSessionRuntime = {
|
||||
...telegramRouteTestSessionRuntime,
|
||||
recordInboundSession: recordInboundSessionForThreadBindingTest,
|
||||
} satisfies TelegramTestSessionRuntime;
|
||||
|
||||
function createBoundRoute(params: { accountId: string; sessionKey: string; agentId: string }) {
|
||||
return {
|
||||
configuredBinding: null,
|
||||
@@ -55,6 +60,7 @@ describe("buildTelegramMessageContext thread binding override", () => {
|
||||
);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
sessionRuntime: threadBindingSessionRuntime,
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: -100200300, type: "supergroup", is_forum: true },
|
||||
@@ -94,6 +100,7 @@ describe("buildTelegramMessageContext thread binding override", () => {
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
accountId: "work",
|
||||
sessionRuntime: threadBindingSessionRuntime,
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: -100200300, type: "supergroup", is_forum: true },
|
||||
@@ -132,6 +139,7 @@ describe("buildTelegramMessageContext thread binding override", () => {
|
||||
);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
sessionRuntime: threadBindingSessionRuntime,
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 1234, type: "private" },
|
||||
|
||||
@@ -107,6 +107,8 @@ export const buildTelegramMessageContext = async ({
|
||||
resolveGroupRequireMention,
|
||||
resolveTelegramGroupConfig,
|
||||
loadFreshConfig,
|
||||
runtime,
|
||||
sessionRuntime,
|
||||
upsertPairingRequest,
|
||||
sendChatActionHandler,
|
||||
}: BuildTelegramMessageContextParams): Promise<TelegramMessageContext | null> => {
|
||||
@@ -146,7 +148,9 @@ export const buildTelegramMessageContext = async ({
|
||||
? (groupConfig.dmPolicy ?? dmPolicy)
|
||||
: dmPolicy;
|
||||
// Fresh config for bindings lookup; other routing inputs are payload-derived.
|
||||
const freshCfg = loadFreshConfig?.() ?? (await loadTelegramMessageContextRuntime()).loadConfig();
|
||||
const freshCfg =
|
||||
loadFreshConfig?.() ??
|
||||
(runtime?.loadConfig ?? (await loadTelegramMessageContextRuntime()).loadConfig)();
|
||||
let { route, configuredBinding, configuredBindingSessionKey } = resolveTelegramConversationRoute({
|
||||
cfg: freshCfg,
|
||||
accountId: account.accountId,
|
||||
@@ -269,7 +273,9 @@ export const buildTelegramMessageContext = async ({
|
||||
if (!configuredBinding) {
|
||||
return true;
|
||||
}
|
||||
const { ensureConfiguredBindingRouteReady } = await loadTelegramMessageContextRuntime();
|
||||
const ensureConfiguredBindingRouteReady =
|
||||
runtime?.ensureConfiguredBindingRouteReady ??
|
||||
(await loadTelegramMessageContextRuntime()).ensureConfiguredBindingRouteReady;
|
||||
const ensured = await ensureConfiguredBindingRouteReady({
|
||||
cfg: freshCfg,
|
||||
bindingResolution: configuredBinding,
|
||||
@@ -328,7 +334,10 @@ export const buildTelegramMessageContext = async ({
|
||||
baseRequireMention,
|
||||
);
|
||||
|
||||
(await loadTelegramMessageContextRuntime()).recordChannelActivity({
|
||||
const recordChannelActivity =
|
||||
runtime?.recordChannelActivity ??
|
||||
(await loadTelegramMessageContextRuntime()).recordChannelActivity;
|
||||
recordChannelActivity({
|
||||
channel: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "inbound",
|
||||
@@ -399,49 +408,51 @@ export const buildTelegramMessageContext = async ({
|
||||
resolvedStatusReactionEmojis,
|
||||
);
|
||||
let allowedStatusReactionEmojisPromise: Promise<Set<TelegramReactionEmoji> | null> | null = null;
|
||||
const statusReactionController: StatusReactionController | null =
|
||||
const createStatusReactionController =
|
||||
statusReactionsEnabled && msg.message_id
|
||||
? (await loadTelegramMessageContextRuntime()).createStatusReactionController({
|
||||
enabled: true,
|
||||
adapter: {
|
||||
setReaction: async (emoji: string) => {
|
||||
if (reactionApi) {
|
||||
if (!allowedStatusReactionEmojisPromise) {
|
||||
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
|
||||
chat: msg.chat,
|
||||
chatId,
|
||||
getChat: getChatApi ?? undefined,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
|
||||
const resolvedEmoji = resolveTelegramReactionVariant({
|
||||
requestedEmoji: emoji,
|
||||
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
|
||||
allowedEmojiReactions: allowedStatusReactionEmojis,
|
||||
});
|
||||
if (!resolvedEmoji) {
|
||||
return;
|
||||
}
|
||||
await reactionApi(chatId, msg.message_id, [
|
||||
{ type: "emoji", emoji: resolvedEmoji },
|
||||
]);
|
||||
}
|
||||
},
|
||||
// Telegram replaces atomically — no removeReaction needed
|
||||
},
|
||||
initialEmoji: ackReaction,
|
||||
emojis: resolvedStatusReactionEmojis,
|
||||
timing: statusReactionsConfig?.timing,
|
||||
onError: (err) => {
|
||||
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
||||
},
|
||||
})
|
||||
? (runtime?.createStatusReactionController ??
|
||||
(await loadTelegramMessageContextRuntime()).createStatusReactionController)
|
||||
: null;
|
||||
const statusReactionController: StatusReactionController | null = createStatusReactionController
|
||||
? createStatusReactionController({
|
||||
enabled: true,
|
||||
adapter: {
|
||||
setReaction: async (emoji: string) => {
|
||||
if (reactionApi) {
|
||||
if (!allowedStatusReactionEmojisPromise) {
|
||||
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
|
||||
chat: msg.chat,
|
||||
chatId,
|
||||
getChat: getChatApi ?? undefined,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
|
||||
const resolvedEmoji = resolveTelegramReactionVariant({
|
||||
requestedEmoji: emoji,
|
||||
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
|
||||
allowedEmojiReactions: allowedStatusReactionEmojis,
|
||||
});
|
||||
if (!resolvedEmoji) {
|
||||
return;
|
||||
}
|
||||
await reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: resolvedEmoji }]);
|
||||
}
|
||||
},
|
||||
// Telegram replaces atomically — no removeReaction needed
|
||||
},
|
||||
initialEmoji: ackReaction,
|
||||
emojis: resolvedStatusReactionEmojis,
|
||||
timing: statusReactionsConfig?.timing,
|
||||
onError: (err) => {
|
||||
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
// When status reactions are enabled, setQueued() replaces the simple ack reaction
|
||||
const ackReactionPromise: Promise<boolean> | null = statusReactionController
|
||||
@@ -494,6 +505,7 @@ export const buildTelegramMessageContext = async ({
|
||||
dmAllowFrom,
|
||||
effectiveGroupAllow,
|
||||
commandAuthorized: bodyResult.commandAuthorized,
|
||||
sessionRuntime,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,6 +44,28 @@ export type ResolveGroupActivation = (params: {
|
||||
|
||||
export type ResolveGroupRequireMention = (chatId: string | number) => boolean;
|
||||
|
||||
export type TelegramMessageContextRuntimeOverrides = Partial<
|
||||
Pick<
|
||||
typeof import("./bot-message-context.runtime.js"),
|
||||
| "createStatusReactionController"
|
||||
| "ensureConfiguredBindingRouteReady"
|
||||
| "loadConfig"
|
||||
| "recordChannelActivity"
|
||||
>
|
||||
>;
|
||||
|
||||
export type TelegramMessageContextSessionRuntimeOverrides = Partial<
|
||||
Pick<
|
||||
typeof import("./bot-message-context.session.runtime.js"),
|
||||
| "finalizeInboundContext"
|
||||
| "readSessionUpdatedAt"
|
||||
| "recordInboundSession"
|
||||
| "resolveInboundLastRouteSessionKey"
|
||||
| "resolvePinnedMainDmOwnerFromAllowlist"
|
||||
| "resolveStorePath"
|
||||
>
|
||||
>;
|
||||
|
||||
export type BuildTelegramMessageContextParams = {
|
||||
primaryCtx: TelegramContext;
|
||||
allMedia: TelegramMediaRef[];
|
||||
@@ -64,6 +86,8 @@ export type BuildTelegramMessageContextParams = {
|
||||
resolveGroupRequireMention: ResolveGroupRequireMention;
|
||||
resolveTelegramGroupConfig: ResolveTelegramGroupConfig;
|
||||
loadFreshConfig?: () => OpenClawConfig;
|
||||
runtime?: TelegramMessageContextRuntimeOverrides;
|
||||
sessionRuntime?: TelegramMessageContextSessionRuntimeOverrides;
|
||||
upsertPairingRequest?: typeof import("openclaw/plugin-sdk/conversation-runtime").upsertChannelPairingRequest;
|
||||
/** Global (per-account) handler for sendChatAction 401 backoff (#27092). */
|
||||
sendChatActionHandler: import("./sendchataction-401-backoff.js").TelegramSendChatActionHandler;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
createReplyDispatcher,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
type MsgContext,
|
||||
} from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { MockFn } from "openclaw/plugin-sdk/testing";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { beforeEach, vi } from "vitest";
|
||||
import type { TelegramBotDeps } from "./bot-deps.js";
|
||||
|
||||
@@ -51,9 +49,6 @@ export function getLoadWebMediaMock(): AnyMock {
|
||||
return loadWebMedia;
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
loadWebMedia,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
loadWebMedia,
|
||||
}));
|
||||
@@ -206,6 +201,25 @@ function parseModelRef(raw: string): { provider?: string; model: string } {
|
||||
return { model: trimmed };
|
||||
}
|
||||
|
||||
function normalizeLowercaseStringOrEmptyForTest(value: string | undefined): string {
|
||||
return value?.trim().toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function resolveDefaultModelForAgentForTest(params: { cfg: OpenClawConfig }): {
|
||||
provider: string;
|
||||
model: string;
|
||||
} {
|
||||
const modelConfig = params.cfg.agents?.defaults?.model;
|
||||
const rawModel =
|
||||
typeof modelConfig === "string" ? modelConfig : (modelConfig?.primary ?? "openai/gpt-5.4");
|
||||
const parsed = parseModelRef(rawModel);
|
||||
const provider = normalizeLowercaseStringOrEmptyForTest(parsed.provider) || "openai";
|
||||
return {
|
||||
provider: provider === "bedrock" ? "amazon-bedrock" : provider,
|
||||
model: parsed.model || "gpt-5.4",
|
||||
};
|
||||
}
|
||||
|
||||
function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
|
||||
byProvider: Map<string, Set<string>>;
|
||||
providers: string[];
|
||||
@@ -214,7 +228,7 @@ function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
|
||||
} {
|
||||
const byProvider = new Map<string, Set<string>>();
|
||||
const add = (providerRaw: string | undefined, modelRaw: string | undefined) => {
|
||||
const provider = normalizeLowercaseStringOrEmpty(providerRaw);
|
||||
const provider = normalizeLowercaseStringOrEmptyForTest(providerRaw);
|
||||
const model = modelRaw?.trim();
|
||||
if (!provider || !model) {
|
||||
return;
|
||||
@@ -224,7 +238,7 @@ function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
|
||||
byProvider.set(provider, existing);
|
||||
};
|
||||
|
||||
const resolvedDefault = resolveDefaultModelForAgent({ cfg });
|
||||
const resolvedDefault = resolveDefaultModelForAgentForTest({ cfg });
|
||||
add(resolvedDefault.provider, resolvedDefault.model);
|
||||
|
||||
for (const raw of Object.keys(cfg.agents?.defaults?.models ?? {})) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { getTelegramNetworkErrorOrigin } from "./network-errors.js";
|
||||
|
||||
const { botCtorSpy, telegramBotDepsForTest } =
|
||||
const { botCtorSpy, telegramBotDepsForTest, telegramBotRuntimeForTest } =
|
||||
await import("./bot.create-telegram-bot.test-harness.js");
|
||||
const { telegramBotRuntimeForTest } = await import("./bot.create-telegram-bot.test-harness.js");
|
||||
const { createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
|
||||
await import("./bot.js");
|
||||
setTelegramBotRuntimeForTest(
|
||||
|
||||
@@ -14,12 +14,9 @@ const listTelegramAccountIdsMock = vi.hoisted(() => vi.fn());
|
||||
const inspectTelegramAccountMock = vi.hoisted(() => vi.fn());
|
||||
const lookupTelegramChatIdMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime")>(
|
||||
"openclaw/plugin-sdk/runtime",
|
||||
);
|
||||
vi.mock("openclaw/plugin-sdk/runtime-secret-resolution", () => {
|
||||
return {
|
||||
...actual,
|
||||
getChannelsCommandSecretTargetIds: () => ["channels"],
|
||||
resolveCommandSecretRefsViaGateway: resolveCommandSecretRefsViaGatewayMock,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/testing";
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
|
||||
type UnknownMock = Mock<(...args: unknown[]) => unknown>;
|
||||
type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise<unknown>>;
|
||||
@@ -30,9 +30,11 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
describe("maybePersistResolvedTelegramTarget", () => {
|
||||
let maybePersistResolvedTelegramTarget: typeof import("./target-writeback.js").maybePersistResolvedTelegramTarget;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
beforeAll(async () => {
|
||||
({ maybePersistResolvedTelegramTarget } = await import("./target-writeback.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
readConfigFileSnapshotForWrite.mockReset();
|
||||
writeConfigFile.mockReset();
|
||||
loadCronStore.mockReset();
|
||||
|
||||
@@ -6,6 +6,13 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveOAuthDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { hasWebCredsSync } from "./src/creds-files.js";
|
||||
|
||||
type WhatsAppAuthPresenceParams =
|
||||
| {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
| OpenClawConfig;
|
||||
|
||||
function addAccountAuthDirs(
|
||||
authDirs: Set<string>,
|
||||
accountId: string,
|
||||
@@ -63,8 +70,11 @@ function listWhatsAppAuthDirs(
|
||||
}
|
||||
|
||||
export function hasAnyWhatsAppAuth(
|
||||
cfg: OpenClawConfig,
|
||||
params: WhatsAppAuthPresenceParams,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return listWhatsAppAuthDirs(cfg, env).some((authDir) => hasWebCredsSync(authDir));
|
||||
const cfg = params && typeof params === "object" && "cfg" in params ? params.cfg : params;
|
||||
const resolvedEnv =
|
||||
params && typeof params === "object" && "cfg" in params ? (params.env ?? env) : env;
|
||||
return listWhatsAppAuthDirs(cfg, resolvedEnv).some((authDir) => hasWebCredsSync(authDir));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
mockExtractMessageContent,
|
||||
mockGetContentType,
|
||||
mockIsJidGroup,
|
||||
mockNormalizeMessageContent,
|
||||
} from "../../../../test/mocks/baileys.js";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mockNormalizeMessageContent } from "../../../../test/mocks/baileys.js";
|
||||
|
||||
type MockMessageInput = Parameters<typeof mockNormalizeMessageContent>[0];
|
||||
|
||||
@@ -14,14 +9,8 @@ const { normalizeMessageContent, downloadMediaMessage } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@whiskeysockets/baileys", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@whiskeysockets/baileys")>("@whiskeysockets/baileys");
|
||||
return {
|
||||
...actual,
|
||||
DisconnectReason: actual.DisconnectReason ?? { loggedOut: 401 },
|
||||
extractMessageContent: vi.fn((message: MockMessageInput) => mockExtractMessageContent(message)),
|
||||
getContentType: vi.fn((message: MockMessageInput) => mockGetContentType(message)),
|
||||
isJidGroup: vi.fn((jid: string | undefined | null) => mockIsJidGroup(jid)),
|
||||
DisconnectReason: { loggedOut: 401 },
|
||||
normalizeMessageContent,
|
||||
downloadMediaMessage,
|
||||
};
|
||||
@@ -41,9 +30,11 @@ async function expectMimetype(message: Record<string, unknown>, expected: string
|
||||
}
|
||||
|
||||
describe("downloadInboundMedia", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
beforeAll(async () => {
|
||||
({ downloadInboundMedia } = await import("./media.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
normalizeMessageContent.mockClear();
|
||||
downloadMediaMessage.mockClear();
|
||||
mockSock.updateMediaMessage.mockClear();
|
||||
|
||||
@@ -53,6 +53,51 @@ const sessionState = vi.hoisted(() => ({
|
||||
sock: undefined as MockSock | undefined,
|
||||
}));
|
||||
|
||||
const inboundRuntimeMocks = vi.hoisted(() => {
|
||||
const wrapperKeys = [
|
||||
"ephemeralMessage",
|
||||
"viewOnceMessage",
|
||||
"viewOnceMessageV2",
|
||||
"viewOnceMessageV2Extension",
|
||||
"documentWithCaptionMessage",
|
||||
] as const;
|
||||
|
||||
function normalizeMessageContent(message: unknown): unknown {
|
||||
let current = message;
|
||||
while (current && typeof current === "object") {
|
||||
const record = current as Record<string, unknown>;
|
||||
const wrapper = wrapperKeys
|
||||
.map((key) => record[key])
|
||||
.find(
|
||||
(candidate): candidate is { message: unknown } =>
|
||||
Boolean(candidate) &&
|
||||
typeof candidate === "object" &&
|
||||
"message" in (candidate as Record<string, unknown>) &&
|
||||
Boolean((candidate as { message?: unknown }).message),
|
||||
);
|
||||
if (!wrapper) {
|
||||
break;
|
||||
}
|
||||
current = wrapper.message;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
downloadMediaMessage: vi.fn().mockResolvedValue(Buffer.from("fake-media-data")),
|
||||
isJidGroup: vi.fn((jid: string | undefined | null) =>
|
||||
typeof jid === "string" ? jid.endsWith("@g.us") : false,
|
||||
),
|
||||
normalizeMessageContent: vi.fn(normalizeMessageContent),
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
id: "mid",
|
||||
path: "/tmp/mid",
|
||||
size: 1,
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
function createResolvedMock() {
|
||||
return vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
@@ -77,21 +122,15 @@ function createMockSock(): MockSock {
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("./inbound/save-media.runtime.js", () => {
|
||||
vi.mock("./inbound/runtime-api.js", () => {
|
||||
return {
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
id: "mid",
|
||||
path: "/tmp/mid",
|
||||
size: 1,
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
DisconnectReason: { loggedOut: 401 },
|
||||
...inboundRuntimeMocks,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./session.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./session.js")>("./session.js");
|
||||
return {
|
||||
...actual,
|
||||
createWaSocket: vi.fn().mockImplementation(async () => {
|
||||
if (!sessionState.sock) {
|
||||
throw new Error("mock WhatsApp socket not initialized");
|
||||
@@ -100,6 +139,7 @@ vi.mock("./session.js", async () => {
|
||||
}),
|
||||
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
|
||||
getStatusCode: vi.fn(() => 500),
|
||||
formatError: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -111,9 +151,11 @@ export function getSock(): MockSock {
|
||||
}
|
||||
|
||||
type MonitorWebInbox = typeof import("./inbound.js").monitorWebInbox;
|
||||
type ResetWebInboundDedupe = typeof import("./inbound.js").resetWebInboundDedupe;
|
||||
export type InboxOnMessage = NonNullable<Parameters<MonitorWebInbox>[0]["onMessage"]>;
|
||||
export type InboxMonitorOptions = Parameters<MonitorWebInbox>[0];
|
||||
let monitorWebInbox: MonitorWebInbox;
|
||||
let resetWebInboundDedupe: ResetWebInboundDedupe;
|
||||
|
||||
function expectInboxPairingReplyText(
|
||||
text: string,
|
||||
@@ -142,7 +184,8 @@ export function getMonitorWebInbox(): MonitorWebInbox {
|
||||
}
|
||||
|
||||
export async function settleInboundWork() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
export async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, count: number) {
|
||||
@@ -223,13 +266,14 @@ export function installWebMonitorInboxUnitTestHooks(opts?: { authDir?: boolean }
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useRealTimers();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
sessionState.sock = createMockSock();
|
||||
resetPairingSecurityMocks(DEFAULT_WEB_INBOX_CONFIG);
|
||||
const inboundModule = await import("./inbound.js");
|
||||
monitorWebInbox = inboundModule.monitorWebInbox;
|
||||
const { resetWebInboundDedupe } = inboundModule;
|
||||
if (!monitorWebInbox || !resetWebInboundDedupe) {
|
||||
const inboundModule = await import("./inbound.js");
|
||||
monitorWebInbox = inboundModule.monitorWebInbox;
|
||||
resetWebInboundDedupe = inboundModule.resetWebInboundDedupe;
|
||||
}
|
||||
resetWebInboundDedupe();
|
||||
if (createAuthDir) {
|
||||
authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// Keep this list additive and scoped to the bundled Matrix surface.
|
||||
|
||||
import { createOptionalChannelSetupSurface } from "./channel-setup.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-loader.js";
|
||||
import {
|
||||
createLazyFacadeArrayValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-loader.js";
|
||||
|
||||
type MatrixFacadeModule = typeof import("@openclaw/matrix/contract-api.js");
|
||||
|
||||
@@ -190,10 +193,10 @@ export {
|
||||
export { setMatrixRuntime } from "./matrix-runtime-surface.js";
|
||||
|
||||
export const singleAccountKeysToMove: MatrixFacadeModule["singleAccountKeysToMove"] =
|
||||
loadMatrixFacadeModule().singleAccountKeysToMove;
|
||||
createLazyFacadeArrayValue(() => loadMatrixFacadeModule().singleAccountKeysToMove);
|
||||
|
||||
export const namedAccountPromotionKeys: MatrixFacadeModule["namedAccountPromotionKeys"] =
|
||||
loadMatrixFacadeModule().namedAccountPromotionKeys;
|
||||
createLazyFacadeArrayValue(() => loadMatrixFacadeModule().namedAccountPromotionKeys);
|
||||
|
||||
export const resolveSingleAccountPromotionTarget: MatrixFacadeModule["resolveSingleAccountPromotionTarget"] =
|
||||
((...args) =>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Manual facade. Keep loader boundary explicit.
|
||||
type FacadeModule = typeof import("@openclaw/telegram/contract-api.js");
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-loader.js";
|
||||
import {
|
||||
createLazyFacadeArrayValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-loader.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
@@ -17,7 +20,7 @@ export const parseTelegramTopicConversation: FacadeModule["parseTelegramTopicCon
|
||||
)) as FacadeModule["parseTelegramTopicConversation"];
|
||||
|
||||
export const singleAccountKeysToMove: FacadeModule["singleAccountKeysToMove"] =
|
||||
loadFacadeModule().singleAccountKeysToMove;
|
||||
createLazyFacadeArrayValue(() => loadFacadeModule().singleAccountKeysToMove);
|
||||
|
||||
export const collectTelegramSecurityAuditFindings: FacadeModule["collectTelegramSecurityAuditFindings"] =
|
||||
((...args) =>
|
||||
|
||||
@@ -19,7 +19,6 @@ const allowedNonExtensionTests = new Set<string>([
|
||||
"src/commands/onboard-channels.e2e.test.ts",
|
||||
"src/gateway/hooks.test.ts",
|
||||
"src/infra/outbound/deliver.test.ts",
|
||||
"src/media-generation/provider-capabilities.contract.test.ts",
|
||||
"src/plugins/interactive.test.ts",
|
||||
"src/plugins/contracts/discovery.contract.test.ts",
|
||||
"src/plugin-sdk/telegram-command-config.test.ts",
|
||||
|
||||
@@ -21,14 +21,30 @@ type TelegramProbe = import("@openclaw/telegram/api.js").TelegramProbe;
|
||||
type TelegramTokenResolution = import("@openclaw/telegram/api.js").TelegramTokenResolution;
|
||||
type WhatsAppApiSurface = typeof import("@openclaw/whatsapp/api.js");
|
||||
|
||||
const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } =
|
||||
loadBundledPluginApiSync<DiscordApiSurface>("discord");
|
||||
const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } =
|
||||
loadBundledPluginApiSync<SlackApiSurface>("slack");
|
||||
const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } =
|
||||
loadBundledPluginApiSync<TelegramApiSurface>("telegram");
|
||||
const { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig } =
|
||||
loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
|
||||
let discordApi: DiscordApiSurface | undefined;
|
||||
let slackApi: SlackApiSurface | undefined;
|
||||
let telegramApi: TelegramApiSurface | undefined;
|
||||
let whatsappApi: WhatsAppApiSurface | undefined;
|
||||
|
||||
function getDiscordApi(): DiscordApiSurface {
|
||||
discordApi ??= loadBundledPluginApiSync<DiscordApiSurface>("discord");
|
||||
return discordApi;
|
||||
}
|
||||
|
||||
function getSlackApi(): SlackApiSurface {
|
||||
slackApi ??= loadBundledPluginApiSync<SlackApiSurface>("slack");
|
||||
return slackApi;
|
||||
}
|
||||
|
||||
function getTelegramApi(): TelegramApiSurface {
|
||||
telegramApi ??= loadBundledPluginApiSync<TelegramApiSurface>("telegram");
|
||||
return telegramApi;
|
||||
}
|
||||
|
||||
function getWhatsAppApi(): WhatsAppApiSurface {
|
||||
whatsappApi ??= loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
|
||||
return whatsappApi;
|
||||
}
|
||||
|
||||
type DirectoryListFn = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -59,6 +75,9 @@ async function expectDirectoryIds(
|
||||
|
||||
export function describeDiscordPluginsCoreExtensionContract() {
|
||||
describe("discord plugins-core extension contract", () => {
|
||||
const listPeers = () => getDiscordApi().listDiscordDirectoryPeersFromConfig;
|
||||
const listGroups = () => getDiscordApi().listDiscordDirectoryGroupsFromConfig;
|
||||
|
||||
it("DiscordProbe satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<DiscordProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
@@ -90,17 +109,14 @@ export function describeDiscordPluginsCoreExtensionContract() {
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
listPeers(),
|
||||
cfg,
|
||||
["user:111", "user:12345", "user:222", "user:333", "user:444"],
|
||||
{ sorted: true },
|
||||
);
|
||||
await expectDirectoryIds(
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
cfg,
|
||||
["channel:555", "channel:666", "channel:777"],
|
||||
{ sorted: true },
|
||||
);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["channel:555", "channel:666", "channel:777"], {
|
||||
sorted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps directories readable when tokens are unresolved SecretRefs", async () => {
|
||||
@@ -125,8 +141,8 @@ export function describeDiscordPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(listDiscordDirectoryPeersFromConfig, cfg, ["user:111"]);
|
||||
await expectDirectoryIds(listDiscordDirectoryGroupsFromConfig, cfg, ["channel:555"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["user:111"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["channel:555"]);
|
||||
});
|
||||
|
||||
it("applies query and limit filtering for config-backed directories", async () => {
|
||||
@@ -147,7 +163,7 @@ export function describeDiscordPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const groups = await listDiscordDirectoryGroupsFromConfig({
|
||||
const groups = await listGroups()({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: "666",
|
||||
@@ -160,6 +176,9 @@ export function describeDiscordPluginsCoreExtensionContract() {
|
||||
|
||||
export function describeSlackPluginsCoreExtensionContract() {
|
||||
describe("slack plugins-core extension contract", () => {
|
||||
const listPeers = () => getSlackApi().listSlackDirectoryPeersFromConfig;
|
||||
const listGroups = () => getSlackApi().listSlackDirectoryGroupsFromConfig;
|
||||
|
||||
it("SlackProbe satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<SlackProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
@@ -178,12 +197,12 @@ export function describeSlackPluginsCoreExtensionContract() {
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(
|
||||
listSlackDirectoryPeersFromConfig,
|
||||
listPeers(),
|
||||
cfg,
|
||||
["user:u123", "user:u234", "user:u777", "user:u999"],
|
||||
{ sorted: true },
|
||||
);
|
||||
await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["channel:c111"]);
|
||||
});
|
||||
|
||||
it("keeps directories readable when tokens are unresolved SecretRefs", async () => {
|
||||
@@ -203,8 +222,8 @@ export function describeSlackPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(listSlackDirectoryPeersFromConfig, cfg, ["user:u123"]);
|
||||
await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["user:u123"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["channel:c111"]);
|
||||
});
|
||||
|
||||
it("applies query and limit filtering for config-backed directories", async () => {
|
||||
@@ -219,7 +238,7 @@ export function describeSlackPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const peers = await listSlackDirectoryPeersFromConfig({
|
||||
const peers = await listPeers()({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: "user:u",
|
||||
@@ -233,6 +252,9 @@ export function describeSlackPluginsCoreExtensionContract() {
|
||||
|
||||
export function describeTelegramPluginsCoreExtensionContract() {
|
||||
describe("telegram plugins-core extension contract", () => {
|
||||
const listPeers = () => getTelegramApi().listTelegramDirectoryPeersFromConfig;
|
||||
const listGroups = () => getTelegramApi().listTelegramDirectoryGroupsFromConfig;
|
||||
|
||||
it("TelegramProbe satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<TelegramProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
@@ -253,13 +275,10 @@ export function describeTelegramPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
cfg,
|
||||
["123", "456", "@alice", "@bob"],
|
||||
{ sorted: true },
|
||||
);
|
||||
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["123", "456", "@alice", "@bob"], {
|
||||
sorted: true,
|
||||
});
|
||||
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
|
||||
});
|
||||
|
||||
it("keeps fallback semantics when accountId is omitted", async () => {
|
||||
@@ -280,8 +299,8 @@ export function describeTelegramPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]);
|
||||
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["@alice"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,8 +320,8 @@ export function describeTelegramPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]);
|
||||
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["@alice"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
|
||||
});
|
||||
|
||||
it("applies query and limit filtering for config-backed directories", async () => {
|
||||
@@ -315,7 +334,7 @@ export function describeTelegramPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const groups = await listTelegramDirectoryGroupsFromConfig({
|
||||
const groups = await listGroups()({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: "-100",
|
||||
@@ -328,6 +347,9 @@ export function describeTelegramPluginsCoreExtensionContract() {
|
||||
|
||||
export function describeWhatsAppPluginsCoreExtensionContract() {
|
||||
describe("whatsapp plugins-core extension contract", () => {
|
||||
const listPeers = () => getWhatsAppApi().listWhatsAppDirectoryPeersFromConfig;
|
||||
const listGroups = () => getWhatsAppApi().listWhatsAppDirectoryGroupsFromConfig;
|
||||
|
||||
it("lists peers/groups from config", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
@@ -338,8 +360,8 @@ export function describeWhatsAppPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
await expectDirectoryIds(listWhatsAppDirectoryPeersFromConfig, cfg, ["+15550000000"]);
|
||||
await expectDirectoryIds(listWhatsAppDirectoryGroupsFromConfig, cfg, ["999@g.us"]);
|
||||
await expectDirectoryIds(listPeers(), cfg, ["+15550000000"]);
|
||||
await expectDirectoryIds(listGroups(), cfg, ["999@g.us"]);
|
||||
});
|
||||
|
||||
it("applies query and limit filtering for config-backed directories", async () => {
|
||||
@@ -351,7 +373,7 @@ export function describeWhatsAppPluginsCoreExtensionContract() {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const groups = await listWhatsAppDirectoryGroupsFromConfig({
|
||||
const groups = await listGroups()({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: "@g.us",
|
||||
|
||||
@@ -28,18 +28,14 @@ type MatrixTestApiSurface = typeof import("@openclaw/matrix/test-api.js");
|
||||
type TelegramApiSurface = typeof import("@openclaw/telegram/api.js");
|
||||
type TelegramTestApiSurface = typeof import("@openclaw/telegram/test-api.js");
|
||||
|
||||
const { bluebubblesPlugin } = loadBundledPluginApiSync<BluebubblesApiSurface>("bluebubbles");
|
||||
const { discordPlugin, discordThreadBindingTesting } =
|
||||
loadBundledPluginTestApiSync<DiscordTestApiSurface>("discord");
|
||||
const { feishuPlugin, feishuThreadBindingTesting } =
|
||||
loadBundledPluginApiSync<FeishuApiSurface>("feishu");
|
||||
const { imessagePlugin } = loadBundledPluginApiSync<IMessageApiSurface>("imessage");
|
||||
const { resetMatrixThreadBindingsForTests } = loadBundledPluginApiSync<MatrixApiSurface>("matrix");
|
||||
const { matrixPlugin, setMatrixRuntime } =
|
||||
loadBundledPluginTestApiSync<MatrixTestApiSurface>("matrix");
|
||||
const { telegramPlugin } = loadBundledPluginApiSync<TelegramApiSurface>("telegram");
|
||||
const { resetTelegramThreadBindingsForTests } =
|
||||
loadBundledPluginTestApiSync<TelegramTestApiSurface>("telegram");
|
||||
let bluebubblesApi: BluebubblesApiSurface | undefined;
|
||||
let discordTestApi: DiscordTestApiSurface | undefined;
|
||||
let feishuApi: FeishuApiSurface | undefined;
|
||||
let imessageApi: IMessageApiSurface | undefined;
|
||||
let matrixApi: MatrixApiSurface | undefined;
|
||||
let matrixTestApi: MatrixTestApiSurface | undefined;
|
||||
let telegramApi: TelegramApiSurface | undefined;
|
||||
let telegramTestApi: TelegramTestApiSurface | undefined;
|
||||
|
||||
type DiscordThreadBindingTesting = {
|
||||
resetThreadBindingsForTests: () => void;
|
||||
@@ -48,47 +44,58 @@ type DiscordThreadBindingTesting = {
|
||||
type ResetTelegramThreadBindingsForTests = () => Promise<void>;
|
||||
|
||||
function getBluebubblesPlugin(): ChannelPlugin {
|
||||
return bluebubblesPlugin as unknown as ChannelPlugin;
|
||||
bluebubblesApi ??= loadBundledPluginApiSync<BluebubblesApiSurface>("bluebubbles");
|
||||
return bluebubblesApi.bluebubblesPlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getDiscordPlugin(): ChannelPlugin {
|
||||
return discordPlugin as unknown as ChannelPlugin;
|
||||
discordTestApi ??= loadBundledPluginTestApiSync<DiscordTestApiSurface>("discord");
|
||||
return discordTestApi.discordPlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getFeishuPlugin(): ChannelPlugin {
|
||||
return feishuPlugin as unknown as ChannelPlugin;
|
||||
feishuApi ??= loadBundledPluginApiSync<FeishuApiSurface>("feishu");
|
||||
return feishuApi.feishuPlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getIMessagePlugin(): ChannelPlugin {
|
||||
return imessagePlugin as unknown as ChannelPlugin;
|
||||
imessageApi ??= loadBundledPluginApiSync<IMessageApiSurface>("imessage");
|
||||
return imessageApi.imessagePlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getMatrixPlugin(): ChannelPlugin {
|
||||
return matrixPlugin as unknown as ChannelPlugin;
|
||||
matrixTestApi ??= loadBundledPluginTestApiSync<MatrixTestApiSurface>("matrix");
|
||||
return matrixTestApi.matrixPlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getSetMatrixRuntime(): (runtime: PluginRuntime) => void {
|
||||
return setMatrixRuntime;
|
||||
matrixTestApi ??= loadBundledPluginTestApiSync<MatrixTestApiSurface>("matrix");
|
||||
return matrixTestApi.setMatrixRuntime;
|
||||
}
|
||||
|
||||
function getTelegramPlugin(): ChannelPlugin {
|
||||
return telegramPlugin as unknown as ChannelPlugin;
|
||||
telegramApi ??= loadBundledPluginApiSync<TelegramApiSurface>("telegram");
|
||||
return telegramApi.telegramPlugin as unknown as ChannelPlugin;
|
||||
}
|
||||
|
||||
function getDiscordThreadBindingTesting(): DiscordThreadBindingTesting {
|
||||
return discordThreadBindingTesting;
|
||||
discordTestApi ??= loadBundledPluginTestApiSync<DiscordTestApiSurface>("discord");
|
||||
return discordTestApi.discordThreadBindingTesting;
|
||||
}
|
||||
|
||||
function getResetTelegramThreadBindingsForTests(): ResetTelegramThreadBindingsForTests {
|
||||
return resetTelegramThreadBindingsForTests;
|
||||
telegramTestApi ??= loadBundledPluginTestApiSync<TelegramTestApiSurface>("telegram");
|
||||
return telegramTestApi.resetTelegramThreadBindingsForTests;
|
||||
}
|
||||
|
||||
async function getFeishuThreadBindingTesting() {
|
||||
return feishuThreadBindingTesting;
|
||||
feishuApi ??= loadBundledPluginApiSync<FeishuApiSurface>("feishu");
|
||||
return feishuApi.feishuThreadBindingTesting;
|
||||
}
|
||||
|
||||
async function getResetMatrixThreadBindingsForTests() {
|
||||
return resetMatrixThreadBindingsForTests;
|
||||
matrixApi ??= loadBundledPluginApiSync<MatrixApiSurface>("matrix");
|
||||
return matrixApi.resetMatrixThreadBindingsForTests;
|
||||
}
|
||||
|
||||
function resolveSessionBindingContractRuntimeConfig(id: string) {
|
||||
@@ -106,21 +113,51 @@ function resolveSessionBindingContractRuntimeConfig(id: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function setSessionBindingPluginRegistryForTests(): void {
|
||||
getSetMatrixRuntime()({
|
||||
state: {
|
||||
resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
} as PluginRuntime);
|
||||
function getSessionBindingPlugin(id: string): ChannelPlugin {
|
||||
switch (id) {
|
||||
case "bluebubbles":
|
||||
return getBluebubblesPlugin();
|
||||
case "discord":
|
||||
return getDiscordPlugin();
|
||||
case "feishu":
|
||||
return getFeishuPlugin();
|
||||
case "imessage":
|
||||
return getIMessagePlugin();
|
||||
case "matrix":
|
||||
getSetMatrixRuntime()({
|
||||
state: {
|
||||
resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
} as PluginRuntime);
|
||||
return getMatrixPlugin();
|
||||
case "telegram":
|
||||
return getTelegramPlugin();
|
||||
default:
|
||||
throw new Error(`missing session binding plugin fixture for ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const channels = [
|
||||
getBluebubblesPlugin(),
|
||||
getDiscordPlugin(),
|
||||
getFeishuPlugin(),
|
||||
getIMessagePlugin(),
|
||||
getMatrixPlugin(),
|
||||
getTelegramPlugin(),
|
||||
].map((plugin) => ({
|
||||
async function resetSessionBindingPluginFixtureForTests(id: string): Promise<void> {
|
||||
switch (id) {
|
||||
case "discord":
|
||||
getDiscordThreadBindingTesting().resetThreadBindingsForTests();
|
||||
return;
|
||||
case "feishu":
|
||||
(await getFeishuThreadBindingTesting()).resetFeishuThreadBindingsForTests();
|
||||
return;
|
||||
case "matrix":
|
||||
(await getResetMatrixThreadBindingsForTests())();
|
||||
return;
|
||||
case "telegram":
|
||||
await getResetTelegramThreadBindingsForTests()();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionBindingPluginRegistryForTests(id: string): void {
|
||||
const channels = [getSessionBindingPlugin(id)].map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
plugin,
|
||||
source: "test" as const,
|
||||
@@ -182,12 +219,9 @@ export function describeSessionBindingRegistryBackedContract(id: string) {
|
||||
setRuntimeConfigSnapshot(runtimeConfig);
|
||||
// These suites only exercise the session-binding channels, so avoid the broader
|
||||
// default registry helper and seed only the six plugins this contract lane needs.
|
||||
setSessionBindingPluginRegistryForTests();
|
||||
setSessionBindingPluginRegistryForTests(entry.id);
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
getDiscordThreadBindingTesting().resetThreadBindingsForTests();
|
||||
(await getFeishuThreadBindingTesting()).resetFeishuThreadBindingsForTests();
|
||||
(await getResetMatrixThreadBindingsForTests())();
|
||||
await getResetTelegramThreadBindingsForTests()();
|
||||
await resetSessionBindingPluginFixtureForTests(entry.id);
|
||||
});
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
|
||||
Reference in New Issue
Block a user