diff --git a/extensions/discord/src/directory-config.ts b/extensions/discord/src/directory-config.ts index 369e55b263e..1158b3bc1e4 100644 --- a/extensions/discord/src/directory-config.ts +++ b/extensions/discord/src/directory-config.ts @@ -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( diff --git a/extensions/slack/src/directory-config.ts b/extensions/slack/src/directory-config.ts index d1002befcb6..44d7acce97a 100644 --- a/extensions/slack/src/directory-config.ts +++ b/extensions/slack/src/directory-config.ts @@ -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"; diff --git a/extensions/telegram/src/directory-config.ts b/extensions/telegram/src/directory-config.ts index eee8a3fb264..077be0bb1e5 100644 --- a/extensions/telegram/src/directory-config.ts +++ b/extensions/telegram/src/directory-config.ts @@ -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"; diff --git a/extensions/whatsapp/src/directory-config.ts b/extensions/whatsapp/src/directory-config.ts index 86939e7901c..16faf276f48 100644 --- a/extensions/whatsapp/src/directory-config.ts +++ b/extensions/whatsapp/src/directory-config.ts @@ -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({ + return listResolvedDirectoryUserEntriesFromAllowFrom({ ...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({ + return listResolvedDirectoryGroupEntriesFromMapKeys({ ...params, - resolveAccount: adaptScopedAccountAccessor(resolveWhatsAppAccount), + resolveAccount: resolveWhatsAppDirectoryAccount, resolveGroups: (account) => account.groups, }); } diff --git a/package.json b/package.json index 9ccd78766cd..c6ef54673e0 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index a950bf1ff91..fc22e141391 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -165,6 +165,7 @@ "string-coerce-runtime", "group-access", "global-singleton", + "directory-config-runtime", "directory-runtime", "googlechat", "googlechat-runtime-shared", diff --git a/src/channels/plugins/contracts/dm-policy.contract.test.ts b/src/channels/plugins/contracts/dm-policy.contract.test.ts index a9b6466286f..bc633c9650b 100644 --- a/src/channels/plugins/contracts/dm-policy.contract.test.ts +++ b/src/channels/plugins/contracts/dm-policy.contract.test.ts @@ -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, }); - }, - ); + } + }); }); diff --git a/src/plugin-sdk/directory-config-runtime.ts b/src/plugin-sdk/directory-config-runtime.ts new file mode 100644 index 00000000000..5b6ecf2598a --- /dev/null +++ b/src/plugin-sdk/directory-config-runtime.ts @@ -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"; diff --git a/src/plugins/contracts/plugin-entry-guardrails.test.ts b/src/plugins/contracts/plugin-entry-guardrails.test.ts index 9fe86947bba..7dfd455156c 100644 --- a/src/plugins/contracts/plugin-entry-guardrails.test.ts +++ b/src/plugins/contracts/plugin-entry-guardrails.test.ts @@ -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(); - 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(); + 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(); 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(".")), diff --git a/test/helpers/channels/dm-policy-contract.ts b/test/helpers/channels/dm-policy-contract.ts index 2f080e2e32d..5f34da9ea40 100644 --- a/test/helpers/channels/dm-policy-contract.ts +++ b/test/helpers/channels/dm-policy-contract.ts @@ -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 | undefined; -function getSignalContractSurface(): SignalContractApiSurface { - signalContractSurface ??= loadBundledPluginContractApiSync("signal"); +export function getSignalContractSurface(): Promise { + signalContractSurface ??= import( + resolveRelativeBundledPluginPublicModuleId({ + fromModuleUrl: import.meta.url, + pluginId: "signal", + artifactBasename: "contract-api.js", + }) + ) as Promise; return signalContractSurface; } - -export const isSignalSenderAllowed = ( - ...args: Parameters -) => getSignalContractSurface().isSignalSenderAllowed(...args); export type { SignalSender }; diff --git a/test/helpers/channels/plugins-core-extension-contract.ts b/test/helpers/channels/plugins-core-extension-contract.ts index 1be328de840..99a18bb4f83 100644 --- a/test/helpers/channels/plugins-core-extension-contract.ts +++ b/test/helpers/channels/plugins-core-extension-contract.ts @@ -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 | undefined; +let slackDirectoryContractApi: Promise | undefined; +let telegramDirectoryContractApi: Promise | undefined; +let whatsappDirectoryContractApi: Promise | undefined; -function loadDirectoryContractApi(pluginId: string): T { - return loadBundledPluginPublicSurfaceSync({ +async function importDirectoryContractApi(pluginId: string): Promise { + 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 { discordDirectoryContractApi ??= - loadDirectoryContractApi("discord"); + importDirectoryContractApi("discord"); return discordDirectoryContractApi; } -function getSlackDirectoryContractApi(): SlackDirectoryContractApiSurface { - slackDirectoryContractApi ??= loadDirectoryContractApi("slack"); +function getSlackDirectoryContractApi(): Promise { + slackDirectoryContractApi ??= + importDirectoryContractApi("slack"); return slackDirectoryContractApi; } -function getTelegramDirectoryContractApi(): TelegramDirectoryContractApiSurface { +function getTelegramDirectoryContractApi(): Promise { telegramDirectoryContractApi ??= - loadDirectoryContractApi("telegram"); + importDirectoryContractApi("telegram"); return telegramDirectoryContractApi; } -function getWhatsAppDirectoryContractApi(): WhatsAppDirectoryContractApiSurface { +function getWhatsAppDirectoryContractApi(): Promise { whatsappDirectoryContractApi ??= - loadDirectoryContractApi("whatsapp"); + importDirectoryContractApi("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().toMatchTypeOf(); }); @@ -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().toMatchTypeOf(); }); 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().toMatchTypeOf(); }); @@ -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",