/ tests / gateway / test_telegram_format.py
test_telegram_format.py
  1  """Tests for Telegram MarkdownV2 formatting in gateway/platforms/telegram.py.
  2  
  3  Covers: _escape_mdv2 (pure function), format_message (markdown-to-MarkdownV2
  4  conversion pipeline), and edge cases that could produce invalid MarkdownV2
  5  or corrupt user-visible content.
  6  """
  7  
  8  import re
  9  import sys
 10  from unittest.mock import AsyncMock, MagicMock
 11  
 12  import pytest
 13  
 14  from gateway.config import PlatformConfig
 15  
 16  
 17  # ---------------------------------------------------------------------------
 18  # Mock the telegram package if it's not installed
 19  # ---------------------------------------------------------------------------
 20  
 21  def _ensure_telegram_mock():
 22      if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"):
 23          return
 24      mod = MagicMock()
 25      mod.ext.ContextTypes.DEFAULT_TYPE = type(None)
 26      mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2"
 27      mod.constants.ChatType.GROUP = "group"
 28      mod.constants.ChatType.SUPERGROUP = "supergroup"
 29      mod.constants.ChatType.CHANNEL = "channel"
 30      mod.constants.ChatType.PRIVATE = "private"
 31      for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
 32          sys.modules.setdefault(name, mod)
 33  
 34  
 35  _ensure_telegram_mock()
 36  
 37  from gateway.platforms.telegram import (  # noqa: E402
 38      TelegramAdapter,
 39      _escape_mdv2,
 40      _strip_mdv2,
 41      _wrap_markdown_tables,
 42  )
 43  
 44  
 45  # ---------------------------------------------------------------------------
 46  # Fixtures
 47  # ---------------------------------------------------------------------------
 48  
 49  @pytest.fixture()
 50  def adapter():
 51      config = PlatformConfig(enabled=True, token="fake-token")
 52      return TelegramAdapter(config)
 53  
 54  
 55  # =========================================================================
 56  # _escape_mdv2
 57  # =========================================================================
 58  
 59  
 60  class TestEscapeMdv2:
 61      def test_escapes_all_special_characters(self):
 62          special = r'_*[]()~`>#+-=|{}.!\ '
 63          escaped = _escape_mdv2(special)
 64          # Every special char should be preceded by backslash
 65          for ch in r'_*[]()~`>#+-=|{}.!\  ':
 66              if ch == ' ':
 67                  continue
 68              assert f'\\{ch}' in escaped
 69  
 70      def test_empty_string(self):
 71          assert _escape_mdv2("") == ""
 72  
 73      def test_no_special_characters(self):
 74          assert _escape_mdv2("hello world 123") == "hello world 123"
 75  
 76      def test_backslash_escaped(self):
 77          assert _escape_mdv2("a\\b") == "a\\\\b"
 78  
 79      def test_dot_escaped(self):
 80          assert _escape_mdv2("v2.0") == "v2\\.0"
 81  
 82      def test_exclamation_escaped(self):
 83          assert _escape_mdv2("wow!") == "wow\\!"
 84  
 85      def test_mixed_text_and_specials(self):
 86          result = _escape_mdv2("Hello (world)!")
 87          assert result == "Hello \\(world\\)\\!"
 88  
 89  
 90  # =========================================================================
 91  # format_message - basic conversions
 92  # =========================================================================
 93  
 94  
 95  class TestFormatMessageBasic:
 96      def test_empty_string(self, adapter):
 97          assert adapter.format_message("") == ""
 98  
 99      def test_none_input(self, adapter):
100          # content is falsy, returned as-is
101          assert adapter.format_message(None) is None
102  
103      def test_plain_text_specials_escaped(self, adapter):
104          result = adapter.format_message("Price is $5.00!")
105          assert "\\." in result
106          assert "\\!" in result
107  
108      def test_plain_text_no_markdown(self, adapter):
109          result = adapter.format_message("Hello world")
110          assert result == "Hello world"
111  
112  
113  # =========================================================================
114  # format_message - code blocks
115  # =========================================================================
116  
117  
118  class TestFormatMessageCodeBlocks:
119      def test_fenced_code_block_preserved(self, adapter):
120          text = "Before\n```python\nprint('hello')\n```\nAfter"
121          result = adapter.format_message(text)
122          # Code block contents must NOT be escaped
123          assert "```python\nprint('hello')\n```" in result
124          # But "After" should have no escaping needed (plain text)
125          assert "After" in result
126  
127      def test_inline_code_preserved(self, adapter):
128          text = "Use `my_var` here"
129          result = adapter.format_message(text)
130          # Inline code content must NOT be escaped
131          assert "`my_var`" in result
132          # The surrounding text's underscore-free content should be fine
133          assert "Use" in result
134  
135      def test_code_block_special_chars_not_escaped(self, adapter):
136          text = "```\nif (x > 0) { return !x; }\n```"
137          result = adapter.format_message(text)
138          # Inside code block, > and ! and { should NOT be escaped
139          assert "if (x > 0) { return !x; }" in result
140  
141      def test_inline_code_special_chars_not_escaped(self, adapter):
142          text = "Run `rm -rf ./*` carefully"
143          result = adapter.format_message(text)
144          assert "`rm -rf ./*`" in result
145  
146      def test_multiple_code_blocks(self, adapter):
147          text = "```\nblock1\n```\ntext\n```\nblock2\n```"
148          result = adapter.format_message(text)
149          assert "block1" in result
150          assert "block2" in result
151          # "text" between blocks should be present
152          assert "text" in result
153  
154      def test_inline_code_backslashes_escaped(self, adapter):
155          r"""Backslashes in inline code must be escaped for MarkdownV2."""
156          text = r"Check `C:\ProgramData\VMware\` path"
157          result = adapter.format_message(text)
158          assert r"`C:\\ProgramData\\VMware\\`" in result
159  
160      def test_fenced_code_block_backslashes_escaped(self, adapter):
161          r"""Backslashes in fenced code blocks must be escaped for MarkdownV2."""
162          text = "```\npath = r'C:\\Users\\test'\n```"
163          result = adapter.format_message(text)
164          assert r"C:\\Users\\test" in result
165  
166      def test_fenced_code_block_backticks_escaped(self, adapter):
167          r"""Backticks inside fenced code blocks must be escaped for MarkdownV2."""
168          text = "```\necho `hostname`\n```"
169          result = adapter.format_message(text)
170          assert r"echo \`hostname\`" in result
171  
172      def test_inline_code_no_double_escape(self, adapter):
173          r"""Already-escaped backslashes should not be quadruple-escaped."""
174          text = r"Use `\\server\share`"
175          result = adapter.format_message(text)
176          # \\ in input → \\\\ in output (each \ escaped once)
177          assert r"`\\\\server\\share`" in result
178  
179  
180  # =========================================================================
181  # format_message - bold and italic
182  # =========================================================================
183  
184  
185  class TestFormatMessageBoldItalic:
186      def test_bold_converted(self, adapter):
187          result = adapter.format_message("This is **bold** text")
188          # MarkdownV2 bold uses single *
189          assert "*bold*" in result
190          # Original ** should be gone
191          assert "**" not in result
192  
193      def test_italic_converted(self, adapter):
194          result = adapter.format_message("This is *italic* text")
195          # MarkdownV2 italic uses _
196          assert "_italic_" in result
197  
198      def test_bold_with_special_chars(self, adapter):
199          result = adapter.format_message("**hello.world!**")
200          # Content inside bold should be escaped
201          assert "*hello\\.world\\!*" in result
202  
203      def test_italic_with_special_chars(self, adapter):
204          result = adapter.format_message("*hello.world*")
205          assert "_hello\\.world_" in result
206  
207      def test_bold_and_italic_in_same_line(self, adapter):
208          result = adapter.format_message("**bold** and *italic*")
209          assert "*bold*" in result
210          assert "_italic_" in result
211  
212  
213  # =========================================================================
214  # format_message - headers
215  # =========================================================================
216  
217  
218  class TestFormatMessageHeaders:
219      def test_h1_converted_to_bold(self, adapter):
220          result = adapter.format_message("# Title")
221          # Header becomes bold in MarkdownV2
222          assert "*Title*" in result
223          # Hash should be removed
224          assert "#" not in result
225  
226      def test_h2_converted(self, adapter):
227          result = adapter.format_message("## Subtitle")
228          assert "*Subtitle*" in result
229  
230      def test_header_with_inner_bold_stripped(self, adapter):
231          # Headers strip redundant **...** inside
232          result = adapter.format_message("## **Important**")
233          # Should be *Important* not ***Important***
234          assert "*Important*" in result
235          count = result.count("*")
236          # Should have exactly 2 asterisks (open + close)
237          assert count == 2
238  
239      def test_header_with_special_chars(self, adapter):
240          result = adapter.format_message("# Hello (World)!")
241          assert "\\(" in result
242          assert "\\)" in result
243          assert "\\!" in result
244  
245      def test_multiline_headers(self, adapter):
246          text = "# First\nSome text\n## Second"
247          result = adapter.format_message(text)
248          assert "*First*" in result
249          assert "*Second*" in result
250          assert "Some text" in result
251  
252  
253  # =========================================================================
254  # format_message - links
255  # =========================================================================
256  
257  
258  class TestFormatMessageLinks:
259      def test_markdown_link_converted(self, adapter):
260          result = adapter.format_message("[Click here](https://example.com)")
261          assert "[Click here](https://example.com)" in result
262  
263      def test_link_display_text_escaped(self, adapter):
264          result = adapter.format_message("[Hello!](https://example.com)")
265          # The ! in display text should be escaped
266          assert "Hello\\!" in result
267  
268      def test_link_url_parentheses_escaped(self, adapter):
269          result = adapter.format_message("[link](https://example.com/path_(1))")
270          # The ) in URL should be escaped
271          assert "\\)" in result
272  
273      def test_link_with_surrounding_text(self, adapter):
274          result = adapter.format_message("Visit [Google](https://google.com) today.")
275          assert "[Google](https://google.com)" in result
276          assert "today\\." in result
277  
278  
279  # =========================================================================
280  # format_message - BUG: italic regex spans newlines
281  # =========================================================================
282  
283  
284  class TestItalicNewlineBug:
285      r"""Italic regex ``\*([^*]+)\*`` matched across newlines, corrupting content.
286  
287      This affects bullet lists using * markers and any text where * appears
288      at the end of one line and start of another.
289      """
290  
291      def test_bullet_list_not_corrupted(self, adapter):
292          """Bullet list items using * must NOT be merged into italic."""
293          text = "* Item one\n* Item two\n* Item three"
294          result = adapter.format_message(text)
295          # Each item should appear in the output (not eaten by italic conversion)
296          assert "Item one" in result
297          assert "Item two" in result
298          assert "Item three" in result
299          # Should NOT contain _ (italic markers) wrapping list items
300          assert "_" not in result or "Item" not in result.split("_")[1] if "_" in result else True
301  
302      def test_asterisk_list_items_preserved(self, adapter):
303          """Each * list item should remain as a separate line, not become italic."""
304          text = "* Alpha\n* Beta"
305          result = adapter.format_message(text)
306          # Both items must be present in output
307          assert "Alpha" in result
308          assert "Beta" in result
309          # The text between first * and second * must NOT become italic
310          lines = result.split("\n")
311          assert len(lines) >= 2
312  
313      def test_italic_does_not_span_lines(self, adapter):
314          """*text on\nmultiple lines* should NOT become italic."""
315          text = "Start *across\nlines* end"
316          result = adapter.format_message(text)
317          # Should NOT have underscore italic markers wrapping cross-line text
318          # If this fails, the italic regex is matching across newlines
319          assert "_across\nlines_" not in result
320  
321      def test_single_line_italic_still_works(self, adapter):
322          """Normal single-line italic must still convert correctly."""
323          text = "This is *italic* text"
324          result = adapter.format_message(text)
325          assert "_italic_" in result
326  
327  
328  # =========================================================================
329  # format_message - strikethrough
330  # =========================================================================
331  
332  
333  class TestFormatMessageStrikethrough:
334      def test_strikethrough_converted(self, adapter):
335          result = adapter.format_message("This is ~~deleted~~ text")
336          assert "~deleted~" in result
337          assert "~~" not in result
338  
339      def test_strikethrough_with_special_chars(self, adapter):
340          result = adapter.format_message("~~hello.world!~~")
341          assert "~hello\\.world\\!~" in result
342  
343      def test_strikethrough_in_code_not_converted(self, adapter):
344          result = adapter.format_message("`~~not struck~~`")
345          assert "`~~not struck~~`" in result
346  
347      def test_strikethrough_with_bold(self, adapter):
348          result = adapter.format_message("**bold** and ~~struck~~")
349          assert "*bold*" in result
350          assert "~struck~" in result
351  
352  
353  # =========================================================================
354  # format_message - spoiler
355  # =========================================================================
356  
357  
358  class TestFormatMessageSpoiler:
359      def test_spoiler_converted(self, adapter):
360          result = adapter.format_message("This is ||hidden|| text")
361          assert "||hidden||" in result
362  
363      def test_spoiler_with_special_chars(self, adapter):
364          result = adapter.format_message("||hello.world!||")
365          assert "||hello\\.world\\!||" in result
366  
367      def test_spoiler_in_code_not_converted(self, adapter):
368          result = adapter.format_message("`||not spoiler||`")
369          assert "`||not spoiler||`" in result
370  
371      def test_spoiler_pipes_not_escaped(self, adapter):
372          """The || delimiters must not be escaped as \\|\\|."""
373          result = adapter.format_message("||secret||")
374          assert "\\|\\|" not in result
375          assert "||secret||" in result
376  
377  
378  # =========================================================================
379  # format_message - blockquote
380  # =========================================================================
381  
382  
383  class TestFormatMessageBlockquote:
384      def test_blockquote_converted(self, adapter):
385          result = adapter.format_message("> This is a quote")
386          assert "> This is a quote" in result
387          # > must NOT be escaped
388          assert "\\>" not in result
389  
390      def test_blockquote_with_special_chars(self, adapter):
391          result = adapter.format_message("> Hello (world)!")
392          assert "> Hello \\(world\\)\\!" in result
393          assert "\\>" not in result
394  
395      def test_blockquote_multiline(self, adapter):
396          text = "> Line one\n> Line two"
397          result = adapter.format_message(text)
398          assert "> Line one" in result
399          assert "> Line two" in result
400          assert "\\>" not in result
401  
402      def test_blockquote_in_code_not_converted(self, adapter):
403          result = adapter.format_message("```\n> not a quote\n```")
404          assert "> not a quote" in result
405  
406      def test_nested_blockquote(self, adapter):
407          result = adapter.format_message(">> Nested quote")
408          assert ">> Nested quote" in result
409          assert "\\>" not in result
410  
411      def test_gt_in_middle_of_line_still_escaped(self, adapter):
412          """Only > at line start is a blockquote; mid-line > should be escaped."""
413          result = adapter.format_message("5 > 3")
414          assert "\\>" in result
415  
416      def test_expandable_blockquote(self, adapter):
417          """Expandable blockquote prefix **> and trailing || must NOT be escaped."""
418          result = adapter.format_message("**> Hidden content||")
419          assert "**>" in result
420          assert "||" in result
421          assert "\\*" not in result  # asterisks in prefix must not be escaped
422          assert "\\>" not in result  # > in prefix must not be escaped
423  
424      def test_single_asterisk_gt_not_blockquote(self, adapter):
425          """Single asterisk before > should not be treated as blockquote prefix."""
426          result = adapter.format_message("*> not a quote")
427          assert "\\*" in result
428          assert "\\>" in result
429  
430      def test_regular_blockquote_with_pipes_escaped(self, adapter):
431          """Regular blockquote ending with || should escape the pipes."""
432          result = adapter.format_message("> not expandable||")
433          assert "> not expandable" in result
434          assert "\\|" in result
435          assert "\\>" not in result
436  
437  
438  # =========================================================================
439  # format_message - mixed/complex
440  # =========================================================================
441  
442  
443  class TestFormatMessageComplex:
444      def test_code_block_with_bold_outside(self, adapter):
445          text = "**Note:**\n```\ncode here\n```"
446          result = adapter.format_message(text)
447          assert "*Note:*" in result or "*Note\\:*" in result
448          assert "```\ncode here\n```" in result
449  
450      def test_bold_inside_code_not_converted(self, adapter):
451          """Bold markers inside code blocks should not be converted."""
452          text = "```\n**not bold**\n```"
453          result = adapter.format_message(text)
454          assert "**not bold**" in result
455  
456      def test_link_inside_code_not_converted(self, adapter):
457          text = "`[not a link](url)`"
458          result = adapter.format_message(text)
459          assert "`[not a link](url)`" in result
460  
461      def test_header_after_code_block(self, adapter):
462          text = "```\ncode\n```\n## Title"
463          result = adapter.format_message(text)
464          assert "*Title*" in result
465          assert "```\ncode\n```" in result
466  
467      def test_multiple_bold_segments(self, adapter):
468          result = adapter.format_message("**a** and **b** and **c**")
469          assert result.count("*") >= 6  # 3 bold pairs = 6 asterisks
470  
471      def test_special_chars_in_plain_text(self, adapter):
472          result = adapter.format_message("Price: $5.00 (50% off!)")
473          assert "\\." in result
474          assert "\\(" in result
475          assert "\\)" in result
476          assert "\\!" in result
477  
478      def test_empty_bold(self, adapter):
479          """**** (empty bold) should not crash."""
480          result = adapter.format_message("****")
481          assert result is not None
482  
483      def test_empty_code_block(self, adapter):
484          result = adapter.format_message("```\n```")
485          assert "```" in result
486  
487      def test_placeholder_collision(self, adapter):
488          """Many formatting elements should not cause placeholder collisions."""
489          text = (
490              "# Header\n"
491              "**bold1** *italic1* `code1`\n"
492              "**bold2** *italic2* `code2`\n"
493              "```\nblock\n```\n"
494              "[link](https://url.com)"
495          )
496          result = adapter.format_message(text)
497          # No placeholder tokens should leak into output
498          assert "\x00" not in result
499          # All elements should be present
500          assert "Header" in result
501          assert "block" in result
502          assert "url.com" in result
503  
504  
505  # =========================================================================
506  # _strip_mdv2 — plaintext fallback
507  # =========================================================================
508  
509  
510  class TestStripMdv2:
511      def test_removes_escape_backslashes(self):
512          assert _strip_mdv2(r"hello\.world\!") == "hello.world!"
513  
514      def test_removes_bold_markers(self):
515          assert _strip_mdv2("*bold text*") == "bold text"
516  
517      def test_removes_italic_markers(self):
518          assert _strip_mdv2("_italic text_") == "italic text"
519  
520      def test_removes_both_bold_and_italic(self):
521          result = _strip_mdv2("*bold* and _italic_")
522          assert result == "bold and italic"
523  
524      def test_preserves_snake_case(self):
525          assert _strip_mdv2("my_variable_name") == "my_variable_name"
526  
527      def test_preserves_multi_underscore_identifier(self):
528          assert _strip_mdv2("some_func_call here") == "some_func_call here"
529  
530      def test_plain_text_unchanged(self):
531          assert _strip_mdv2("plain text") == "plain text"
532  
533      def test_empty_string(self):
534          assert _strip_mdv2("") == ""
535  
536      def test_removes_strikethrough_markers(self):
537          assert _strip_mdv2("~struck text~") == "struck text"
538  
539      def test_removes_spoiler_markers(self):
540          assert _strip_mdv2("||hidden text||") == "hidden text"
541  
542  
543  # =========================================================================
544  # Markdown table auto-wrap
545  # =========================================================================
546  
547  
548  class TestWrapMarkdownTables:
549      """_wrap_markdown_tables rewrites GFM pipe tables into Telegram-friendly
550      row groups instead of leaving noisy pipe syntax in the final message."""
551  
552      def test_basic_table_rewritten_as_row_groups(self):
553          text = (
554              "Scores:\n\n"
555              "| Player | Score |\n"
556              "|--------|-------|\n"
557              "| Alice  | 150   |\n"
558              "| Bob    | 120   |\n"
559              "\nEnd."
560          )
561          out = _wrap_markdown_tables(text)
562          assert "**Alice**" in out
563          assert "• Player: Alice" in out
564          assert "• Score: 150" in out
565          assert "**Bob**" in out
566          assert "• Score: 120" in out
567          # Surrounding prose is preserved
568          assert out.startswith("Scores:")
569          assert out.endswith("End.")
570  
571      def test_bare_pipe_table_rewritten(self):
572          """Tables without outer pipes (GFM allows this) are still detected."""
573          text = "head1 | head2\n--- | ---\na | b\nc | d"
574          out = _wrap_markdown_tables(text)
575          assert out.startswith("**a**")
576          assert "• head1: a" in out
577          assert "• head2: b" in out
578          assert "**c**" in out
579  
580      def test_alignment_separators(self):
581          """Separator rows with :--- / ---: / :---: alignment markers match."""
582          text = (
583              "| Name | Age | City |\n"
584              "|:-----|----:|:----:|\n"
585              "| Ada  |  30 | NYC  |"
586          )
587          out = _wrap_markdown_tables(text)
588          assert "**Ada**" in out
589          assert "• Age: 30" in out
590          assert "• City: NYC" in out
591  
592      def test_two_consecutive_tables_rewritten_separately(self):
593          text = (
594              "| A | B |\n"
595              "|---|---|\n"
596              "| 1 | 2 |\n"
597              "\n"
598              "| X | Y |\n"
599              "|---|---|\n"
600              "| 9 | 8 |"
601          )
602          out = _wrap_markdown_tables(text)
603          assert out.count("**1**") == 1
604          assert out.count("**9**") == 1
605          assert "• A: 1" in out
606          assert "• X: 9" in out
607  
608      def test_plain_text_with_pipes_not_wrapped(self):
609          """A bare pipe in prose must NOT trigger wrapping."""
610          text = "Use the | pipe operator to chain commands."
611          assert _wrap_markdown_tables(text) == text
612  
613      def test_horizontal_rule_not_wrapped(self):
614          """A lone '---' horizontal rule must not be mistaken for a separator."""
615          text = "Section A\n\n---\n\nSection B"
616          assert _wrap_markdown_tables(text) == text
617  
618      def test_existing_code_block_with_pipes_left_alone(self):
619          """A table already inside a fenced code block must not be re-wrapped."""
620          text = (
621              "```\n"
622              "| a | b |\n"
623              "|---|---|\n"
624              "| 1 | 2 |\n"
625              "```"
626          )
627          assert _wrap_markdown_tables(text) == text
628  
629      def test_no_pipe_character_short_circuits(self):
630          text = "Plain **bold** text with no table."
631          assert _wrap_markdown_tables(text) == text
632  
633      def test_no_dash_short_circuits(self):
634          text = "a | b\nc | d"  # has pipes but no '-' separator row
635          assert _wrap_markdown_tables(text) == text
636  
637      def test_single_column_separator_not_matched(self):
638          """Single-column tables (rare) are not detected — we require at
639          least one internal pipe in the separator row to avoid false
640          positives on formatting rules."""
641          text = "| a |\n| - |\n| b |"
642          assert _wrap_markdown_tables(text) == text
643  
644  
645  class TestFormatMessageTables:
646      """End-to-end: pipe tables become readable Telegram-native text instead
647      of escaped pipe syntax or fenced code blocks."""
648  
649      def test_table_rendered_as_bullets(self, adapter):
650          text = (
651              "Data:\n\n"
652              "| Col1 | Col2 |\n"
653              "|------|------|\n"
654              "| A    | B    |\n"
655          )
656          out = adapter.format_message(text)
657          assert "*A*" in out
658          assert "• Col1: A" in out
659          assert "• Col2: B" in out
660          assert "```" not in out
661          assert "\\|" not in out
662  
663      def test_text_after_table_still_formatted(self, adapter):
664          text = (
665              "| A | B |\n"
666              "|---|---|\n"
667              "| 1 | 2 |\n"
668              "\n"
669              "Nice **work** team!"
670          )
671          out = adapter.format_message(text)
672          # MarkdownV2 bold conversion still happens outside the table
673          assert "*work*" in out
674          # Exclamation outside fence is escaped
675          assert "\\!" in out
676          assert "*1*" in out
677          assert "• A: 1" in out
678  
679      def test_multiple_tables_in_single_message(self, adapter):
680          text = (
681              "First:\n"
682              "| A | B |\n"
683              "|---|---|\n"
684              "| 1 | 2 |\n"
685              "\n"
686              "Second:\n"
687              "| X | Y |\n"
688              "|---|---|\n"
689              "| 9 | 8 |\n"
690          )
691          out = adapter.format_message(text)
692          assert out.count("*1*") == 1
693          assert out.count("*9*") == 1
694          assert "• X: 9" in out
695  
696  
697  @pytest.mark.asyncio
698  async def test_send_escapes_chunk_indicator_for_markdownv2(adapter):
699      adapter.MAX_MESSAGE_LENGTH = 80
700      adapter._bot = MagicMock()
701  
702      sent_texts = []
703  
704      async def _fake_send_message(**kwargs):
705          sent_texts.append(kwargs["text"])
706          msg = MagicMock()
707          msg.message_id = len(sent_texts)
708          return msg
709  
710      adapter._bot.send_message = AsyncMock(side_effect=_fake_send_message)
711  
712      content = ("**bold** chunk content " * 12).strip()
713      result = await adapter.send("123", content)
714  
715      assert result.success is True
716      assert len(sent_texts) > 1
717      assert re.search(r" \\\([0-9]+/[0-9]+\\\)$", sent_texts[0])
718      assert re.search(r" \\\([0-9]+/[0-9]+\\\)$", sent_texts[-1])