test: speed channel contract hotspots

This commit is contained in:
Peter Steinberger
2026-04-18 01:00:18 +01:00
parent 576ce7c656
commit 569247cff8
11 changed files with 230 additions and 145 deletions

View File

@@ -2,7 +2,7 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import {
createResolvedDirectoryEntriesLister,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
} from "openclaw/plugin-sdk/directory-config-runtime";
import { mergeDiscordAccountConfig, resolveDefaultDiscordAccountId } from "./accounts.js";
function resolveDiscordDirectoryConfigAccount(

View File

@@ -2,7 +2,7 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-resolution";
import {
createResolvedDirectoryEntriesLister,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
} from "openclaw/plugin-sdk/directory-config-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js";
import { parseSlackTarget } from "./targets.js";

View File

@@ -1,7 +1,7 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-core";
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime";
import { createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
import { createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-config-runtime";
import { mergeTelegramAccountConfig } from "./account-config.js";
import { resolveDefaultTelegramAccountSelection } from "./account-selection.js";

View File

@@ -1,16 +1,25 @@
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
import {
listResolvedDirectoryGroupEntriesFromMapKeys,
listResolvedDirectoryUserEntriesFromAllowFrom,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import { resolveWhatsAppAccount, type ResolvedWhatsAppAccount } from "./accounts.js";
} from "openclaw/plugin-sdk/directory-config-runtime";
import { resolveMergedWhatsAppAccountConfig } from "./account-config.js";
import type { WhatsAppAccountConfig } from "./account-types.js";
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
type WhatsAppDirectoryAccount = WhatsAppAccountConfig & { accountId: string };
function resolveWhatsAppDirectoryAccount(
cfg: DirectoryConfigParams["cfg"],
accountId?: string | null,
): WhatsAppDirectoryAccount {
return resolveMergedWhatsAppAccountConfig({ cfg, accountId });
}
export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConfigParams) {
return listResolvedDirectoryUserEntriesFromAllowFrom<ResolvedWhatsAppAccount>({
return listResolvedDirectoryUserEntriesFromAllowFrom<WhatsAppDirectoryAccount>({
...params,
resolveAccount: adaptScopedAccountAccessor(resolveWhatsAppAccount),
resolveAccount: resolveWhatsAppDirectoryAccount,
resolveAllowFrom: (account) => account.allowFrom,
normalizeId: (entry) => {
const normalized = normalizeWhatsAppTarget(entry);
@@ -23,9 +32,9 @@ export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConf
}
export async function listWhatsAppDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
return listResolvedDirectoryGroupEntriesFromMapKeys<ResolvedWhatsAppAccount>({
return listResolvedDirectoryGroupEntriesFromMapKeys<WhatsAppDirectoryAccount>({
...params,
resolveAccount: adaptScopedAccountAccessor(resolveWhatsAppAccount),
resolveAccount: resolveWhatsAppDirectoryAccount,
resolveGroups: (account) => account.groups,
});
}

View File

@@ -716,6 +716,10 @@
"types": "./dist/plugin-sdk/global-singleton.d.ts",
"default": "./dist/plugin-sdk/global-singleton.js"
},
"./plugin-sdk/directory-config-runtime": {
"types": "./dist/plugin-sdk/directory-config-runtime.d.ts",
"default": "./dist/plugin-sdk/directory-config-runtime.js"
},
"./plugin-sdk/directory-runtime": {
"types": "./dist/plugin-sdk/directory-runtime.d.ts",
"default": "./dist/plugin-sdk/directory-runtime.js"

View File

@@ -165,6 +165,7 @@
"string-coerce-runtime",
"group-access",
"global-singleton",
"directory-config-runtime",
"directory-runtime",
"googlechat",
"googlechat-runtime-shared",

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import {
isSignalSenderAllowed,
getSignalContractSurface,
type SignalSender,
} from "../../../../test/helpers/channels/dm-policy-contract.js";
import {
@@ -19,24 +19,29 @@ const signalSender: SignalSender = {
raw: "+15550001111",
e164: "+15550001111",
};
const signalSenderE164 = "+15550001111";
const channelSmokeCases: ChannelSmokeCase[] = [
{
name: "bluebubbles",
storeAllowFrom: ["attacker-user"],
isSenderAllowed: (allowFrom) => allowFrom.includes("attacker-user"),
},
{
name: "signal",
storeAllowFrom: [signalSender.e164],
isSenderAllowed: (allowFrom) => isSignalSenderAllowed(signalSender, allowFrom),
},
{
name: "mattermost",
storeAllowFrom: ["user:attacker-user"],
isSenderAllowed: (allowFrom) => allowFrom.includes("user:attacker-user"),
},
];
function createChannelSmokeCases(
isSignalSenderAllowed: (sender: SignalSender, allowFrom: string[]) => boolean,
): ChannelSmokeCase[] {
return [
{
name: "bluebubbles",
storeAllowFrom: ["attacker-user"],
isSenderAllowed: (allowFrom) => allowFrom.includes("attacker-user"),
},
{
name: "signal",
storeAllowFrom: [signalSenderE164],
isSenderAllowed: (allowFrom) => isSignalSenderAllowed(signalSender, allowFrom),
},
{
name: "mattermost",
storeAllowFrom: ["user:attacker-user"],
isSenderAllowed: (allowFrom) => allowFrom.includes("user:attacker-user"),
},
];
}
function expandChannelIngressCases(cases: readonly ChannelSmokeCase[]) {
return cases.flatMap((testCase) =>
@@ -66,13 +71,15 @@ describe("security/dm-policy-shared channel smoke", () => {
expect(access.reason).toBe("groupPolicy=allowlist (not allowlisted)");
}
it.each(expandChannelIngressCases(channelSmokeCases))(
"[$testCase.name] blocks group $ingress when sender is only in pairing store",
({ testCase }) => {
it("blocks group ingress when sender is only in pairing store", async () => {
const { isSignalSenderAllowed } = await getSignalContractSurface();
for (const { testCase } of expandChannelIngressCases(
createChannelSmokeCases(isSignalSenderAllowed),
)) {
expectBlockedGroupAccess({
storeAllowFrom: testCase.storeAllowFrom,
isSenderAllowed: testCase.isSenderAllowed,
});
},
);
}
});
});

View File

@@ -0,0 +1,22 @@
/** Slim directory-config helper surface for config-backed plugin directory contracts. */
export type { DirectoryConfigParams } from "../channels/plugins/directory-types.js";
export type {
ChannelDirectoryEntry,
ChannelDirectoryEntryKind,
} from "../channels/plugins/types.public.js";
export {
applyDirectoryQueryAndLimit,
collectNormalizedDirectoryIds,
createInspectedDirectoryEntriesLister,
createResolvedDirectoryEntriesLister,
listDirectoryEntriesFromSources,
listDirectoryGroupEntriesFromMapKeys,
listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
listDirectoryUserEntriesFromAllowFrom,
listDirectoryUserEntriesFromAllowFromAndMapKeys,
listInspectedDirectoryEntriesFromSources,
listResolvedDirectoryEntriesFromSources,
listResolvedDirectoryGroupEntriesFromMapKeys,
listResolvedDirectoryUserEntriesFromAllowFrom,
toDirectoryEntries,
} from "../channels/plugins/directory-config-helpers.js";

View File

@@ -1,7 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import path, { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { describe, expect, it } from "vitest";
import { listBundledPluginMetadata } from "../bundled-plugin-metadata.js";
import { loadPluginManifestRegistry } from "../manifest-registry.js";
@@ -25,6 +24,13 @@ const FORBIDDEN_CONTRACT_MODULE_PATH_PATTERNS = [
/(^|\/)[^/]*\.test(?:[-.][^/]*)?\.[cm]?[jt]s$/u,
/(^|\/)[^/]*(?:test-harness|test-plugin|test-helper|test-support|harness)[^/]*\.[cm]?[jt]s$/u,
] as const;
const STATIC_FROM_IMPORT_RE =
/^\s*import(?:\s+type)?\s+(?!["'])([\s\S]*?)\s+from\s*["']([^"']+)["']/gmu;
const STATIC_SIDE_EFFECT_IMPORT_RE = /^\s*import\s*["']([^"']+)["']/gmu;
const RE_EXPORT_STAR_RE =
/^\s*export\s+(?:type\s+)?\*\s*(?:as\s+\w+\s+)?from\s*["']([^"']+)["']/gmu;
const RE_EXPORT_NAMED_RE = /^\s*export\s+(?:type\s+)?\{[^}]*\}\s+from\s*["']([^"']+)["']/gmu;
function listBundledPluginRoots() {
return loadPluginManifestRegistry({})
.plugins.filter((plugin) => plugin.origin === "bundled")
@@ -60,77 +66,92 @@ function collectProductionContractEntryPaths(): Array<{
entryPath: string;
pluginRoot: string;
}> {
return listBundledPluginMetadata({ rootDir: REPO_ROOT }).flatMap((plugin) => {
const pluginRoot = resolve(REPO_ROOT, "extensions", plugin.dirName);
const entryPaths = new Set<string>();
for (const artifact of plugin.publicSurfaceArtifacts ?? []) {
if (!isGuardedContractArtifactBasename(artifact)) {
continue;
return listBundledPluginMetadata({ rootDir: REPO_ROOT, includeChannelConfigs: false }).flatMap(
(plugin) => {
const pluginRoot = resolve(REPO_ROOT, "extensions", plugin.dirName);
const entryPaths = new Set<string>();
for (const artifact of plugin.publicSurfaceArtifacts ?? []) {
if (!isGuardedContractArtifactBasename(artifact)) {
continue;
}
const sourcePath = resolvePublicSurfaceSourcePath(pluginRoot, artifact);
if (sourcePath) {
entryPaths.add(sourcePath);
}
}
const sourcePath = resolvePublicSurfaceSourcePath(pluginRoot, artifact);
if (sourcePath) {
entryPaths.add(sourcePath);
}
}
return [...entryPaths].map((entryPath) => ({
pluginId: plugin.manifest.id,
entryPath,
pluginRoot,
}));
});
return [...entryPaths].map((entryPath) => ({
pluginId: plugin.manifest.id,
entryPath,
pluginRoot,
}));
},
);
}
function formatRepoRelativePath(filePath: string): string {
return relative(REPO_ROOT, filePath).replaceAll(path.sep, "/");
}
function stripSourceComments(source: string): string {
return source.replaceAll(/\/\*[\s\S]*?\*\//gu, "").replaceAll(/(^|[^:])\/\/.*$/gmu, "$1");
}
function importsDefinePluginEntry(importClause: string | undefined): boolean {
const namedImports = importClause?.match(/\{([\s\S]*)\}/u)?.[1];
if (!namedImports) {
return false;
}
return namedImports
.split(",")
.map((part) => part.trim().replace(/^type\s+/u, ""))
.some((part) => part.split(/\s+as\s+/u)[0]?.trim() === "definePluginEntry");
}
function analyzeSourceModule(params: { filePath: string; source: string }): {
specifiers: string[];
relativeSpecifiers: string[];
importsDefinePluginEntryFromCore: boolean;
} {
const sourceFile = ts.createSourceFile(
params.filePath,
params.source,
ts.ScriptTarget.Latest,
true,
);
const source = stripSourceComments(params.source);
const specifiers = new Set<string>();
let importsDefinePluginEntryFromCore = false;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
? statement.moduleSpecifier.text
: undefined;
if (specifier) {
specifiers.add(specifier);
}
if (
specifier === "openclaw/plugin-sdk/core" &&
statement.importClause?.namedBindings &&
ts.isNamedImports(statement.importClause.namedBindings) &&
statement.importClause.namedBindings.elements.some(
(element) => (element.propertyName?.text ?? element.name.text) === "definePluginEntry",
)
) {
importsDefinePluginEntryFromCore = true;
}
for (const match of source.matchAll(STATIC_FROM_IMPORT_RE)) {
const importClause = match[1];
const specifier = match[2];
if (!specifier) {
continue;
}
specifiers.add(specifier);
if (!ts.isExportDeclaration(statement)) {
continue;
}
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
specifiers.add(statement.moduleSpecifier.text);
if (specifier === "openclaw/plugin-sdk/core" && importsDefinePluginEntry(importClause)) {
importsDefinePluginEntryFromCore = true;
}
}
const nextSpecifiers = [...specifiers];
for (const match of source.matchAll(STATIC_SIDE_EFFECT_IMPORT_RE)) {
const specifier = match[1];
if (specifier) {
specifiers.add(specifier);
}
}
for (const match of source.matchAll(RE_EXPORT_STAR_RE)) {
const specifier = match[1];
if (specifier) {
specifiers.add(specifier);
}
}
for (const match of source.matchAll(RE_EXPORT_NAMED_RE)) {
const specifier = match[1];
if (specifier) {
specifiers.add(specifier);
}
}
const nextSpecifiers = [...specifiers].toSorted();
return {
specifiers: nextSpecifiers,
relativeSpecifiers: nextSpecifiers.filter((specifier) => specifier.startsWith(".")),

View File

@@ -1,19 +1,21 @@
import type { SignalSender } from "@openclaw/signal/contract-api.js";
import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
type SignalContractApiSurface = Pick<
typeof import("@openclaw/signal/contract-api.js"),
"isSignalSenderAllowed"
>;
let signalContractSurface: SignalContractApiSurface | undefined;
let signalContractSurface: Promise<SignalContractApiSurface> | undefined;
function getSignalContractSurface(): SignalContractApiSurface {
signalContractSurface ??= loadBundledPluginContractApiSync<SignalContractApiSurface>("signal");
export function getSignalContractSurface(): Promise<SignalContractApiSurface> {
signalContractSurface ??= import(
resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "signal",
artifactBasename: "contract-api.js",
})
) as Promise<SignalContractApiSurface>;
return signalContractSurface;
}
export const isSignalSenderAllowed = (
...args: Parameters<SignalContractApiSurface["isSignalSenderAllowed"]>
) => getSignalContractSurface().isSignalSenderAllowed(...args);
export type { SignalSender };

View File

@@ -6,7 +6,7 @@ import type {
} from "../../../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { LineProbeResult } from "../../../src/plugin-sdk/line.js";
import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { withEnvAsync } from "../../../src/test-utils/env.js";
type DiscordDirectoryContractApiSurface = Pick<
@@ -33,38 +33,41 @@ type WhatsAppDirectoryContractApiSurface = Pick<
"listWhatsAppDirectoryPeersFromConfig" | "listWhatsAppDirectoryGroupsFromConfig"
>;
let discordDirectoryContractApi: DiscordDirectoryContractApiSurface | undefined;
let slackDirectoryContractApi: SlackDirectoryContractApiSurface | undefined;
let telegramDirectoryContractApi: TelegramDirectoryContractApiSurface | undefined;
let whatsappDirectoryContractApi: WhatsAppDirectoryContractApiSurface | undefined;
let discordDirectoryContractApi: Promise<DiscordDirectoryContractApiSurface> | undefined;
let slackDirectoryContractApi: Promise<SlackDirectoryContractApiSurface> | undefined;
let telegramDirectoryContractApi: Promise<TelegramDirectoryContractApiSurface> | undefined;
let whatsappDirectoryContractApi: Promise<WhatsAppDirectoryContractApiSurface> | undefined;
function loadDirectoryContractApi<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
async function importDirectoryContractApi<T extends object>(pluginId: string): Promise<T> {
const moduleId = resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId,
artifactBasename: "directory-contract-api.js",
});
return (await import(moduleId)) as T;
}
function getDiscordDirectoryContractApi(): DiscordDirectoryContractApiSurface {
function getDiscordDirectoryContractApi(): Promise<DiscordDirectoryContractApiSurface> {
discordDirectoryContractApi ??=
loadDirectoryContractApi<DiscordDirectoryContractApiSurface>("discord");
importDirectoryContractApi<DiscordDirectoryContractApiSurface>("discord");
return discordDirectoryContractApi;
}
function getSlackDirectoryContractApi(): SlackDirectoryContractApiSurface {
slackDirectoryContractApi ??= loadDirectoryContractApi<SlackDirectoryContractApiSurface>("slack");
function getSlackDirectoryContractApi(): Promise<SlackDirectoryContractApiSurface> {
slackDirectoryContractApi ??=
importDirectoryContractApi<SlackDirectoryContractApiSurface>("slack");
return slackDirectoryContractApi;
}
function getTelegramDirectoryContractApi(): TelegramDirectoryContractApiSurface {
function getTelegramDirectoryContractApi(): Promise<TelegramDirectoryContractApiSurface> {
telegramDirectoryContractApi ??=
loadDirectoryContractApi<TelegramDirectoryContractApiSurface>("telegram");
importDirectoryContractApi<TelegramDirectoryContractApiSurface>("telegram");
return telegramDirectoryContractApi;
}
function getWhatsAppDirectoryContractApi(): WhatsAppDirectoryContractApiSurface {
function getWhatsAppDirectoryContractApi(): Promise<WhatsAppDirectoryContractApiSurface> {
whatsappDirectoryContractApi ??=
loadDirectoryContractApi<WhatsAppDirectoryContractApiSurface>("whatsapp");
importDirectoryContractApi<WhatsAppDirectoryContractApiSurface>("whatsapp");
return whatsappDirectoryContractApi;
}
@@ -97,9 +100,6 @@ async function expectDirectoryIds(
export function describeDiscordPluginsCoreExtensionContract() {
describe("discord plugins-core extension contract", () => {
const listPeers = () => getDiscordDirectoryContractApi().listDiscordDirectoryPeersFromConfig;
const listGroups = () => getDiscordDirectoryContractApi().listDiscordDirectoryGroupsFromConfig;
it("DiscordProbe satisfies BaseProbeResult", () => {
expectTypeOf<DiscordProbe>().toMatchTypeOf<BaseProbeResult>();
});
@@ -109,6 +109,8 @@ export function describeDiscordPluginsCoreExtensionContract() {
});
it("lists peers/groups from config (numeric ids only)", async () => {
const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } =
await getDiscordDirectoryContractApi();
const cfg = {
channels: {
discord: {
@@ -131,17 +133,24 @@ export function describeDiscordPluginsCoreExtensionContract() {
} as unknown as OpenClawConfig;
await expectDirectoryIds(
listPeers(),
listDiscordDirectoryPeersFromConfig,
cfg,
["user:111", "user:12345", "user:222", "user:333", "user:444"],
{ sorted: true },
);
await expectDirectoryIds(listGroups(), cfg, ["channel:555", "channel:666", "channel:777"], {
sorted: true,
});
await expectDirectoryIds(
listDiscordDirectoryGroupsFromConfig,
cfg,
["channel:555", "channel:666", "channel:777"],
{
sorted: true,
},
);
});
it("keeps directories readable when tokens are unresolved SecretRefs", async () => {
const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } =
await getDiscordDirectoryContractApi();
const envSecret = {
source: "env",
provider: "default",
@@ -163,11 +172,12 @@ export function describeDiscordPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["user:111"]);
await expectDirectoryIds(listGroups(), cfg, ["channel:555"]);
await expectDirectoryIds(listDiscordDirectoryPeersFromConfig, cfg, ["user:111"]);
await expectDirectoryIds(listDiscordDirectoryGroupsFromConfig, cfg, ["channel:555"]);
});
it("applies query and limit filtering for config-backed directories", async () => {
const { listDiscordDirectoryGroupsFromConfig } = await getDiscordDirectoryContractApi();
const cfg = {
channels: {
discord: {
@@ -185,7 +195,7 @@ export function describeDiscordPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
const groups = await listGroups()({
const groups = await listDiscordDirectoryGroupsFromConfig({
cfg,
accountId: "default",
query: "666",
@@ -198,14 +208,13 @@ export function describeDiscordPluginsCoreExtensionContract() {
export function describeSlackPluginsCoreExtensionContract() {
describe("slack plugins-core extension contract", () => {
const listPeers = () => getSlackDirectoryContractApi().listSlackDirectoryPeersFromConfig;
const listGroups = () => getSlackDirectoryContractApi().listSlackDirectoryGroupsFromConfig;
it("SlackProbe satisfies BaseProbeResult", () => {
expectTypeOf<SlackProbe>().toMatchTypeOf<BaseProbeResult>();
});
it("lists peers/groups from config", async () => {
const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } =
await getSlackDirectoryContractApi();
const cfg = {
channels: {
slack: {
@@ -219,15 +228,17 @@ export function describeSlackPluginsCoreExtensionContract() {
} as unknown as OpenClawConfig;
await expectDirectoryIds(
listPeers(),
listSlackDirectoryPeersFromConfig,
cfg,
["user:u123", "user:u234", "user:u777", "user:u999"],
{ sorted: true },
);
await expectDirectoryIds(listGroups(), cfg, ["channel:c111"]);
await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]);
});
it("keeps directories readable when tokens are unresolved SecretRefs", async () => {
const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } =
await getSlackDirectoryContractApi();
const envSecret = {
source: "env",
provider: "default",
@@ -244,11 +255,12 @@ export function describeSlackPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["user:u123"]);
await expectDirectoryIds(listGroups(), cfg, ["channel:c111"]);
await expectDirectoryIds(listSlackDirectoryPeersFromConfig, cfg, ["user:u123"]);
await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]);
});
it("applies query and limit filtering for config-backed directories", async () => {
const { listSlackDirectoryPeersFromConfig } = await getSlackDirectoryContractApi();
const cfg = {
channels: {
slack: {
@@ -260,7 +272,7 @@ export function describeSlackPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
const peers = await listPeers()({
const peers = await listSlackDirectoryPeersFromConfig({
cfg,
accountId: "default",
query: "user:u",
@@ -274,10 +286,6 @@ export function describeSlackPluginsCoreExtensionContract() {
export function describeTelegramPluginsCoreExtensionContract() {
describe("telegram plugins-core extension contract", () => {
const listPeers = () => getTelegramDirectoryContractApi().listTelegramDirectoryPeersFromConfig;
const listGroups = () =>
getTelegramDirectoryContractApi().listTelegramDirectoryGroupsFromConfig;
it("TelegramProbe satisfies BaseProbeResult", () => {
expectTypeOf<TelegramProbe>().toMatchTypeOf<BaseProbeResult>();
});
@@ -287,6 +295,8 @@ export function describeTelegramPluginsCoreExtensionContract() {
});
it("lists peers/groups from config", async () => {
const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } =
await getTelegramDirectoryContractApi();
const cfg = {
channels: {
telegram: {
@@ -298,13 +308,20 @@ export function describeTelegramPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["123", "456", "@alice", "@bob"], {
sorted: true,
});
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
await expectDirectoryIds(
listTelegramDirectoryPeersFromConfig,
cfg,
["123", "456", "@alice", "@bob"],
{
sorted: true,
},
);
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
});
it("keeps fallback semantics when accountId is omitted", async () => {
const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } =
await getTelegramDirectoryContractApi();
await withEnvAsync({ TELEGRAM_BOT_TOKEN: "tok-env" }, async () => {
const cfg = {
channels: {
@@ -322,12 +339,14 @@ export function describeTelegramPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["@alice"]);
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]);
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
});
});
it("keeps directories readable when tokens are unresolved SecretRefs", async () => {
const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } =
await getTelegramDirectoryContractApi();
const envSecret = {
source: "env",
provider: "default",
@@ -343,11 +362,12 @@ export function describeTelegramPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["@alice"]);
await expectDirectoryIds(listGroups(), cfg, ["-1001"]);
await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]);
await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]);
});
it("applies query and limit filtering for config-backed directories", async () => {
const { listTelegramDirectoryGroupsFromConfig } = await getTelegramDirectoryContractApi();
const cfg = {
channels: {
telegram: {
@@ -357,7 +377,7 @@ export function describeTelegramPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
const groups = await listGroups()({
const groups = await listTelegramDirectoryGroupsFromConfig({
cfg,
accountId: "default",
query: "-100",
@@ -370,11 +390,9 @@ export function describeTelegramPluginsCoreExtensionContract() {
export function describeWhatsAppPluginsCoreExtensionContract() {
describe("whatsapp plugins-core extension contract", () => {
const listPeers = () => getWhatsAppDirectoryContractApi().listWhatsAppDirectoryPeersFromConfig;
const listGroups = () =>
getWhatsAppDirectoryContractApi().listWhatsAppDirectoryGroupsFromConfig;
it("lists peers/groups from config", async () => {
const { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig } =
await getWhatsAppDirectoryContractApi();
const cfg = {
channels: {
whatsapp: {
@@ -384,11 +402,12 @@ export function describeWhatsAppPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
await expectDirectoryIds(listPeers(), cfg, ["+15550000000"]);
await expectDirectoryIds(listGroups(), cfg, ["999@g.us"]);
await expectDirectoryIds(listWhatsAppDirectoryPeersFromConfig, cfg, ["+15550000000"]);
await expectDirectoryIds(listWhatsAppDirectoryGroupsFromConfig, cfg, ["999@g.us"]);
});
it("applies query and limit filtering for config-backed directories", async () => {
const { listWhatsAppDirectoryGroupsFromConfig } = await getWhatsAppDirectoryContractApi();
const cfg = {
channels: {
whatsapp: {
@@ -397,7 +416,7 @@ export function describeWhatsAppPluginsCoreExtensionContract() {
},
} as unknown as OpenClawConfig;
const groups = await listGroups()({
const groups = await listWhatsAppDirectoryGroupsFromConfig({
cfg,
accountId: "default",
query: "@g.us",