Improve Telegram groups config shape diagnostics (#83260)

* Improve Telegram groups config diagnostics

Add targeted guidance when channels.telegram.groups uses a non-object shape so startup/config validation and doctor explain the required group-id object map and topic nesting.

* fix(config): keep channel validation hints generic
This commit is contained in:
Gio Della-Libera
2026-05-18 18:47:28 -07:00
committed by GitHub
parent 88585da2e8
commit 19065e4a2f
4 changed files with 123 additions and 6 deletions

View File

@@ -5,12 +5,14 @@ import {
collectTelegramApiRootWarnings,
collectTelegramEmptyAllowlistExtraWarnings,
collectTelegramGroupPolicyWarnings,
collectTelegramMalformedGroupsWarnings,
collectTelegramMissingEnvTokenWarnings,
collectTelegramSelectedQuoteToolProgressWarnings,
maybeRepairTelegramApiRoots,
maybeRepairTelegramAllowFromUsernames,
scanTelegramBotEndpointApiRoots,
scanTelegramInvalidAllowFromEntries,
scanTelegramMalformedGroupsConfig,
scanTelegramSelectedQuoteToolProgressWarnings,
telegramDoctor,
} from "./doctor.js";
@@ -206,6 +208,42 @@ describe("telegram doctor", () => {
).toHaveLength(1);
});
it("warns when Telegram groups use a non-object shape", async () => {
const cfg = {
channels: {
telegram: {
groups: ["-1001234567890"],
accounts: {
work: {
groups: null,
},
},
},
},
} as unknown as OpenClawConfig;
const hits = scanTelegramMalformedGroupsConfig(cfg);
expect(hits).toEqual([
{ path: "channels.telegram.groups", actualType: "array" },
{ path: "channels.telegram.accounts.work.groups", actualType: "null" },
]);
const warnings = collectTelegramMalformedGroupsWarnings({
hits,
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings[0]).toContain("object map keyed by Telegram group/chat id");
expect(warnings[1]).toContain('channels.telegram.groups."-1001234567890".topics."99"');
expect(warnings[1]).toContain("openclaw doctor --fix");
expect(
await telegramDoctor.collectPreviewWarnings?.({
cfg,
doctorFixCommand: "openclaw doctor --fix",
}),
).toEqual(expect.arrayContaining(warnings));
});
it("repairs @username entries to numeric ids", async () => {
lookupTelegramChatIdMock.mockResolvedValue("111");

View File

@@ -26,6 +26,7 @@ import {
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
type TelegramAllowFromInvalidHit = { path: string; entry: string };
type TelegramMalformedGroupsHit = { path: string; actualType: string };
type TelegramSelectedQuoteToolProgressHit = { path: string; replyToMode: string };
type TelegramApiRootBotEndpointHit = {
path: string;
@@ -131,6 +132,53 @@ function collectTelegramAllowFromLists(
return refs;
}
function describeConfigValueType(value: unknown): string {
if (Array.isArray(value)) {
return "array";
}
if (value === null) {
return "null";
}
return typeof value;
}
export function scanTelegramMalformedGroupsConfig(
cfg: OpenClawConfig,
): TelegramMalformedGroupsHit[] {
const hits: TelegramMalformedGroupsHit[] = [];
for (const scope of collectTelegramAccountScopes(cfg)) {
if (!Object.prototype.hasOwnProperty.call(scope.account, "groups")) {
continue;
}
const groups = scope.account.groups;
if (asObjectRecord(groups)) {
continue;
}
hits.push({
path: `${scope.prefix}.groups`,
actualType: describeConfigValueType(groups),
});
}
return hits;
}
export function collectTelegramMalformedGroupsWarnings(params: {
hits: TelegramMalformedGroupsHit[];
doctorFixCommand: string;
}): string[] {
if (params.hits.length === 0) {
return [];
}
const sample = params.hits[0] ?? {
path: "channels.telegram.groups",
actualType: "unknown",
};
return [
`- ${sanitizeForLog(sample.path)} has invalid Telegram groups shape (${sanitizeForLog(sample.actualType)}); expected an object map keyed by Telegram group/chat id, not an array, string, or null.`,
`- Example shape: channels.telegram.groups."-1001234567890".topics."99" = { agentId: "support" }. Use topics for forum-topic routing, then rerun ${params.doctorFixCommand} for any remaining Telegram config cleanup.`,
];
}
export function scanTelegramInvalidAllowFromEntries(
cfg: OpenClawConfig,
): TelegramAllowFromInvalidHit[] {
@@ -557,6 +605,10 @@ export const telegramDoctor: ChannelDoctorAdapter = {
normalizeCompatibilityConfig: normalizeTelegramCompatibilityConfig,
collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [
...collectTelegramMissingEnvTokenWarnings({ cfg, env }),
...collectTelegramMalformedGroupsWarnings({
hits: scanTelegramMalformedGroupsConfig(cfg),
doctorFixCommand,
}),
...collectTelegramInvalidAllowFromWarnings({
hits: scanTelegramInvalidAllowFromEntries(cfg),
doctorFixCommand,

View File

@@ -246,6 +246,28 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => {
expect(result.ok).toBe(true);
});
it("keeps raw channel validation diagnostics plugin-agnostic", () => {
const result = validateConfigObjectRawWithPlugins({
channels: {
telegram: {
groups: ["-1001234567890"],
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.issues).toContainEqual(
expect.objectContaining({
path: "channels.telegram.groups",
message: expect.stringContaining("invalid config:"),
}),
);
expect(result.issues[0]?.message).not.toContain("Telegram groups");
expect(result.issues[0]?.message).not.toContain("openclaw doctor --fix");
}
});
});
describe("validateConfigObjectRawWithPlugins plugin config defaults", () => {

View File

@@ -231,6 +231,9 @@ function collectAllowedValuesFromBundledChannelSchemaPath(
return collectAllowedValuesFromJsonSchemaNode(targetNode);
}
function formatRawChannelConfigIssueMessage(message: string): string {
return `invalid config: ${message}`;
}
function collectRawBundledChannelConfigIssues(config: OpenClawConfig): ConfigValidationIssue[] {
if (!config.channels || !isRecord(config.channels)) {
return [];
@@ -253,10 +256,11 @@ function collectRawBundledChannelConfigIssues(config: OpenClawConfig): ConfigVal
const message = error.additionalProperty
? `${error.message}: "${error.additionalProperty}"`
: error.message;
const path =
error.path === "<root>" ? `channels.${channelId}` : `channels.${channelId}.${error.path}`;
issues.push({
path:
error.path === "<root>" ? `channels.${channelId}` : `channels.${channelId}.${error.path}`,
message: `invalid config: ${message}`,
path,
message: formatRawChannelConfigIssueMessage(message),
allowedValues: error.allowedValues,
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
});
@@ -1401,10 +1405,11 @@ function validateConfigObjectWithPluginsBase(
});
if (!result.ok) {
for (const error of result.errors) {
const path =
error.path === "<root>" ? `channels.${trimmed}` : `channels.${trimmed}.${error.path}`;
issues.push({
path:
error.path === "<root>" ? `channels.${trimmed}` : `channels.${trimmed}.${error.path}`,
message: `invalid config: ${error.message}`,
path,
message: formatRawChannelConfigIssueMessage(error.message),
allowedValues: error.allowedValues,
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
});