Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions tests/tools/test_send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,82 @@ def test_room_id_is_percent_encoded_in_url(self):
assert "!HLOQwxYGgFPMPJUSNR:matrix.org" not in put_url


class TestSendMatrixFormattedBody:
"""_send_matrix renders markdown to formatted_body in the cron delivery
path, with a regex fallback when the optional ``markdown`` library is
not installed (default Docker image case — issue #32486)."""

@staticmethod
def _build_session_mock():
mock_resp = MagicMock()
mock_resp.status = 200
mock_resp.json = AsyncMock(return_value={"event_id": "$evt"})
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
mock_resp.__aexit__ = AsyncMock(return_value=None)

mock_session = MagicMock()
mock_session.put = MagicMock(return_value=mock_resp)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=None)
return mock_session

def test_markdown_lib_path_sets_formatted_body(self):
"""When ``markdown`` is installed, the rendered HTML lands in
``formatted_body`` and ``format`` is set to the Matrix custom-HTML
content type."""
pytest.importorskip("markdown")
from tools.send_message_tool import _send_matrix

mock_session = self._build_session_mock()
with patch("aiohttp.ClientSession", return_value=mock_session):
result = asyncio.run(
_send_matrix(
"tok",
{"homeserver": "https://matrix.example.org"},
"!room:matrix.org",
"## Heading\n\n- bullet\n- bullet",
)
)

assert result["success"] is True
payload = mock_session.put.call_args.kwargs["json"]
assert payload["format"] == "org.matrix.custom.html"
# Element X compatibility: headings collapse to <strong>, not <h1-6>.
assert "<strong>Heading</strong>" in payload["formatted_body"]
assert "<h1>" not in payload["formatted_body"]
# List rendering survives.
assert "<li>bullet</li>" in payload["formatted_body"]

def test_fallback_path_sets_formatted_body_without_markdown_lib(self):
"""When ``markdown`` is missing (default Docker image), the regex
fallback shared with ``MatrixAdapter`` still produces HTML — the
client no longer sees raw ``##`` / ``-`` / ``|...|`` source."""
from tools.send_message_tool import _send_matrix

mock_session = self._build_session_mock()
# Hide the markdown library to force the ImportError branch.
with patch.dict(sys.modules, {"markdown": None}):
Comment on lines +1601 to +1602
with patch("aiohttp.ClientSession", return_value=mock_session):
result = asyncio.run(
_send_matrix(
"tok",
{"homeserver": "https://matrix.example.org"},
"!room:matrix.org",
"## Heading\n\n- bullet\n- bullet",
)
)

assert result["success"] is True
payload = mock_session.put.call_args.kwargs["json"]
assert payload["format"] == "org.matrix.custom.html"
# Element X compatibility carries through the fallback too.
assert "<strong>Heading</strong>" in payload["formatted_body"]
assert "<h1>" not in payload["formatted_body"]
# Raw markdown source must NOT survive into formatted_body.
assert "## Heading" not in payload["formatted_body"]
assert "<li>bullet</li>" in payload["formatted_body"]


# ---------------------------------------------------------------------------
# Tests for _derive_forum_thread_name
# ---------------------------------------------------------------------------
Expand Down
19 changes: 13 additions & 6 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,17 +1375,24 @@ async def _send_matrix(token, extra, chat_id, message):
url = f"{homeserver}/_matrix/client/v3/rooms/{encoded_room}/send/m.room.message/{txn_id}"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

# Build message payload with optional HTML formatted_body.
# Build message payload with HTML formatted_body. Prefer the
# ``markdown`` library (ships with the ``[matrix]`` extra); fall
# back to the regex converter shared with the gateway adapter so
# cron deliveries still get formatted_body when the lib isn't
# installed. The official Docker image installs ``[all]`` +
# ``[messaging]`` only — neither pulls Markdown, so this branch
# is the default path inside the container.
payload = {"msgtype": "m.text", "body": message}
try:
import markdown as _md
html = _md.markdown(message, extensions=["fenced_code", "tables"])
# Convert h1-h6 to bold for Element X compatibility.
html = re.sub(r"<h[1-6]>(.*?)</h[1-6]>", r"<strong>\1</strong>", html)
payload["format"] = "org.matrix.custom.html"
payload["formatted_body"] = html
except ImportError:
pass
from gateway.platforms.matrix import MatrixAdapter
html = MatrixAdapter._markdown_to_html_fallback(message)
Comment on lines +1390 to +1391
# Convert h1-h6 to bold for Element X compatibility.
html = re.sub(r"<h[1-6]>(.*?)</h[1-6]>", r"<strong>\1</strong>", html)
payload["format"] = "org.matrix.custom.html"
payload["formatted_body"] = html

async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
async with session.put(url, headers=headers, json=payload) as resp:
Expand Down
Loading