test(telegram): pin inline-keyboards flag gate and buttons validation
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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user