diff --git a/nanobot/channels/telegram/runtime.py b/nanobot/channels/telegram/runtime.py index c87eda01..1bccbcfe 100644 --- a/nanobot/channels/telegram/runtime.py +++ b/nanobot/channels/telegram/runtime.py @@ -90,18 +90,27 @@ def _split_telegram_markdown(content: str, max_len: int) -> list[str]: min_code_pos = len(fence) if content.startswith(fence + "\n"): min_code_pos += 1 - if pos < min_code_pos and min_code_pos + len(closing) > max_len: - chunks.append(content[:max_len]) - content = content[max_len:].lstrip() - continue - if pos + len(closing) > max_len: + # When the only break in range is the opening fence newline, + # cutting there re-emits the same fence and never advances. + if pos < min_code_pos: + if min_code_pos + len(closing) > max_len: + chunks.append(content[:max_len]) + content = content[max_len:].lstrip() + continue + budget = max_len - len(closing) + recut = content[:budget] + adjusted = recut.rfind("\n", min_code_pos) + if adjusted < min_code_pos: + adjusted = recut.rfind(" ", min_code_pos) + pos = adjusted if adjusted >= min_code_pos else budget + elif pos + len(closing) > max_len: budget = max_len - len(closing) if budget > 0: recut = content[:budget] - adjusted = recut.rfind("\n") - if adjusted <= 0: - adjusted = recut.rfind(" ") - pos = adjusted if adjusted > 0 else budget + adjusted = recut.rfind("\n", min_code_pos) + if adjusted < min_code_pos: + adjusted = recut.rfind(" ", min_code_pos) + pos = adjusted if adjusted >= min_code_pos else budget else: closing = "```" pos = max_len - len(closing) diff --git a/nanobot/channels/telegram/tests/test_telegram_channel.py b/nanobot/channels/telegram/tests/test_telegram_channel.py index f459664e..8693157f 100644 --- a/nanobot/channels/telegram/tests/test_telegram_channel.py +++ b/nanobot/channels/telegram/tests/test_telegram_channel.py @@ -15,6 +15,7 @@ from nanobot.bus.events import OutboundMessage from nanobot.bus.outbound_events import ProgressEvent from nanobot.bus.queue import MessageBus from nanobot.channels.telegram.runtime import ( + TELEGRAM_MAX_MESSAGE_LEN, TELEGRAM_REPLY_CONTEXT_MAX_LEN, TelegramChannel, TelegramConfig, @@ -243,6 +244,30 @@ def test_split_telegram_markdown_leading_whitespace_before_fence() -> None: _assert_code_blocks_render_balanced(chunks) +def test_split_telegram_markdown_long_single_line_code_body() -> None: + """Long fence bodies with no interior newlines must still advance.""" + body = "a" * 4500 + content = f"```\n{body}\n```" + + chunks = _split_telegram_markdown(content, TELEGRAM_MAX_MESSAGE_LEN) + + assert len(chunks) > 1 + assert all(len(chunk) <= TELEGRAM_MAX_MESSAGE_LEN for chunk in chunks) + assert chunks[0].startswith("```\n") + assert chunks[0].endswith("\n```") + assert chunks[1].startswith("```\n") + reassembled = [] + for chunk in chunks: + part = chunk.split("\n", 1)[1] + if part.endswith("\n```"): + part = part[:-4] + elif part.endswith("```"): + part = part[:-3] + reassembled.append(part) + assert "".join(reassembled) == body + _assert_code_blocks_render_balanced(chunks) + + @pytest.mark.asyncio async def test_start_creates_separate_pools_with_proxy(monkeypatch) -> None: _FakeHTTPXRequest.clear()