diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index dc4b94ead0..ecc90bb969 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -3922,7 +3922,17 @@ class SlackAdapter(BasePlatformAdapter): # (keeps the message intact rather than emptying a mention). display = (name or uid).strip() or uid # Replace both the bare and labelled forms of this exact ID. - text = re.sub(rf"<@{uid}(?:\|[^>]*)?>", f"@{display}", text) + # The replacement goes in as a *function* so the resolved name is + # inserted verbatim: as a template string, ``re`` parses backslash + # escapes in it, and a display name is arbitrary user-set text. + # ``dev\ops`` raises ``re.error: bad escape \o``, ``a\1b`` raises + # on the group reference, and ``\g<0>`` silently re-injects the + # raw ``<@UID>`` this method exists to remove. + text = re.sub( + rf"<@{uid}(?:\|[^>]*)?>", + lambda _m, _name=f"@{display}": _name, + text, + ) return text def _build_identity_prompt(self, team_id: str = "") -> str: diff --git a/tests/gateway/test_slack_mention_humanization.py b/tests/gateway/test_slack_mention_humanization.py index f08b127880..f77da13d26 100644 --- a/tests/gateway/test_slack_mention_humanization.py +++ b/tests/gateway/test_slack_mention_humanization.py @@ -95,6 +95,46 @@ async def test_handles_labelled_mention_form(): assert out == "@Alice Example hi" +@pytest.mark.asyncio +async def test_backslash_in_display_name_does_not_raise(): + """A display name is arbitrary user-set text. Fed to ``re.sub`` as a + replacement template it is parsed for escapes, so ``dev\\ops`` blew up + with ``re.error: bad escape \\o`` and the inbound message was lost.""" + adapter = _adapter_with_names({"U07DEV": r"dev\ops"}) + out = await adapter._humanize_user_mentions("ping <@U07DEV> please", chat_id="C1") + assert out == r"ping @dev\ops please" + + +@pytest.mark.asyncio +async def test_group_reference_in_display_name_is_literal(): + """``\\1`` in a name is a group reference in a replacement template — with + no groups in the pattern it raised ``invalid group reference``.""" + adapter = _adapter_with_names({"U07ODD": r"a\1b"}) + out = await adapter._humanize_user_mentions("hi <@U07ODD>", chat_id="C1") + assert out == r"hi @a\1b" + + +@pytest.mark.asyncio +async def test_named_group_reference_does_not_reinject_the_raw_id(): + """``\\g<0>`` expands to the whole match, silently putting the opaque + ``<@UID>`` back — the exact token this method exists to remove.""" + adapter = _adapter_with_names({"U07ODD": r"\g<0>"}) + out = await adapter._humanize_user_mentions("hi <@U07ODD>", chat_id="C1") + assert "<@" not in out + assert out == r"hi @\g<0>" + + +@pytest.mark.asyncio +async def test_one_odd_name_does_not_break_the_other_mentions(): + adapter = _adapter_with_names( + {"U07DEV": r"dev\ops", "U07ALICE": "Alice Example"} + ) + out = await adapter._humanize_user_mentions( + "<@U07DEV> and <@U07ALICE> ship it", chat_id="C1" + ) + assert out == r"@dev\ops and @Alice Example ship it" + + # ----- _build_identity_prompt -------------------------------------------------- def test_identity_prompt_names_the_bot():