test: slim channel directory contracts

This commit is contained in:
Peter Steinberger
2026-04-17 23:55:06 +01:00
parent 7db9a53254
commit ed65e8017d
8 changed files with 126 additions and 79 deletions

View File

@@ -0,0 +1,4 @@
export {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
} from "./src/directory-config.js";

View File

@@ -0,0 +1,4 @@
export {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "./src/directory-config.js";

View File

@@ -0,0 +1,4 @@
export {
listTelegramDirectoryGroupsFromConfig,
listTelegramDirectoryPeersFromConfig,
} from "./src/directory-config.js";

View File

@@ -0,0 +1,37 @@
import {
normalizeAccountId,
resolveAccountEntry,
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-core";
import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime";
export function resolveTelegramAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): TelegramAccountConfig | undefined {
const normalized = normalizeAccountId(accountId);
return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized);
}
export function mergeTelegramAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): TelegramAccountConfig {
const {
accounts: _ignored,
defaultAccount: _ignoredDefaultAccount,
groups: channelGroups,
...base
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & {
accounts?: unknown;
defaultAccount?: unknown;
};
const account = resolveTelegramAccountConfig(cfg, accountId) ?? {};
// Multi-account bots must not inherit channel-level groups unless explicitly set.
const configuredAccountIds = Object.keys(cfg.channels?.telegram?.accounts ?? {});
const isMultiAccount = configuredAccountIds.length > 1;
const groups = account.groups ?? (isMultiAccount ? undefined : channelGroups);
return { ...base, ...account, groups };
}

View File

@@ -3,7 +3,6 @@ import {
createAccountActionGate,
normalizeAccountId,
normalizeOptionalAccountId,
resolveAccountEntry,
resolveAccountWithDefaultFallback,
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-core";
@@ -14,6 +13,7 @@ import type {
import { formatSetExplicitDefaultInstruction } from "openclaw/plugin-sdk/routing";
import { createSubsystemLogger, isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { mergeTelegramAccountConfig, resolveTelegramAccountConfig } from "./account-config.js";
import {
listTelegramAccountIds as listSelectedTelegramAccountIds,
resolveDefaultTelegramAccountSelection,
@@ -21,6 +21,8 @@ import {
import type { TelegramTransport } from "./fetch.js";
import { resolveTelegramToken } from "./token.js";
export { mergeTelegramAccountConfig, resolveTelegramAccountConfig } from "./account-config.js";
let log: ReturnType<typeof createSubsystemLogger> | null = null;
function getLog() {
@@ -89,43 +91,6 @@ export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string {
return selection.accountId;
}
export function resolveTelegramAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): TelegramAccountConfig | undefined {
const normalized = normalizeAccountId(accountId);
return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized);
}
export function mergeTelegramAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): TelegramAccountConfig {
const {
accounts: _ignored,
defaultAccount: _ignoredDefaultAccount,
groups: channelGroups,
...base
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & {
accounts?: unknown;
defaultAccount?: unknown;
};
const account = resolveTelegramAccountConfig(cfg, accountId) ?? {};
// In multi-account setups, channel-level `groups` must NOT be inherited by
// accounts that don't have their own `groups` config. A bot that is not a
// member of a configured group will fail when handling group messages, and
// this failure disrupts message delivery for *all* accounts.
// Single-account setups keep backward compat: channel-level groups still
// applies when the account has no override.
// See: https://github.com/openclaw/openclaw/issues/30673
const configuredAccountIds = Object.keys(cfg.channels?.telegram?.accounts ?? {});
const isMultiAccount = configuredAccountIds.length > 1;
const groups = account.groups ?? (isMultiAccount ? undefined : channelGroups);
return { ...base, ...account, groups };
}
export function createTelegramActionGate(params: {
cfg: OpenClawConfig;
accountId?: string | null;

View File

@@ -1,12 +1,30 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-core";
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import { createInspectedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
import { inspectTelegramAccount, type InspectedTelegramAccount } from "./account-inspect.js";
import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime";
import { createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
import { mergeTelegramAccountConfig } from "./account-config.js";
import { resolveDefaultTelegramAccountSelection } from "./account-selection.js";
type TelegramDirectoryAccount = {
config: TelegramAccountConfig;
};
function resolveTelegramDirectoryAccount(
cfg: OpenClawConfig,
accountId?: string | null,
): TelegramDirectoryAccount {
const resolvedAccountId = accountId?.trim()
? normalizeAccountId(accountId)
: resolveDefaultTelegramAccountSelection(cfg).accountId;
return {
config: mergeTelegramAccountConfig(cfg, resolvedAccountId),
};
}
export const listTelegramDirectoryPeersFromConfig =
createInspectedDirectoryEntriesLister<InspectedTelegramAccount>({
createResolvedDirectoryEntriesLister<TelegramDirectoryAccount>({
kind: "user",
inspectAccount: (cfg, accountId) =>
inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null,
resolveAccount: (cfg, accountId) => resolveTelegramDirectoryAccount(cfg, accountId),
resolveSources: (account) => [
mapAllowFromEntries(account.config.allowFrom),
Object.keys(account.config.dms ?? {}),
@@ -24,10 +42,9 @@ export const listTelegramDirectoryPeersFromConfig =
});
export const listTelegramDirectoryGroupsFromConfig =
createInspectedDirectoryEntriesLister<InspectedTelegramAccount>({
createResolvedDirectoryEntriesLister<TelegramDirectoryAccount>({
kind: "group",
inspectAccount: (cfg, accountId) =>
inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null,
resolveAccount: (cfg, accountId) => resolveTelegramDirectoryAccount(cfg, accountId),
resolveSources: (account) => [Object.keys(account.config.groups ?? {})],
normalizeId: (entry) => entry.trim() || null,
});

View File

@@ -0,0 +1,4 @@
export {
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
} from "./src/directory-config.js";

View File

@@ -6,56 +6,66 @@ 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 { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { withEnvAsync } from "../../../src/test-utils/env.js";
type DiscordContractApiSurface = Pick<
typeof import("@openclaw/discord/contract-api.js"),
type DiscordDirectoryContractApiSurface = Pick<
typeof import("@openclaw/discord/directory-contract-api.js"),
"listDiscordDirectoryPeersFromConfig" | "listDiscordDirectoryGroupsFromConfig"
>;
type DiscordProbe = import("@openclaw/discord/api.js").DiscordProbe;
type DiscordTokenResolution = import("@openclaw/discord/api.js").DiscordTokenResolution;
type IMessageProbe = import("@openclaw/imessage/runtime-api.js").IMessageProbe;
type SignalProbe = import("@openclaw/signal/api.js").SignalProbe;
type SlackContractApiSurface = Pick<
typeof import("@openclaw/slack/contract-api.js"),
type SlackDirectoryContractApiSurface = Pick<
typeof import("@openclaw/slack/directory-contract-api.js"),
"listSlackDirectoryPeersFromConfig" | "listSlackDirectoryGroupsFromConfig"
>;
type SlackProbe = import("@openclaw/slack/api.js").SlackProbe;
type TelegramContractApiSurface = Pick<
typeof import("@openclaw/telegram/contract-api.js"),
type TelegramDirectoryContractApiSurface = Pick<
typeof import("@openclaw/telegram/directory-contract-api.js"),
"listTelegramDirectoryPeersFromConfig" | "listTelegramDirectoryGroupsFromConfig"
>;
type TelegramProbe = import("@openclaw/telegram/api.js").TelegramProbe;
type TelegramTokenResolution = import("@openclaw/telegram/api.js").TelegramTokenResolution;
type WhatsAppContractApiSurface = Pick<
typeof import("@openclaw/whatsapp/contract-api.js"),
type WhatsAppDirectoryContractApiSurface = Pick<
typeof import("@openclaw/whatsapp/directory-contract-api.js"),
"listWhatsAppDirectoryPeersFromConfig" | "listWhatsAppDirectoryGroupsFromConfig"
>;
let discordContractApi: DiscordContractApiSurface | undefined;
let slackContractApi: SlackContractApiSurface | undefined;
let telegramContractApi: TelegramContractApiSurface | undefined;
let whatsappContractApi: WhatsAppContractApiSurface | undefined;
let discordDirectoryContractApi: DiscordDirectoryContractApiSurface | undefined;
let slackDirectoryContractApi: SlackDirectoryContractApiSurface | undefined;
let telegramDirectoryContractApi: TelegramDirectoryContractApiSurface | undefined;
let whatsappDirectoryContractApi: WhatsAppDirectoryContractApiSurface | undefined;
function getDiscordContractApi(): DiscordContractApiSurface {
discordContractApi ??= loadBundledPluginContractApiSync<DiscordContractApiSurface>("discord");
return discordContractApi;
function loadDirectoryContractApi<T extends object>(pluginId: string): T {
return loadBundledPluginPublicSurfaceSync<T>({
pluginId,
artifactBasename: "directory-contract-api.js",
});
}
function getSlackContractApi(): SlackContractApiSurface {
slackContractApi ??= loadBundledPluginContractApiSync<SlackContractApiSurface>("slack");
return slackContractApi;
function getDiscordDirectoryContractApi(): DiscordDirectoryContractApiSurface {
discordDirectoryContractApi ??=
loadDirectoryContractApi<DiscordDirectoryContractApiSurface>("discord");
return discordDirectoryContractApi;
}
function getTelegramContractApi(): TelegramContractApiSurface {
telegramContractApi ??= loadBundledPluginContractApiSync<TelegramContractApiSurface>("telegram");
return telegramContractApi;
function getSlackDirectoryContractApi(): SlackDirectoryContractApiSurface {
slackDirectoryContractApi ??= loadDirectoryContractApi<SlackDirectoryContractApiSurface>("slack");
return slackDirectoryContractApi;
}
function getWhatsAppContractApi(): WhatsAppContractApiSurface {
whatsappContractApi ??= loadBundledPluginContractApiSync<WhatsAppContractApiSurface>("whatsapp");
return whatsappContractApi;
function getTelegramDirectoryContractApi(): TelegramDirectoryContractApiSurface {
telegramDirectoryContractApi ??=
loadDirectoryContractApi<TelegramDirectoryContractApiSurface>("telegram");
return telegramDirectoryContractApi;
}
function getWhatsAppDirectoryContractApi(): WhatsAppDirectoryContractApiSurface {
whatsappDirectoryContractApi ??=
loadDirectoryContractApi<WhatsAppDirectoryContractApiSurface>("whatsapp");
return whatsappDirectoryContractApi;
}
type DirectoryListFn = (params: {
@@ -87,8 +97,8 @@ async function expectDirectoryIds(
export function describeDiscordPluginsCoreExtensionContract() {
describe("discord plugins-core extension contract", () => {
const listPeers = () => getDiscordContractApi().listDiscordDirectoryPeersFromConfig;
const listGroups = () => getDiscordContractApi().listDiscordDirectoryGroupsFromConfig;
const listPeers = () => getDiscordDirectoryContractApi().listDiscordDirectoryPeersFromConfig;
const listGroups = () => getDiscordDirectoryContractApi().listDiscordDirectoryGroupsFromConfig;
it("DiscordProbe satisfies BaseProbeResult", () => {
expectTypeOf<DiscordProbe>().toMatchTypeOf<BaseProbeResult>();
@@ -188,8 +198,8 @@ export function describeDiscordPluginsCoreExtensionContract() {
export function describeSlackPluginsCoreExtensionContract() {
describe("slack plugins-core extension contract", () => {
const listPeers = () => getSlackContractApi().listSlackDirectoryPeersFromConfig;
const listGroups = () => getSlackContractApi().listSlackDirectoryGroupsFromConfig;
const listPeers = () => getSlackDirectoryContractApi().listSlackDirectoryPeersFromConfig;
const listGroups = () => getSlackDirectoryContractApi().listSlackDirectoryGroupsFromConfig;
it("SlackProbe satisfies BaseProbeResult", () => {
expectTypeOf<SlackProbe>().toMatchTypeOf<BaseProbeResult>();
@@ -264,8 +274,9 @@ export function describeSlackPluginsCoreExtensionContract() {
export function describeTelegramPluginsCoreExtensionContract() {
describe("telegram plugins-core extension contract", () => {
const listPeers = () => getTelegramContractApi().listTelegramDirectoryPeersFromConfig;
const listGroups = () => getTelegramContractApi().listTelegramDirectoryGroupsFromConfig;
const listPeers = () => getTelegramDirectoryContractApi().listTelegramDirectoryPeersFromConfig;
const listGroups = () =>
getTelegramDirectoryContractApi().listTelegramDirectoryGroupsFromConfig;
it("TelegramProbe satisfies BaseProbeResult", () => {
expectTypeOf<TelegramProbe>().toMatchTypeOf<BaseProbeResult>();
@@ -359,8 +370,9 @@ export function describeTelegramPluginsCoreExtensionContract() {
export function describeWhatsAppPluginsCoreExtensionContract() {
describe("whatsapp plugins-core extension contract", () => {
const listPeers = () => getWhatsAppContractApi().listWhatsAppDirectoryPeersFromConfig;
const listGroups = () => getWhatsAppContractApi().listWhatsAppDirectoryGroupsFromConfig;
const listPeers = () => getWhatsAppDirectoryContractApi().listWhatsAppDirectoryPeersFromConfig;
const listGroups = () =>
getWhatsAppDirectoryContractApi().listWhatsAppDirectoryGroupsFromConfig;
it("lists peers/groups from config", async () => {
const cfg = {