From a3c95ebd2d8d2d45148a96c7371f625380fac94e Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Fri, 24 Jul 2026 14:29:56 +0700 Subject: [PATCH 1/3] fix(telegram): restore 2x2 exec-approval button layout Pair conditional approval buttons into rows of two so the full Allow Once / Session / Always / Deny set stays readable instead of one truncated 4x1 row. --- plugins/platforms/telegram/adapter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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), From ca7146d45cb2ba5a49f2e030ebd90f48bfa19f64 Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Fri, 24 Jul 2026 14:29:56 +0700 Subject: [PATCH 2/3] test(telegram): cover exec-approval keyboard row pairing Assert the full set renders as 2x2 and the three-button case keeps Deny on its own second row. --- .../gateway/test_telegram_approval_buttons.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/gateway/test_telegram_approval_buttons.py b/tests/gateway/test_telegram_approval_buttons.py index d65b2ad683a6f..c01e2cf7fc537 100644 --- a/tests/gateway/test_telegram_approval_buttons.py +++ b/tests/gateway/test_telegram_approval_buttons.py @@ -148,6 +148,54 @@ 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_stores_approval_state(self): adapter = _make_adapter() From d6d8a879d584424f9f7b1b1e6b1e4b3bf2ed6871 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:01:01 +0500 Subject: [PATCH 3/3] test(telegram): cover smart_deny 2-button row structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up for salvaged PR #70615 — the 2-button smart_deny case (Allow Once + Deny only) was exercised by an existing test but only at the flat-label level, not asserting the row pairing. Adds the missing row-structure assertion using the same capture pattern as the 4-button and 3-button tests. --- .../gateway/test_telegram_approval_buttons.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/gateway/test_telegram_approval_buttons.py b/tests/gateway/test_telegram_approval_buttons.py index c01e2cf7fc537..da9297e562c18 100644 --- a/tests/gateway/test_telegram_approval_buttons.py +++ b/tests/gateway/test_telegram_approval_buttons.py @@ -196,6 +196,30 @@ async def test_three_button_keyboard_pairs_then_singleton(self, monkeypatch): ["❌ 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()