fix(config): migrate removed telegram groupMentionsOnly key (#55336)

Merged via squash.

Prepared head SHA: 23731e27bf
Co-authored-by: jameslcowan <112015792+jameslcowan@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
James L. Cowan Jr. 2026-03-31 03:11:44 -03:00 committed by GitHub
parent 8dfbcaa200
commit 3bed73dc36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 185 additions and 0 deletions

View File

@ -130,6 +130,7 @@ Docs: https://docs.openclaw.ai
- TTS: Restore 3.28 schema compatibility and fallback observability. (#57953) Thanks @joshavant.
- Telegram/forum topics: restore reply routing to the active topic and keep ACP `sessions_spawn(..., thread=true, mode="session")` bound to that same topic instead of falling back to root chat or losing follow-up routing. (#56060) Thanks @one27001.
- Config/SecretRef + Control UI: harden SecretRef redaction round-trip restore, block unsafe raw fallback (force Form mode when raw is unavailable), and preflight submitted-config SecretRefs before config write RPC persistence. (#58044) Thanks @joshavant.
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.
## 2026.3.28

View File

@ -214,6 +214,25 @@ describe("legacy config detection", () => {
expect(res.changes).toEqual([]);
expect(res.config).toBeNull();
});
it("flags channels.telegram.groupMentionsOnly as legacy in snapshot", async () => {
await withSnapshotForConfig(
{ channels: { telegram: { groupMentionsOnly: true } } },
async (ctx) => {
expect(ctx.snapshot.valid).toBe(true);
expect(
ctx.snapshot.legacyIssues.some(
(issue) => issue.path === "channels.telegram.groupMentionsOnly",
),
).toBe(true);
const parsed = ctx.parsed as {
channels?: { telegram?: { groupMentionsOnly?: boolean } };
};
expect(parsed.channels?.telegram?.groupMentionsOnly).toBe(true);
},
);
});
it("does not rewrite removed messages.tts.enabled migrations", async () => {
const res = migrateLegacyConfig({
messages: { tts: { enabled: true } },

View File

@ -242,6 +242,17 @@ describe("legacy config detection", () => {
expect(res.issues[0]?.message).toContain('"telegram"');
}
});
it("rejects channels.telegram.groupMentionsOnly", async () => {
const res = validateConfigObject({
channels: { telegram: { groupMentionsOnly: true } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues.some((issue) => issue.path === "channels.telegram.groupMentionsOnly")).toBe(
true,
);
}
});
it("rejects gateway.token", async () => {
const res = validateConfigObject({
gateway: { token: "legacy-token" },

View File

@ -77,6 +77,89 @@ describe("legacy migrate mention routing", () => {
expect(res.changes).toEqual([]);
expect(res.config).toBeNull();
});
it("moves channels.telegram.groupMentionsOnly into groups.*.requireMention", () => {
const res = migrateLegacyConfig({
channels: {
telegram: {
groupMentionsOnly: true,
},
},
});
expect(res.changes).toContain(
'Moved channels.telegram.groupMentionsOnly → channels.telegram.groups."*".requireMention.',
);
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(true);
expect(
(res.config?.channels?.telegram as { groupMentionsOnly?: unknown } | undefined)
?.groupMentionsOnly,
).toBeUndefined();
});
it('keeps explicit channels.telegram.groups."*".requireMention when migrating groupMentionsOnly', () => {
const res = migrateLegacyConfig({
channels: {
telegram: {
groupMentionsOnly: true,
groups: {
"*": {
requireMention: false,
},
},
},
},
});
expect(res.changes).toContain(
'Removed channels.telegram.groupMentionsOnly (channels.telegram.groups."*" already set).',
);
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
expect(
(res.config?.channels?.telegram as { groupMentionsOnly?: unknown } | undefined)
?.groupMentionsOnly,
).toBeUndefined();
});
it("does not overwrite invalid channels.telegram.groups when migrating groupMentionsOnly", () => {
const res = migrateLegacyConfig({
channels: {
telegram: {
groupMentionsOnly: true,
groups: [],
},
},
});
expect(res.config).toBeNull();
expect(res.changes).toContain(
"Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.",
);
expect(res.changes).toContain(
"Migration applied, but config still invalid; fix remaining issues manually.",
);
});
it('does not overwrite invalid channels.telegram.groups."*" when migrating groupMentionsOnly', () => {
const res = migrateLegacyConfig({
channels: {
telegram: {
groupMentionsOnly: true,
groups: {
"*": false,
},
},
},
});
expect(res.config).toBeNull();
expect(res.changes).toContain(
"Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.",
);
expect(res.changes).toContain(
"Migration applied, but config still invalid; fix remaining issues manually.",
);
});
});
describe("legacy migrate tts provider shape", () => {

View File

@ -215,12 +215,36 @@ function migrateLegacyTtsConfig(
}
}
function resolveCompatibleDefaultGroupEntry(section: Record<string, unknown>): {
groups: Record<string, unknown>;
entry: Record<string, unknown>;
} | null {
const existingGroups = section.groups;
if (existingGroups !== undefined && !getRecord(existingGroups)) {
return null;
}
const groups = getRecord(existingGroups) ?? {};
const defaultKey = "*";
const existingEntry = groups[defaultKey];
if (existingEntry !== undefined && !getRecord(existingEntry)) {
return null;
}
const entry = getRecord(existingEntry) ?? {};
return { groups, entry };
}
const MEMORY_SEARCH_RULE: LegacyConfigRule = {
path: ["memorySearch"],
message:
"top-level memorySearch was moved; use agents.defaults.memorySearch instead (auto-migrated on load).",
};
const GROUP_MENTIONS_ONLY_RULE: LegacyConfigRule = {
path: ["channels", "telegram", "groupMentionsOnly"],
message:
'channels.telegram.groupMentionsOnly was removed; use channels.telegram.groups."*".requireMention instead (auto-migrated on load).',
};
const GATEWAY_BIND_RULE: LegacyConfigRule = {
path: ["gateway", "bind"],
message:
@ -307,6 +331,53 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [
);
},
}),
defineLegacyConfigMigration({
// v2026.2.23 replaced channels.telegram.groupMentionsOnly with
// channels.telegram.groups."*".requireMention. Existing configs crash on
// startup because gateway auto-migration only runs for registered legacy
// keys, and this removed key previously fell through as an unknown field.
id: "channels.telegram.groupMentionsOnly->channels.telegram.groups.*.requireMention",
describe:
"Move channels.telegram.groupMentionsOnly to channels.telegram.groups.*.requireMention",
legacyRules: [GROUP_MENTIONS_ONLY_RULE],
apply: (raw, changes) => {
const channels = ensureRecord(raw, "channels");
const telegram = getRecord(channels.telegram);
if (!telegram || telegram.groupMentionsOnly === undefined) {
return;
}
const groupMentionsOnly = telegram.groupMentionsOnly;
const defaultGroupEntry = resolveCompatibleDefaultGroupEntry(telegram);
const defaultKey = "*";
if (!defaultGroupEntry) {
changes.push(
"Skipped channels.telegram.groupMentionsOnly migration because channels.telegram.groups already has an incompatible shape; fix remaining issues manually.",
);
return;
}
const { groups, entry } = defaultGroupEntry;
if (entry.requireMention === undefined) {
entry.requireMention = groupMentionsOnly;
groups[defaultKey] = entry;
telegram.groups = groups;
changes.push(
'Moved channels.telegram.groupMentionsOnly → channels.telegram.groups."*".requireMention.',
);
} else {
changes.push(
'Removed channels.telegram.groupMentionsOnly (channels.telegram.groups."*" already set).',
);
}
delete telegram.groupMentionsOnly;
channels.telegram = telegram;
raw.channels = channels;
},
}),
defineLegacyConfigMigration({
id: "memorySearch->agents.defaults.memorySearch",
describe: "Move top-level memorySearch to agents.defaults.memorySearch",