feat(gateway): private notice delivery and Slack format_message fixes

Adds platform-level private notice delivery abstraction so operational
messages (e.g. sethome prompt) can be sent ephemerally on Slack when
configured with `slack.notice_delivery: private`.

Changes:
- gateway/config.py: _normalize_notice_delivery() + GatewayConfig.get_notice_delivery()
  with per-platform config bridging
- gateway/platforms/base.py: send_private_notice() default implementation
  (falls through to send())
- gateway/platforms/slack.py: send_private_notice() via chat_postEphemeral
- gateway/run.py: _deliver_platform_notice() helper replaces direct
  adapter.send() for the sethome notice, with private→public fallback
- gateway/platforms/slack.py: app_mention handler now forwards to
  _handle_slack_message (safe due to ts-based dedup) instead of no-op pass,
  fixing edge-case Slack configs where mentions arrive only as app_mention
- gateway/platforms/slack.py format_message: negative lookbehind prevents
  markdown images (![]()) from becoming broken Slack links; italic regex
  now requires non-whitespace boundaries so 'a * b * c' stays literal

Based on PR #9340 by @probepark.
This commit is contained in:
probepark 2026-05-01 09:07:39 +05:30 committed by kshitij
parent 7cda0e5224
commit 0ab2d752ff
7 changed files with 269 additions and 25 deletions

View file

@ -518,6 +518,28 @@ class TestSendDocument:
sleep_mock.assert_awaited_once()
class TestSendPrivateNotice:
@pytest.mark.asyncio
async def test_send_private_notice_uses_ephemeral_api(self, adapter):
adapter._app.client.chat_postEphemeral = AsyncMock(return_value={"message_ts": "123.456"})
result = await adapter.send_private_notice(
chat_id="C123",
user_id="U123",
content="private hello",
metadata={"thread_id": "1234567890.123456"},
)
assert result.success
adapter._app.client.chat_postEphemeral.assert_called_once_with(
channel="C123",
user="U123",
text="private hello",
mrkdwn=True,
thread_ts="1234567890.123456",
)
# ---------------------------------------------------------------------------
# TestSendVideo
# ---------------------------------------------------------------------------
@ -1315,6 +1337,16 @@ class TestFormatMessage:
result = adapter.format_message("[link](https://x.com?a=1&b=2)")
assert result == "<https://x.com?a=1&b=2|link>"
def test_markdown_image_does_not_create_broken_slack_link(self, adapter):
"""Markdown image syntax should not become '!<url|alt>' in Slack."""
result = adapter.format_message("![alt](https://img.example.com/cat.png)")
assert result == "![alt](https://img.example.com/cat.png)"
def test_literal_asterisks_with_spaces_are_not_treated_as_italic(self, adapter):
"""Asterisks used as plain delimiters should stay literal."""
result = adapter.format_message("a * b * c")
assert result == "a * b * c"
def test_emoji_shortcodes_passthrough(self, adapter):
"""Emoji shortcodes like :smile: pass through unchanged."""
assert adapter.format_message(":smile: hello :wave:") == ":smile: hello :wave:"