diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index fb7123aaf27e7..db9cd199d30c5 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -5149,7 +5149,10 @@ async def send_exec_approval( InlineKeyboardButton("✅ Always", callback_data=f"ea:always:{approval_id}") ) buttons.append(InlineKeyboardButton("❌ Deny", callback_data=f"ea:deny:{approval_id}")) - keyboard = InlineKeyboardMarkup([buttons]) + # Pair into rows (2x2 for the full set) so labels stay readable on + # mobile — a single 4-button row truncates to "Allo… / Ses… / …". + rows = [buttons[i:i + 2] for i in range(0, len(buttons), 2)] + keyboard = InlineKeyboardMarkup(rows) kwargs: Dict[str, Any] = { "chat_id": normalize_telegram_chat_id(chat_id), diff --git a/tests/gateway/test_telegram_approval_buttons.py b/tests/gateway/test_telegram_approval_buttons.py index d65b2ad683a6f..da9297e562c18 100644 --- a/tests/gateway/test_telegram_approval_buttons.py +++ b/tests/gateway/test_telegram_approval_buttons.py @@ -148,6 +148,78 @@ async def test_non_smart_allow_permanent_false_keeps_session(self, monkeypatch): assert buttons == ["✅ Allow Once", "✅ Session", "❌ Deny"] + @pytest.mark.asyncio + async def test_full_approval_keyboard_is_two_by_two(self, monkeypatch): + """Regression: d48bf743f flattened all buttons into one row (4x1).""" + adapter = _make_adapter() + adapter._bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42)) + captured_rows = [] + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardButton", + lambda text, callback_data: text, + ) + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardMarkup", + lambda rows: captured_rows.extend(rows) or rows, + ) + + await adapter.send_exec_approval( + chat_id="12345", command="curl example.test", session_key="s", + ) + + assert captured_rows == [ + ["✅ Allow Once", "✅ Session"], + ["✅ Always", "❌ Deny"], + ] + + @pytest.mark.asyncio + async def test_three_button_keyboard_pairs_then_singleton(self, monkeypatch): + adapter = _make_adapter() + adapter._bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42)) + captured_rows = [] + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardButton", + lambda text, callback_data: text, + ) + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardMarkup", + lambda rows: captured_rows.extend(rows) or rows, + ) + + await adapter.send_exec_approval( + chat_id="12345", command="curl example.test", session_key="s", + allow_permanent=False, + ) + + assert captured_rows == [ + ["✅ Allow Once", "✅ Session"], + ["❌ Deny"], + ] + + @pytest.mark.asyncio + async def test_smart_deny_two_buttons_share_one_row(self, monkeypatch): + """smart_deny yields 2 buttons — they pair into a single readable row.""" + adapter = _make_adapter() + adapter._bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42)) + captured_rows = [] + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardButton", + lambda text, callback_data: text, + ) + monkeypatch.setattr( + "plugins.platforms.telegram.adapter.InlineKeyboardMarkup", + lambda rows: captured_rows.extend(rows) or rows, + ) + + await adapter.send_exec_approval( + chat_id="12345", command="curl example.test", session_key="s", + allow_permanent=False, smart_denied=True, + ) + + assert captured_rows == [ + ["✅ Allow Once", "❌ Deny"], + ] + @pytest.mark.asyncio async def test_stores_approval_state(self): adapter = _make_adapter()