mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
8e6d1a9a53
commit
b810711e4e
2 changed files with 74 additions and 5 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue