From e1877758d5de3b779bf345b4a3214ec7bb9cdad5 Mon Sep 17 00:00:00 2001 From: tony-chinchill Date: Wed, 13 May 2026 17:52:42 -0700 Subject: [PATCH 1/2] fix(slack): skip @ inside email localparts when wrapping mentions `SlackFormatConverter._convert_mentions_to_slack` and the AST text branch in `_node_to_mrkdwn` rewrote `alice@example.com` to `alice<@example>.com` because `(? `Hey <@alice>` - `@alice at start` -> `<@alice> at start` Emails pass through: - `Contact alice@example.com` -> `Contact alice@example.com` Two regression tests in `tests/test_slack_format.py` cover the str path and the AST path; existing mention tests untouched. --- src/chat_sdk/adapters/slack/adapter.py | 2 +- src/chat_sdk/adapters/slack/format_converter.py | 4 ++-- tests/test_slack_format.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index 1958c59..d7e4c36 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -1651,7 +1651,7 @@ async def _resolve_outgoing_mentions(self, text: str, thread_id: str) -> str: return text state = self._chat.get_state() - mention_pattern = re.compile(r"@(\w+)") + mention_pattern = re.compile(r"(? None: def _convert_mentions_to_slack(self, text: str) -> str: """Convert @mentions to Slack format: @name -> <@name>.""" - return re.sub(r"(?", text) + return re.sub(r"(?", text) def _node_to_mrkdwn(self, node: Content) -> str: """Convert a single AST node to Slack mrkdwn.""" @@ -215,7 +215,7 @@ def _node_to_mrkdwn(self, node: Content) -> str: if node_type == "text": value = node.get("value", "") - return re.sub(r"(?", value) + return re.sub(r"(?", value) if node_type == "strong": content = "".join(self._node_to_mrkdwn(c) for c in children) diff --git a/tests/test_slack_format.py b/tests/test_slack_format.py index 6608062..39dc26e 100644 --- a/tests/test_slack_format.py +++ b/tests/test_slack_format.py @@ -199,6 +199,23 @@ def test_from_markdown_no_double_wrap(self): result = self.converter.from_markdown("Hey <@U12345>") assert result == "Hey <@U12345>" + def test_does_not_wrap_at_in_email_localpart(self): + """`@` preceded by a word char is part of an email address, not a mention. + + Without the lookbehind tightening, `alice@example.com` was rewritten to + `alice<@example>.com`, surfacing a broken-looking mention in messages + that quote email addresses from upstream APIs. + """ + result = self.converter.render_postable("Contact alice@example.com or bob@example.org") + assert "<@example>" not in result + assert "alice@example.com" in result + assert "bob@example.org" in result + + def test_does_not_wrap_at_in_email_localpart_from_markdown_ast(self): + result = self.converter.render_postable({"markdown": "Contact alice@example.com"}) + assert "<@example>" not in result + assert "alice@example.com" in result + # --------------------------------------------------------------------------- # toPlainText From f74d6214a44bfa34c294332b2d0cf24cc88136af Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Fri, 15 May 2026 13:20:08 -0700 Subject: [PATCH 2/2] Update src/chat_sdk/adapters/slack/adapter.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: patrick-chinchill --- src/chat_sdk/adapters/slack/adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index d7e4c36..918240a 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -1651,7 +1651,7 @@ async def _resolve_outgoing_mentions(self, text: str, thread_id: str) -> str: return text state = self._chat.get_state() - mention_pattern = re.compile(r"(?