fix(gateway): strip language tag from Slack fenced code blocks

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
5600105478.

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.
This commit is contained in:
z23 2026-07-15 20:54:36 +08:00 committed by Teknium
parent 8e6d1a9a53
commit b810711e4e
2 changed files with 74 additions and 5 deletions

View file

@ -3219,10 +3219,25 @@ class SlackAdapter(BasePlatformAdapter):
lambda m: m.group(0).replace("<", "&lt;", 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,
)

View file

@ -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):