From 63faf9bf4a8feb7f38cbe3cf61908d9cd9e89ea4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 01:34:52 +0100 Subject: [PATCH] refactor: share telegram model selection matcher --- .../telegram/src/bot-model-list.test.ts | 143 ++++++++++++++++++ extensions/telegram/src/model-buttons.ts | 15 +- extensions/telegram/src/model-selection.ts | 14 ++ 3 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 extensions/telegram/src/bot-model-list.test.ts create mode 100644 extensions/telegram/src/model-selection.ts diff --git a/extensions/telegram/src/bot-model-list.test.ts b/extensions/telegram/src/bot-model-list.test.ts new file mode 100644 index 00000000000..ec9fc1631a9 --- /dev/null +++ b/extensions/telegram/src/bot-model-list.test.ts @@ -0,0 +1,143 @@ +import { rm } from "node:fs/promises"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +const { + answerCallbackQuerySpy, + editMessageTextSpy, + getLoadConfigMock, + getOnHandler, + replySpy, + telegramBotDepsForTest, + telegramBotRuntimeForTest, +} = await import("./bot.create-telegram-bot.test-harness.js"); + +let createTelegramBotBase: typeof import("./bot.js").createTelegramBot; +let setTelegramBotRuntimeForTest: typeof import("./bot.js").setTelegramBotRuntimeForTest; +let createTelegramBot: ( + opts: Parameters[0], +) => ReturnType; + +const loadConfig = getLoadConfigMock(); + +describe("createTelegramBot model list callbacks", () => { + beforeAll(async () => { + ({ createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } = + await import("./bot.js")); + }); + + beforeEach(() => { + loadConfig.mockReturnValue({ + channels: { + telegram: { dmPolicy: "open", allowFrom: ["*"] }, + }, + }); + setTelegramBotRuntimeForTest( + telegramBotRuntimeForTest as unknown as Parameters[0], + ); + createTelegramBot = (opts) => + createTelegramBotBase({ + ...opts, + telegramDeps: telegramBotDepsForTest, + }); + }); + + it("keeps provider-scoped current-model markers in model list callbacks", async () => { + replySpy.mockClear(); + editMessageTextSpy.mockClear(); + + const storePath = `/tmp/openclaw-telegram-model-list-${process.pid}-${Date.now()}.json`; + + await rm(storePath, { force: true }); + try { + const config = { + agents: { + defaults: { + model: "anthropic/claude-opus-4-6", + models: { + "anthropic/claude-opus-4-6": {}, + "github-copilot/gpt-5.4": {}, + "openai-codex/gpt-5.4": {}, + "openai-codex/gpt-5.3-codex-spark": {}, + }, + }, + }, + channels: { + telegram: { + dmPolicy: "open", + allowFrom: ["*"], + }, + }, + session: { + store: storePath, + }, + } satisfies NonNullable[0]["config"]>; + + loadConfig.mockReturnValue(config); + createTelegramBot({ + token: "tok", + config, + }); + const callbackHandler = getOnHandler("callback_query") as ( + ctx: Record, + ) => Promise; + expect(callbackHandler).toBeDefined(); + + await callbackHandler({ + callbackQuery: { + id: "cbq-model-list-1", + data: "mdl_sel_github-copilot/gpt-5.4", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 18, + }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + editMessageTextSpy.mockClear(); + answerCallbackQuerySpy.mockClear(); + + await callbackHandler({ + callbackQuery: { + id: "cbq-model-list-2", + data: "mdl_list_openai-codex_1", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 18, + }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).not.toHaveBeenCalled(); + expect(editMessageTextSpy).toHaveBeenCalledTimes(1); + const [chatId, messageId, _text, params] = editMessageTextSpy.mock.calls[0] ?? []; + expect(chatId).toBe(1234); + expect(messageId).toBe(18); + + const buttonTexts = + ( + params as + | { + reply_markup?: { + inline_keyboard?: Array>; + }; + } + | undefined + )?.reply_markup?.inline_keyboard + ?.flat() + .map((button) => button.text) ?? []; + + expect(buttonTexts).toContain("gpt-5.4"); + expect(buttonTexts).not.toContain("gpt-5.4 ✓"); + expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-list-2"); + } finally { + await rm(storePath, { force: true }); + } + }); +}); diff --git a/extensions/telegram/src/model-buttons.ts b/extensions/telegram/src/model-buttons.ts index 8f421f2fc11..6570f8494ad 100644 --- a/extensions/telegram/src/model-buttons.ts +++ b/extensions/telegram/src/model-buttons.ts @@ -9,6 +9,7 @@ * - mdl_back - back to providers list */ import { fitsTelegramCallbackData } from "./approval-callback-data.js"; +import { isCurrentModelSelection } from "./model-selection.js"; export type ButtonRow = Array<{ text: string; callback_data: string }>; @@ -144,20 +145,6 @@ export function resolveModelSelection(params: { }; } -function isCurrentModelSelection(params: { - currentModel?: string; - provider: string; - model: string; -}): boolean { - const currentModel = params.currentModel?.trim(); - if (!currentModel) { - return false; - } - return currentModel.includes("/") - ? currentModel === `${params.provider}/${params.model}` - : currentModel === params.model; -} - /** * Build provider selection keyboard with 2 providers per row. */ diff --git a/extensions/telegram/src/model-selection.ts b/extensions/telegram/src/model-selection.ts new file mode 100644 index 00000000000..bd53448d2eb --- /dev/null +++ b/extensions/telegram/src/model-selection.ts @@ -0,0 +1,14 @@ +export function isCurrentModelSelection(params: { + currentModel?: string; + provider: string; + model: string; +}): boolean { + const currentModel = params.currentModel?.trim(); + if (!currentModel) { + return false; + } + + return currentModel.includes("/") + ? currentModel === `${params.provider}/${params.model}` + : currentModel === params.model; +}