From b9b81d9301877cff7aa4ae6bc7ea8c937ceec98a Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Thu, 23 Apr 2026 04:32:06 +0000 Subject: [PATCH] test(telegram): pin inline-keyboards flag gate and buttons validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two kill-switch tests for the new inline-keyboards path. Neither is flashy — they just make sure the next unrelated refactor can't quietly regress two narrow contracts the PR relies on. 1. TelegramChannel._build_keyboard returns None whenever TelegramConfig.inline_keyboards is False, even if buttons are supplied. The flag defaults off; if someone ever flips that default the change should fail this test before it reaches prod bots. 2. MessageTool rejects malformed `buttons` payloads (non-list, mixed list/str row, non-str label, None label) up front instead of letting them slip into the channel layer where Telegram would silently 400 the send. Parametrized over four shapes the guard needs to reject. No production code touched. Made-with: Cursor --- tests/channels/test_telegram_channel.py | 26 +++++++++++++++++++++++++ tests/tools/test_message_tool.py | 21 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tests/channels/test_telegram_channel.py b/tests/channels/test_telegram_channel.py index 4a69d31a..5480b878 100644 --- a/tests/channels/test_telegram_channel.py +++ b/tests/channels/test_telegram_channel.py @@ -1591,3 +1591,29 @@ async def test_send_delta_mid_stream_strips_markdown() -> None: assert "**" not in edited_text assert "Title" in edited_text assert "1. step" in edited_text + + +def test_build_keyboard_respects_inline_keyboards_flag() -> None: + """``_build_keyboard`` returns ``None`` whenever the feature flag is off, + regardless of whether buttons are provided; returns a proper Markup only + when the flag is explicitly enabled. Pins the kill-switch so accidentally + flipping the default doesn't silently expose callback handlers.""" + from telegram import InlineKeyboardMarkup + + off = TelegramChannel( + TelegramConfig(enabled=True, token="123:abc", inline_keyboards=False), + MessageBus(), + ) + assert off._build_keyboard([["A", "B"]]) is None + + on = TelegramChannel( + TelegramConfig(enabled=True, token="123:abc", inline_keyboards=True), + MessageBus(), + ) + assert on._build_keyboard([]) is None # empty still no-op + markup = on._build_keyboard([["Yes", "No"], ["Cancel"]]) + assert isinstance(markup, InlineKeyboardMarkup) + rows = markup.inline_keyboard + assert [[b.text for b in row] for row in rows] == [["Yes", "No"], ["Cancel"]] + # callback_data mirrors label so _on_callback_query can echo the tap back. + assert rows[0][0].callback_data == "Yes" diff --git a/tests/tools/test_message_tool.py b/tests/tools/test_message_tool.py index dc8e11d5..b65b5cd8 100644 --- a/tests/tools/test_message_tool.py +++ b/tests/tools/test_message_tool.py @@ -8,3 +8,24 @@ async def test_message_tool_returns_error_when_no_target_context() -> None: tool = MessageTool() result = await tool.execute(content="test") assert result == "Error: No target channel/chat specified" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "bad", + [ + "not a list", + [["ok"], "row-not-a-list"], + [["ok", 42]], + [[None]], + ], +) +async def test_message_tool_rejects_malformed_buttons(bad) -> None: + """``buttons`` must be ``list[list[str]]``; the tool validates the shape + up front so a malformed LLM payload errors visibly instead of slipping + into the channel layer where Telegram would silently reject the frame.""" + tool = MessageTool() + result = await tool.execute( + content="hi", channel="telegram", chat_id="1", buttons=bad, + ) + assert result == "Error: buttons must be a list of list of strings"