diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..72521e0
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,12 @@
+"""Shared pytest configuration.
+
+Ensures the repository root is on sys.path so tests can import the ``src``
+package regardless of the working directory pytest is invoked from.
+"""
+
+import os
+import sys
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if REPO_ROOT not in sys.path:
+ sys.path.insert(0, REPO_ROOT)
diff --git a/tests/test_matrix_formatting.py b/tests/test_matrix_formatting.py
new file mode 100644
index 0000000..ed1c082
--- /dev/null
+++ b/tests/test_matrix_formatting.py
@@ -0,0 +1,98 @@
+"""Assert-based tests for Matrix message formatting.
+
+Matrix clients only render formatting from an HTML ``formatted_body``; Markdown
+in the plain ``body`` shows as literal characters. These tests lock in that the
+notifier emits real HTML plus a clean plain-text fallback, and that untrusted
+station data is HTML-escaped.
+"""
+
+import datetime
+
+from src.alerting import AlertLevel, MatrixNotifier, WaterAlert, markdown_to_matrix_html, strip_markdown
+
+
+class _FakeResponse:
+ def raise_for_status(self):
+ pass
+
+ def json(self):
+ return {"event_id": "$test"}
+
+
+def _notifier_capturing(captured):
+ """A MatrixNotifier whose HTTP PUT records the JSON payload into ``captured``."""
+ notifier = MatrixNotifier("https://hs.example", "token", "!room:hs.example")
+
+ def fake_put(url, headers=None, json=None, timeout=None):
+ captured.update(json)
+ return _FakeResponse()
+
+ notifier.session.put = fake_put
+ return notifier
+
+
+def test_bold_becomes_strong():
+ assert markdown_to_matrix_html("**hi**") == "hi"
+
+
+def test_url_is_linkified():
+ out = markdown_to_matrix_html("see https://x.example/z")
+ assert 'https://x.example/z' in out
+
+
+def test_newlines_become_br():
+ assert markdown_to_matrix_html("a\nb") == "a
b"
+
+
+def test_html_is_escaped():
+ out = markdown_to_matrix_html("