From f1ec7a75f639d1ca5515c9ad4809c87136ab7ccd Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 17:34:51 +0100 Subject: [PATCH] fix(telegram): retry failed plugin binding callbacks --- .../telegram/src/bot-handlers.runtime.ts | 15 +++-- .../src/bot.create-telegram-bot.test.ts | 63 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 731bb775a02..6c473f8d8f6 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -1283,11 +1283,16 @@ export const registerTelegramHandlers = ({ callbackThreadId != null ? `${chatId}:topic:${callbackThreadId}` : String(chatId); const pluginBindingApproval = parsePluginBindingApprovalCustomId(data); if (pluginBindingApproval) { - const resolved = await resolvePluginConversationBindingApproval({ - approvalId: pluginBindingApproval.approvalId, - decision: pluginBindingApproval.decision, - senderId: senderId || undefined, - }); + let resolved: Awaited>; + try { + resolved = await resolvePluginConversationBindingApproval({ + approvalId: pluginBindingApproval.approvalId, + decision: pluginBindingApproval.decision, + senderId: senderId || undefined, + }); + } catch (err) { + throw new TelegramRetryableCallbackError(err); + } await clearCallbackButtons(); await replyToCallbackChat(buildPluginBindingResolvedText(resolved)); return; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 81e40e930be..56c7b662966 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -3,12 +3,14 @@ import { withEnvAsync } from "openclaw/plugin-sdk/testing"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; const harness = await import("./bot.create-telegram-bot.test-harness.js"); +const conversationRuntime = await import("openclaw/plugin-sdk/conversation-runtime"); const EYES_EMOJI = "\u{1F440}"; const { answerCallbackQuerySpy, botCtorSpy, commandSpy, dispatchReplyWithBufferedBlockDispatcher, + editMessageReplyMarkupSpy, editMessageTextSpy, enqueueSystemEventSpy, getLoadWebMediaMock, @@ -2984,4 +2986,65 @@ describe("createTelegramBot", () => { expect(editMessageTextSpy).toHaveBeenCalledTimes(2); expect(editMessageTextSpy.mock.calls.at(-1)?.[2]).toContain("Commands (2/"); }); + + it("retries plugin binding approval callbacks after a bubbled resolution failure", async () => { + createTelegramBot({ token: "tok" }); + const callbackHandler = getOnHandler("callback_query"); + const middlewares = middlewareUseSpy.mock.calls + .map((call) => call[0]) + .filter( + (fn): fn is (ctx: Record, next: () => Promise) => Promise => + typeof fn === "function", + ); + const runMiddlewareChain = async (ctx: Record) => { + let idx = -1; + const dispatch = async (i: number): Promise => { + if (i <= idx) { + throw new Error("middleware dispatch called multiple times"); + } + idx = i; + const fn = middlewares[i]; + if (!fn) { + await callbackHandler(ctx); + return; + } + await fn(ctx, async () => dispatch(i + 1)); + }; + await dispatch(0); + }; + + const resolvePluginBindingApprovalSpy = vi.spyOn( + conversationRuntime, + "resolvePluginConversationBindingApproval", + ); + resolvePluginBindingApprovalSpy.mockRejectedValueOnce(new Error("binding boom")); + + const ctx = { + update: { update_id: 888 }, + callbackQuery: { + id: "cbq-plugin-binding-retry-1", + data: conversationRuntime.buildPluginBindingApprovalCustomId("binding-1", "allow-once"), + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 20, + text: "Plugin approval required.", + }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }; + + try { + await expect(runMiddlewareChain(ctx)).rejects.toThrow("binding boom"); + await runMiddlewareChain(ctx); + } finally { + resolvePluginBindingApprovalSpy.mockRestore(); + } + + expect(editMessageReplyMarkupSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0]?.[1]).toContain("plugin bind approval"); + }); });