From b810711e4e463bd203d4529849843081855ef2f0 Mon Sep 17 00:00:00 2001 From: z23 Date: Wed, 15 Jul 2026 20:54:36 +0800 Subject: [PATCH] fix(gateway): strip language tag from Slack fenced code blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slack's mrkdwn does not strip the optional language tag from fenced code blocks like GitHub-flavored markdown does — it renders ```text\nfoo\n``` as a code block whose literal first line is "text". The agent emitted ```text fences around raw command output, which surfaced "text" as the first line of every such block. Drop the tag from the opening fence in format_message() before stashing the block behind a placeholder. Stripping only fires for a genuine opening fence — a ``` at the start of a line, tagged with a single token (no spaces or backticks) — and the original line ending is preserved. The fence-protection regex deliberately matches loosely, so a mid-line ``` (e.g. an inline ```span``` wrapping across a newline) can be grouped as an "opening fence" whose first line is real content; differential fuzzing against the pre-change formatter (40k generated messages) confirms the only behavioral delta is the tag strip itself. The Block Kit renderer is unaffected: render_blocks() intercepts fences itself before mrkdwn_fn is applied, so this only changes the mrkdwn surfaces that still go through format_message() — the plain-text fallback field (notifications, search indexing, accessibility), slash-command ephemeral replies, and standalone cron delivery. Originally written against gateway/platforms/slack.py; ported to plugins/platforms/slack/adapter.py after the adapter migration in 5600105478ffde29d7566b45421b100eaa29c4ef. Manually verified against a live Slack workspace (pre-migration adapter; the ```text case strips identically) — code blocks no longer carry a literal "text" first line. --- plugins/platforms/slack/adapter.py | 19 +++++++++- tests/gateway/test_slack.py | 60 ++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 67b30864812..6b2c0034baa 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -3219,10 +3219,25 @@ class SlackAdapter(BasePlatformAdapter): lambda m: m.group(0).replace("<", "<", 1), text ) - # 1) Protect fenced code blocks (``` ... ```) + # 1) Protect fenced code blocks (``` ... ```). Slack's mrkdwn does not + # strip the optional language tag like GitHub-flavored markdown — it + # renders ```text\nfoo\n``` as a code block whose literal first line + # is "text". Drop the tag from the opening fence before stashing. + # Stripping only fires for a genuine opening fence — a ``` at the + # start of a line, tagged with a single token (no spaces/backticks). + # The outer regex below deliberately matches loosely, so it can also + # group from a mid-line ``` (e.g. an inline ```span```); that first + # line is real content and must survive byte-for-byte. This pass + # runs first, so match positions refer to the original message. + def _protect_fence(m): + block = m.group(0) + if m.start() == 0 or m.string[m.start() - 1] == "\n": + block = re.sub(r"\A```[^\s`]+[ \t]*(\r?\n)", r"```\1", block) + return _ph(block) + text = re.sub( r"(```(?:[^\n]*\n)?[\s\S]*?```)", - lambda m: _ph(m.group(0)), + _protect_fence, text, ) diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index f601cf63e25..13a1fba3d8f 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -3979,7 +3979,60 @@ class TestFormatMessage: assert adapter.format_message("~~deleted~~") == "~deleted~" def test_code_block_preserved(self, adapter): + # Slack mrkdwn doesn't recognize language tags — it would render the + # tag as a literal first line of the code block — so the converter + # strips it. Body content is still passed through verbatim. code = "```python\nx = **not bold**\n```" + assert adapter.format_message(code) == "```\nx = **not bold**\n```" + + def test_code_block_strips_language_tag(self, adapter): + # Regression: Slack rendered a literal "text" line at the top of code + # blocks containing raw command output because the LLM emitted + # ```text fences and the converter passed them through unchanged. + code = "```text\nhello world\nline 2\n```" + assert adapter.format_message(code) == "```\nhello world\nline 2\n```" + + def test_code_block_no_language_tag_unchanged(self, adapter): + code = "```\nplain output\n```" + assert adapter.format_message(code) == code + + def test_inline_triple_backtick_unchanged(self, adapter): + # Single-line ```hello``` has no newline after the opening fence, so + # nothing should be stripped. + code = "```hello```" + assert adapter.format_message(code) == code + + def test_mid_line_triple_backticks_content_preserved(self, adapter): + # The fence-protection regex matches loosely, so the inline + # ```pip install foo``` span is grouped as an "opening fence" whose + # first line is real content. Stripping only fires for a ``` at the + # start of a line, so the span survives byte-for-byte. + text = "Use ```pip install foo``` then:\n```bash\ncode\n```" + assert adapter.format_message(text) == text + + def test_mid_line_single_token_span_preserved(self, adapter): + # A single-token inline span that wraps across a newline looks + # exactly like a language tag — the line-start guard is what keeps + # the word "quotes" from being stripped as one. + text = "Wrap it in ```quotes\nlike this\n```" + assert adapter.format_message(text) == text + + def test_back_to_back_fences_second_token_preserved(self, adapter): + # The second ``` group starts mid-line (right after the previous + # closing fence), so its first token is content, not a tag. + text = "```\nx\n``````b\ny\n```" + assert adapter.format_message(text) == text + + def test_code_block_lang_tag_trailing_spaces_stripped(self, adapter): + code = "```python \nx = 1\n```" + assert adapter.format_message(code) == "```\nx = 1\n```" + + def test_code_block_crlf_lang_tag_stripped_preserves_crlf(self, adapter): + code = "```python\r\nx = 1\r\n```" + assert adapter.format_message(code) == "```\r\nx = 1\r\n```" + + def test_code_block_crlf_no_tag_unchanged(self, adapter): + code = "```\r\nplain output\r\n```" assert adapter.format_message(code) == code def test_inline_code_preserved(self, adapter): @@ -4143,9 +4196,9 @@ class TestFormatMessage: # --- Additional edge cases --- def test_message_only_code_block(self, adapter): - """Entire message is a fenced code block — no conversion.""" + """Entire message is a fenced code block — body preserved, lang tag dropped.""" code = "```python\nx = 1\n```" - assert adapter.format_message(code) == code + assert adapter.format_message(code) == "```\nx = 1\n```" def test_multiline_mixed_formatting(self, adapter): """Multi-line message with headers, bold, links, code, and blockquotes.""" @@ -4351,7 +4404,8 @@ class TestEditMessageStreamingPipeline: assert result.success is True kwargs = adapter._app.client.chat_update.call_args.kwargs assert kwargs["text"].startswith("*Result:*") - assert "```python\nprint('hello')\n```" in kwargs["text"] + # Language tag is stripped — Slack mrkdwn would render it as a literal line + assert "```\nprint('hello')\n```" in kwargs["text"] @pytest.mark.asyncio async def test_edit_message_formats_blockquote_in_stream(self, adapter):