diff --git a/src/lib/actions/sandbox/channel-status-config-core.test.ts b/src/lib/actions/sandbox/channel-status-config-core.test.ts index d98ec5ddbd6..fcbcbaade13 100644 --- a/src/lib/actions/sandbox/channel-status-config-core.test.ts +++ b/src/lib/actions/sandbox/channel-status-config-core.test.ts @@ -16,7 +16,12 @@ describe("showSandboxChannelStatus config comparison", () => { telegram: { accounts: { default: { - groupPolicy: "allowlist", + groupPolicy: "open", + }, + }, + groups: { + "*": { + requireMention: true, }, }, }, @@ -51,7 +56,7 @@ describe("showSandboxChannelStatus config comparison", () => { required: false, sourceEnv: "TELEGRAM_GROUP_POLICY", statePath: "telegramConfig.groupPolicy", - value: "allowlist", + value: "open", }, ], }), @@ -68,19 +73,80 @@ describe("showSandboxChannelStatus config comparison", () => { signals.find((signal) => signal.label === "Telegram group policy (TELEGRAM_GROUP_POLICY)"), ).toMatchObject({ severity: "ok", - detail: "allowlist", + detail: "open", }); expect( signals.find( (signal) => signal.label === "Telegram group mention mode (TELEGRAM_REQUIRE_MENTION)", ), - ).toBeUndefined(); + ).toMatchObject({ + severity: "ok", + detail: "yes", + }); const dump = out_lines.join("\n"); - expect(dump).toMatch(/Telegram group policy \(TELEGRAM_GROUP_POLICY\):\s+allowlist/); + expect(dump).toMatch(/Telegram group policy \(TELEGRAM_GROUP_POLICY\):\s+open/); + expect(dump).toMatch(/Telegram group mention mode \(TELEGRAM_REQUIRE_MENTION\):\s+yes/); expect(dump).not.toMatch(/Telegram Bot Token/); expect(dump).not.toMatch(/TELEGRAM_BOT_TOKEN/); }); + it("marks Telegram all-message mode ok when OpenClaw omits the groups stanza (#5691)", async () => { + const { deps } = makeDeps({ + exec: () => ({ + status: 0, + stdout: JSON.stringify({ + channels: { + telegram: { + accounts: { + default: { + groupPolicy: "open", + }, + }, + }, + }, + }), + stderr: "", + }), + sandbox: entry(["telegram"], [], { + telegram: [ + { + channelId: "telegram", + inputId: "requireMention", + kind: "config", + required: false, + sourceEnv: "TELEGRAM_REQUIRE_MENTION", + statePath: "telegramConfig.requireMention", + value: "0", + }, + { + channelId: "telegram", + inputId: "groupPolicy", + kind: "config", + required: false, + sourceEnv: "TELEGRAM_GROUP_POLICY", + statePath: "telegramConfig.groupPolicy", + value: "open", + }, + ], + }), + appliedPresets: ["telegram"], + }); + const result = await showSandboxChannelStatus("alpha", { + deps, + channel: "telegram", + }); + + const signals = result && "signals" in result ? result.signals : []; + expect( + signals.find( + (signal) => signal.label === "Telegram group mention mode (TELEGRAM_REQUIRE_MENTION)", + ), + ).toMatchObject({ + severity: "ok", + detail: "no", + }); + }); + it("does not compare Hermes Telegram group policy when the manifest does not render it", async () => { const { deps } = makeDeps({ exec: (_sandbox, command) => @@ -160,7 +226,7 @@ describe("showSandboxChannelStatus config comparison", () => { ), ).toMatchObject({ severity: "ok", - detail: "1", + detail: "yes", }); expect( signals.find((signal) => signal.label === "Telegram group policy (TELEGRAM_GROUP_POLICY)"), diff --git a/src/lib/actions/sandbox/channel-status-config.ts b/src/lib/actions/sandbox/channel-status-config.ts index 15b129ba1e1..779f8c74a1a 100644 --- a/src/lib/actions/sandbox/channel-status-config.ts +++ b/src/lib/actions/sandbox/channel-status-config.ts @@ -24,7 +24,11 @@ import type { } from "../../messaging/manifest"; import type { DiagnosticSignal } from "../../sandbox/whatsapp-diagnostics"; import * as registry from "../../state/registry"; -import { configInputDetail, configValuesEqual } from "./channel-status-config-values"; +import { + booleanConfigValue, + configInputDetail, + configValuesEqual, +} from "./channel-status-config-values"; const CONFIG_STATUS_TIMEOUT_MS = 5_000; const CONFIG_STATUS_MAX_SOURCE_BYTES = 64 * 1024; @@ -107,7 +111,7 @@ function configInputSignal( } const comparisons = sources.map((source) => - compareConfigSource(expected, source, sourceReads.sourceValues), + compareConfigSource(input, expected, source, sourceReads.sourceValues), ); const checkedComparisons = comparisons.filter((comparison) => comparison.checked); const hasMismatch = checkedComparisons.some((comparison) => !comparison.matches); @@ -159,7 +163,7 @@ function expectedConfigValue( if (planInputHasValue(planInput)) { return { value: planInput.value, - detail: configInputDetail(planInput.value), + detail: configInputDisplayDetail(input, planInput.value), hasValue: true, }; } @@ -168,18 +172,29 @@ function expectedConfigValue( if (defaultValue) { return { value: defaultValue, - detail: `${configInputDetail(defaultValue)} (default)`, + detail: `${configInputDisplayDetail(input, defaultValue)} (default)`, hasValue: true, }; } return { value: undefined, - detail: configInputDetail(undefined), + detail: configInputDisplayDetail(input, undefined), hasValue: false, }; } +function configInputDisplayDetail( + input: ChannelConfigInputSpec, + value: MessagingSerializableValue | undefined, +): string { + if (input.id === "requireMention" && input.envKey === "TELEGRAM_REQUIRE_MENTION") { + const booleanValue = value === undefined || value === null ? null : booleanConfigValue(value); + if (booleanValue !== null) return booleanValue ? "yes" : "no"; + } + return configInputDetail(value); +} + interface ConfigRenderSource extends RenderedConfigVisibilityKey { readonly resolvedTarget: string; } @@ -372,6 +387,7 @@ function parseRenderedConfigSource( } function compareConfigSource( + input: ChannelConfigInputSpec, expected: ExpectedConfigValue, source: ConfigRenderSource, sourceValues: ReadonlyMap, @@ -397,7 +413,7 @@ function compareConfigSource( matches, detail: matches ? expected.detail - : `expected ${expected.detail}; rendered ${configInputDetail(actual.value)}`, + : `expected ${expected.detail}; rendered ${configInputDisplayDetail(input, actual.value)}`, }; } diff --git a/src/lib/actions/sandbox/channel-status-telegram-policy.test.ts b/src/lib/actions/sandbox/channel-status-telegram-policy.test.ts index 1fad223e077..a4ee248837f 100644 --- a/src/lib/actions/sandbox/channel-status-telegram-policy.test.ts +++ b/src/lib/actions/sandbox/channel-status-telegram-policy.test.ts @@ -17,6 +17,11 @@ describe("showSandboxChannelStatus Telegram group policy", () => { groupPolicy: "open", }, }, + groups: { + "*": { + requireMention: true, + }, + }, }, }, }), @@ -41,10 +46,16 @@ describe("showSandboxChannelStatus Telegram group policy", () => { signals.find( (signal) => signal.label === "Telegram group mention mode (TELEGRAM_REQUIRE_MENTION)", ), - ).toBeUndefined(); + ).toMatchObject({ + severity: "ok", + detail: "yes (default)", + }); const dump = out_lines.join("\n"); expect(dump).toMatch(/Telegram User ID \(for DM access\) \(TELEGRAM_ALLOWED_IDS\):\s+not set/); expect(dump).toMatch(/Telegram group policy \(TELEGRAM_GROUP_POLICY\):\s+open \(default\)/); + expect(dump).toMatch( + /Telegram group mention mode \(TELEGRAM_REQUIRE_MENTION\):\s+yes \(default\)/, + ); }); it("accepts Telegram disabled group policy from rendered config", async () => { diff --git a/src/lib/messaging/channels/telegram/rendered-config-parser.test.ts b/src/lib/messaging/channels/telegram/rendered-config-parser.test.ts new file mode 100644 index 00000000000..ec3e762d203 --- /dev/null +++ b/src/lib/messaging/channels/telegram/rendered-config-parser.test.ts @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { telegramManifest } from "./manifest"; +import { telegramRenderedConfigParser } from "./rendered-config-parser"; + +describe("telegram rendered config parser", () => { + const openClawContext = { + agentId: "openclaw" as const, + manifest: telegramManifest, + inputs: [], + }; + + it("extracts OpenClaw wildcard group mention mode (#5691)", () => { + const requireMentionKey = telegramRenderedConfigParser + .listConfigVisibilityKeys(openClawContext) + .find((key) => key.key === "openclawGroupRequireMention"); + + expect(requireMentionKey).toBeDefined(); + expect( + telegramRenderedConfigParser.getValue(requireMentionKey!, { + kind: "structured", + value: { + channels: { + telegram: { + accounts: { + default: { + groupPolicy: "open", + }, + }, + groups: { + "*": { + requireMention: true, + }, + }, + }, + }, + }, + }), + ).toBe(true); + }); + + it("treats missing OpenClaw groups as all-message mode when group policy is open (#5691)", () => { + const requireMentionKey = telegramRenderedConfigParser + .listConfigVisibilityKeys(openClawContext) + .find((key) => key.key === "openclawGroupRequireMention"); + + expect(requireMentionKey).toBeDefined(); + expect( + telegramRenderedConfigParser.getValue(requireMentionKey!, { + kind: "structured", + value: { + channels: { + telegram: { + accounts: { + default: { + groupPolicy: "open", + }, + }, + }, + }, + }, + }), + ).toBe(false); + }); + + it("treats missing OpenClaw group policy as unknown mention mode (#5691)", () => { + const requireMentionKey = telegramRenderedConfigParser + .listConfigVisibilityKeys(openClawContext) + .find((key) => key.key === "openclawGroupRequireMention"); + + expect(requireMentionKey).toBeDefined(); + expect( + telegramRenderedConfigParser.getValue(requireMentionKey!, { + kind: "structured", + value: { + channels: { + telegram: { + accounts: { + default: {}, + }, + }, + }, + }, + }), + ).toBeUndefined(); + }); + + it("does not expose OpenClaw mention mode when group policy is not open", () => { + const keys = telegramRenderedConfigParser.listConfigVisibilityKeys({ + ...openClawContext, + inputs: [ + { + channelId: "telegram", + inputId: "groupPolicy", + kind: "config", + required: false, + statePath: "telegramConfig.groupPolicy", + value: "allowlist", + }, + ], + }); + + expect(keys.find((key) => key.key === "openclawGroupRequireMention")).toBeUndefined(); + }); +}); diff --git a/src/lib/messaging/channels/telegram/rendered-config-parser.ts b/src/lib/messaging/channels/telegram/rendered-config-parser.ts index 3013aa1313a..c3490d49267 100644 --- a/src/lib/messaging/channels/telegram/rendered-config-parser.ts +++ b/src/lib/messaging/channels/telegram/rendered-config-parser.ts @@ -5,14 +5,22 @@ import { envConfigKey, getEnvConfigValue, getStructuredConfigValue, + getStructuredPath, + type RenderedChannelConfigParserContext, + type RenderedConfigSource, + type RenderedConfigVisibilityKey, type RenderedChannelConfigParser, structuredConfigKey, } from "../rendered-config-parser-utils"; +const OPENCLAW_ACCOUNT_PATH = ["channels", "telegram", "accounts", "default"] as const; +const OPENCLAW_GROUPS_PATH = ["channels", "telegram", "groups"] as const; +const DEFAULT_OPENCLAW_GROUP_POLICY = "open"; + export const telegramRenderedConfigParser: RenderedChannelConfigParser = { listConfigVisibilityKeys(context) { if (context.agentId === "openclaw") { - return [ + const keys = [ structuredConfigKey("allowedIds", "openclaw.json", [ "channels", "telegram", @@ -28,6 +36,17 @@ export const telegramRenderedConfigParser: RenderedChannelConfigParser = { "groupPolicy", ]), ]; + if (openClawGroupPolicyFromInputs(context) === "open") { + keys.push( + structuredConfigKey( + "requireMention", + "openclaw.json", + OPENCLAW_GROUPS_PATH, + "openclawGroupRequireMention", + ), + ); + } + return keys; } if (context.agentId === "hermes") { return [ @@ -42,8 +61,46 @@ export const telegramRenderedConfigParser: RenderedChannelConfigParser = { }, getValue(key, source) { + if (key.key === "openclawGroupRequireMention") { + return getOpenClawGroupRequireMention(key, source); + } return key.kind === "env" ? getEnvConfigValue(source, key.envKey) : getStructuredConfigValue(source, key.path); }, }; + +function openClawGroupPolicyFromInputs(context: RenderedChannelConfigParserContext): string { + const inputValue = context.inputs.find((input) => input.inputId === "groupPolicy")?.value; + if (typeof inputValue === "string" && inputValue.trim()) return inputValue.trim(); + const defaultValue = context.manifest.inputs.find((input) => input.id === "groupPolicy"); + return defaultValue?.kind === "config" && defaultValue.defaultValue + ? defaultValue.defaultValue + : DEFAULT_OPENCLAW_GROUP_POLICY; +} + +function getOpenClawGroupRequireMention( + key: RenderedConfigVisibilityKey, + source: RenderedConfigSource, +): boolean | boolean[] | undefined { + const accountGroupPolicy = + source.kind === "structured" + ? getStructuredPath(source.value, [...OPENCLAW_ACCOUNT_PATH, "groupPolicy"]) + : undefined; + if (accountGroupPolicy !== "open") { + return undefined; + } + + const groups = getStructuredConfigValue(source, key.path); + if (!groups || typeof groups !== "object" || Array.isArray(groups)) return false; + + const values = Object.values(groups) + .map((group) => + group && typeof group === "object" && !Array.isArray(group) + ? getStructuredPath(group, ["requireMention"]) + : undefined, + ) + .filter((value): value is boolean => typeof value === "boolean"); + if (values.length === 0) return false; + return [...new Set(values)].sort().length === 1 ? values[0] : [...new Set(values)].sort(); +}