""" Tests for services/matrix-bridge-dagi/app/ingress.py Strategy: - mock MatrixClient.sync_poll and extract_room_messages - mock httpx gateway client - verify gateway is invoked once per unique message (dedupe works) - verify stop_event terminates loop """ import asyncio import sys from pathlib import Path from typing import Any, Dict, List from unittest.mock import AsyncMock, MagicMock, patch, call import pytest _BRIDGE = Path(__file__).parent.parent / "services" / "matrix-bridge-dagi" if str(_BRIDGE) not in sys.path: sys.path.insert(0, str(_BRIDGE)) from app.ingress import MatrixIngressLoop, _invoke_gateway # noqa: E402 from app.room_mapping import parse_room_map # noqa: E402 def run(coro): return asyncio.run(coro) ALLOWED = frozenset({"sofiia"}) ROOM_ID = "!QwHczWXgefDHBEVkTH:daarion.space" ROOM_MAP_STR = f"sofiia:{ROOM_ID}" GW_URL = "http://127.0.0.1:9300" HS_URL = "http://localhost:8008" TOKEN = "syt_fake_token" BOT_USER = "@dagi_bridge:daarion.space" MOCK_EVENT_1 = { "type": "m.room.message", "event_id": "$event1:server", "sender": "@user:server", "content": {"msgtype": "m.text", "body": "Hello Sofiia!"}, "origin_server_ts": 1000, } MOCK_EVENT_2 = { "type": "m.room.message", "event_id": "$event2:server", "sender": "@user:server", "content": {"msgtype": "m.text", "body": "Another message"}, "origin_server_ts": 2000, } def _make_loop(**kwargs) -> MatrixIngressLoop: room_map = parse_room_map(ROOM_MAP_STR, ALLOWED) defaults = dict( matrix_homeserver_url=HS_URL, matrix_access_token=TOKEN, matrix_user_id=BOT_USER, gateway_url=GW_URL, node_id="NODA1", room_map=room_map, ) defaults.update(kwargs) return MatrixIngressLoop(**defaults) def _fake_sync_resp(events: list) -> dict: return { "next_batch": "s_next", "rooms": { "join": { ROOM_ID: { "timeline": {"events": events} } } } } # ── _invoke_gateway ───────────────────────────────────────────────────────── def test_invoke_gateway_builds_correct_request(): async def _inner(): captured = {} async def fake_post(url, *, json=None, timeout=None): captured["url"] = url captured["json"] = json resp = MagicMock() resp.status_code = 200 resp.json.return_value = {"ok": True} resp.raise_for_status = MagicMock() return resp client = MagicMock() client.post = fake_post result = await _invoke_gateway( client, GW_URL, "sofiia", "NODA1", "Hello!", ROOM_ID, "$event1", "@user:server" ) assert "/v1/invoke" in captured["url"] payload = captured["json"] assert payload["agent_id"] == "sofiia" assert payload["node_id"] == "NODA1" assert payload["message"] == "Hello!" meta = payload["metadata"] assert meta["transport"] == "matrix" assert meta["matrix_room_id"] == ROOM_ID assert meta["matrix_event_id"] == "$event1" assert meta["matrix_sender"] == "@user:server" run(_inner()) # ── Ingress loop — normal flow ────────────────────────────────────────────── def test_ingress_loop_invokes_gateway_once_per_message(): async def _inner(): received_calls: List[Dict] = [] async def fake_post(url, *, json=None, timeout=None): received_calls.append({"url": url, "json": json}) resp = MagicMock() resp.status_code = 200 resp.json.return_value = {"ok": True} resp.raise_for_status = MagicMock() return resp stop = asyncio.Event() loop = _make_loop() sync_responses = [ _fake_sync_resp([MOCK_EVENT_1]), _fake_sync_resp([]), # empty on second poll ] call_count = [0] async def fake_sync_poll(**kwargs): idx = call_count[0] call_count[0] += 1 if idx >= len(sync_responses): stop.set() return {"next_batch": "end", "rooms": {}} return sync_responses[idx] with patch("app.ingress.MatrixClient") as MockClient: mock_mc_instance = AsyncMock() mock_mc_instance.__aenter__ = AsyncMock(return_value=mock_mc_instance) mock_mc_instance.__aexit__ = AsyncMock(return_value=False) mock_mc_instance.sync_poll = fake_sync_poll mock_mc_instance.join_room = AsyncMock() mock_mc_instance.mark_seen = MagicMock() mock_mc_instance.is_duplicate = MagicMock(return_value=False) def fake_extract(sync_resp, room_id): events = sync_resp.get("rooms", {}).get("join", {}).get(room_id, {}).get("timeline", {}).get("events", []) return [e for e in events if e.get("type") == "m.room.message" and e.get("sender") != BOT_USER] mock_mc_instance.extract_room_messages = fake_extract MockClient.return_value = mock_mc_instance with patch("app.ingress.httpx.AsyncClient") as MockHTTP: mock_http = AsyncMock() mock_http.__aenter__ = AsyncMock(return_value=mock_http) mock_http.__aexit__ = AsyncMock(return_value=False) mock_http.post = fake_post MockHTTP.return_value = mock_http await asyncio.wait_for(loop.run(stop), timeout=3.0) # Gateway should have been invoked exactly once (for MOCK_EVENT_1) assert len(received_calls) == 1 assert received_calls[0]["json"]["message"] == "Hello Sofiia!" run(_inner()) def test_ingress_loop_deduplicates_same_event(): """Same event_id appearing twice should only invoke gateway once.""" async def _inner(): invoke_count = [0] async def fake_post(url, *, json=None, timeout=None): invoke_count[0] += 1 resp = MagicMock() resp.status_code = 200 resp.json.return_value = {"ok": True} resp.raise_for_status = MagicMock() return resp stop = asyncio.Event() loop = _make_loop() # Same event in two consecutive syncs sync_responses = [ _fake_sync_resp([MOCK_EVENT_1]), _fake_sync_resp([MOCK_EVENT_1]), # duplicate event_id ] call_count = [0] seen = set() async def fake_sync_poll(**kwargs): idx = call_count[0] call_count[0] += 1 if idx >= len(sync_responses): stop.set() return {"next_batch": "end", "rooms": {}} return sync_responses[idx] with patch("app.ingress.MatrixClient") as MockClient: mock_mc_instance = AsyncMock() mock_mc_instance.__aenter__ = AsyncMock(return_value=mock_mc_instance) mock_mc_instance.__aexit__ = AsyncMock(return_value=False) mock_mc_instance.sync_poll = fake_sync_poll mock_mc_instance.join_room = AsyncMock() def fake_mark_seen(event_id): seen.add(event_id) def fake_is_dup(event_id): return event_id in seen mock_mc_instance.mark_seen = fake_mark_seen mock_mc_instance.is_duplicate = fake_is_dup def fake_extract(sync_resp, room_id): events = sync_resp.get("rooms", {}).get("join", {}).get(room_id, {}).get("timeline", {}).get("events", []) return [e for e in events if e.get("type") == "m.room.message" and e.get("sender") != BOT_USER and not fake_is_dup(e.get("event_id", ""))] mock_mc_instance.extract_room_messages = fake_extract MockClient.return_value = mock_mc_instance with patch("app.ingress.httpx.AsyncClient") as MockHTTP: mock_http = AsyncMock() mock_http.__aenter__ = AsyncMock(return_value=mock_http) mock_http.__aexit__ = AsyncMock(return_value=False) mock_http.post = fake_post MockHTTP.return_value = mock_http await asyncio.wait_for(loop.run(stop), timeout=3.0) # Dedupe: only 1 invoke despite 2 sync responses with same event assert invoke_count[0] == 1 run(_inner()) def test_ingress_loop_calls_metric_callbacks(): """on_message_received and on_gateway_error callbacks should fire.""" async def _inner(): received_events = [] error_events = [] stop = asyncio.Event() loop = _make_loop( on_message_received=lambda room, agent: received_events.append((room, agent)), on_gateway_error=lambda etype: error_events.append(etype), ) call_count = [0] async def fake_sync_poll(**kwargs): call_count[0] += 1 if call_count[0] > 1: stop.set() return {"next_batch": "end", "rooms": {}} return _fake_sync_resp([MOCK_EVENT_1]) with patch("app.ingress.MatrixClient") as MockClient: mock_mc = AsyncMock() mock_mc.__aenter__ = AsyncMock(return_value=mock_mc) mock_mc.__aexit__ = AsyncMock(return_value=False) mock_mc.sync_poll = fake_sync_poll mock_mc.join_room = AsyncMock() mock_mc.mark_seen = MagicMock() mock_mc.is_duplicate = MagicMock(return_value=False) def fake_extract(sync_resp, room_id): events = sync_resp.get("rooms", {}).get("join", {}).get(room_id, {}).get("timeline", {}).get("events", []) return [e for e in events if e.get("type") == "m.room.message" and e.get("sender") != BOT_USER] mock_mc.extract_room_messages = fake_extract MockClient.return_value = mock_mc with patch("app.ingress.httpx.AsyncClient") as MockHTTP: mock_http = AsyncMock() mock_http.__aenter__ = AsyncMock(return_value=mock_http) mock_http.__aexit__ = AsyncMock(return_value=False) async def fake_post(url, *, json=None, timeout=None): resp = MagicMock() resp.status_code = 200 resp.json.return_value = {"ok": True} resp.raise_for_status = MagicMock() return resp mock_http.post = fake_post MockHTTP.return_value = mock_http await asyncio.wait_for(loop.run(stop), timeout=3.0) assert len(received_events) == 1 assert received_events[0] == (ROOM_ID, "sofiia") run(_inner()) def test_ingress_loop_no_mappings_is_idle(): """Loop with 0 mappings should start and stop cleanly without invoking gateway.""" async def _inner(): empty_map = parse_room_map("", ALLOWED) loop = MatrixIngressLoop( matrix_homeserver_url=HS_URL, matrix_access_token=TOKEN, matrix_user_id=BOT_USER, gateway_url=GW_URL, node_id="NODA1", room_map=empty_map, ) stop = asyncio.Event() with patch("app.ingress.MatrixClient") as MockClient: mock_mc = AsyncMock() mock_mc.__aenter__ = AsyncMock(return_value=mock_mc) mock_mc.__aexit__ = AsyncMock(return_value=False) async def fake_sync_poll(**kwargs): stop.set() return {"next_batch": "end", "rooms": {}} mock_mc.sync_poll = fake_sync_poll MockClient.return_value = mock_mc with patch("app.ingress.httpx.AsyncClient") as MockHTTP: mock_http = AsyncMock() mock_http.__aenter__ = AsyncMock(return_value=mock_http) mock_http.__aexit__ = AsyncMock(return_value=False) MockHTTP.return_value = mock_http await asyncio.wait_for(loop.run(stop), timeout=3.0) # Should complete without error assert True run(_inner())