mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(slack): stop double-decoding HTML entities when escaping message text
format_message unescapes already-escaped input before re-escaping, so that
pre-escaped text doesn't get double-escaped. That unescape was three
sequential str.replace calls, which re-scan each other's output:
"&lt;" --(& -> &)--> "<" --(< -> <)--> "<"
The & produced by the first replace pairs with the following "lt;" and
decodes a second time. "&lt;" is the wire form of the literal text
"<", so the text is silently destroyed: Slack receives "<" and renders
"<". Anyone writing about HTML or markup ("&lt;b&gt;" -> "<b>")
loses their literal text, with no error.
re.sub scans left-to-right and never re-scans its own replacements, so a
single pass fixes it. The escape pass on the next line is left untouched --
it is correctly ordered (& first, so the &s it inserts aren't re-escaped).
Only the double-decode cases change; every other input is byte-identical
before and after. This is the same round-trip invariant the neighbouring
test_pre_escaped_{ampersand,lt,gt}_not_double_escaped tests already assert,
extended to the case they miss. Affects the plain mrkdwn path (send,
edit_message) and Block Kit sections, which route section text through
format_message.
This commit is contained in:
parent
50a6dc7efc
commit
8e6d1a9a53
2 changed files with 18 additions and 1 deletions
|
|
@ -3256,7 +3256,14 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
|
||||
# 6) Escape Slack control characters in remaining plain text.
|
||||
# Unescape first so already-escaped input doesn't get double-escaped.
|
||||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
# Single pass: sequential str.replace would re-scan its own output, so
|
||||
# the & from "&" could pair with a following "lt;" and decode twice
|
||||
# ("&lt;" → "<" → "<"), destroying literal entity text.
|
||||
text = re.sub(
|
||||
r"&(amp|lt|gt);",
|
||||
lambda m: {"amp": "&", "lt": "<", "gt": ">"}[m.group(1)],
|
||||
text,
|
||||
)
|
||||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
# 7) Convert headers (## Title) → *Title* (bold)
|
||||
|
|
|
|||
|
|
@ -4068,6 +4068,16 @@ class TestFormatMessage:
|
|||
"""Already-escaped > in plain text must not become &gt;."""
|
||||
assert adapter.format_message("5 > 3") == "5 > 3"
|
||||
|
||||
def test_escaped_entity_text_not_double_decoded(self, adapter):
|
||||
"""&lt; is the wire form of the literal text < — it must survive.
|
||||
|
||||
The unescape pass must not re-scan its own output: decoding & to &
|
||||
first must not let the resulting & combine with a following lt; into a
|
||||
second decode, or the literal text is silently destroyed.
|
||||
"""
|
||||
assert adapter.format_message("&lt;") == "&lt;"
|
||||
assert adapter.format_message("&gt;") == "&gt;"
|
||||
|
||||
def test_mixed_raw_and_escaped_entities(self, adapter):
|
||||
"""Raw & and pre-escaped & coexist correctly."""
|
||||
result = adapter.format_message("AT&T and & entity")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue