fix: skip Telegram command sync when menu is unchanged (#32017)

Hash the command list and cache it to disk per account. On restart,
compare the current hash against the cached one and skip the
deleteMyCommands + setMyCommands round-trip when nothing changed.
This prevents 429 rate-limit errors when the gateway restarts
several times in quick succession.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
scoootscooob
2026-03-02 10:58:48 -08:00
committed by Peter Steinberger
parent 4a2329e0af
commit 10fb632c9e
3 changed files with 116 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import {
buildCappedTelegramMenuCommands,
buildPluginTelegramMenuCommands,
hashCommandList,
syncTelegramMenuCommands,
} from "./bot-native-command-menu.js";
@@ -108,6 +109,66 @@ describe("bot-native-command-menu", () => {
expect(callOrder).toEqual(["delete", "set"]);
});
it("produces a stable hash regardless of command order (#32017)", () => {
const commands = [
{ command: "bravo", description: "B" },
{ command: "alpha", description: "A" },
];
const reversed = [...commands].toReversed();
expect(hashCommandList(commands)).toBe(hashCommandList(reversed));
});
it("produces different hashes for different command lists (#32017)", () => {
const a = [{ command: "alpha", description: "A" }];
const b = [{ command: "alpha", description: "Changed" }];
expect(hashCommandList(a)).not.toBe(hashCommandList(b));
});
it("skips sync when command hash is unchanged (#32017)", async () => {
const deleteMyCommands = vi.fn(async () => undefined);
const setMyCommands = vi.fn(async () => undefined);
const runtimeLog = vi.fn();
// Use a unique accountId so cached hashes from other tests don't interfere.
const accountId = `test-skip-${Date.now()}`;
const commands = [{ command: "skip_test", description: "Skip test command" }];
// First sync — no cached hash, should call setMyCommands.
syncTelegramMenuCommands({
bot: {
api: { deleteMyCommands, setMyCommands },
} as unknown as Parameters<typeof syncTelegramMenuCommands>[0]["bot"],
runtime: { log: runtimeLog, error: vi.fn(), exit: vi.fn() } as Parameters<
typeof syncTelegramMenuCommands
>[0]["runtime"],
commandsToRegister: commands,
accountId,
});
await vi.waitFor(() => {
expect(setMyCommands).toHaveBeenCalledTimes(1);
});
// Second sync with the same commands — hash is cached, should skip.
syncTelegramMenuCommands({
bot: {
api: { deleteMyCommands, setMyCommands },
} as unknown as Parameters<typeof syncTelegramMenuCommands>[0]["bot"],
runtime: { log: runtimeLog, error: vi.fn(), exit: vi.fn() } as Parameters<
typeof syncTelegramMenuCommands
>[0]["runtime"],
commandsToRegister: commands,
accountId,
});
await vi.waitFor(() => {
expect(runtimeLog).toHaveBeenCalledWith("telegram: command menu unchanged; skipping sync");
});
// setMyCommands should NOT have been called a second time.
expect(setMyCommands).toHaveBeenCalledTimes(1);
});
it("retries with fewer commands on BOT_COMMANDS_TOO_MUCH", async () => {
const deleteMyCommands = vi.fn(async () => undefined);
const setMyCommands = vi