mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:30:43 +00:00
test: speed channel contract hotspots
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"string-coerce-runtime",
|
||||
"group-access",
|
||||
"global-singleton",
|
||||
"directory-config-runtime",
|
||||
"directory-runtime",
|
||||
"googlechat",
|
||||
"googlechat-runtime-shared",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
22
src/plugin-sdk/directory-config-runtime.ts
Normal file
22
src/plugin-sdk/directory-config-runtime.ts
Normal 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";
|
||||
@@ -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(".")),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user