PR-M1.2 — room-to-agent mapping: - adds room_mapping.py: parse BRIDGE_ROOM_MAP (format: agent:!room_id:server) - RoomMappingConfig with O(1) room→agent lookup, agent allowlist check - /bridge/mappings endpoint (read-only ops summary, no secrets) - health endpoint now includes mappings_count - 21 tests for parsing, validation, allowlist, summary PR-M1.3 — Matrix ingress loop: - adds ingress.py: MatrixIngressLoop asyncio task - sync_poll → extract → dedupe → _invoke_gateway (POST /v1/invoke) - gateway payload: agent_id, node_id, message, metadata (transport, room_id, event_id, sender) - exponential backoff on errors (2s..60s) - joins all mapped rooms at startup - metric callbacks: on_message_received, on_gateway_error - graceful shutdown via asyncio.Event - 5 ingress tests (invoke, dedupe, callbacks, empty-map idle) Synapse setup (docker-compose.synapse-node1.yml): - fixed volume: bind mount ./synapse-data instead of named volume - added port mapping 127.0.0.1:8008:8008 Synapse running on NODA1 (localhost:8008), bot @dagi_bridge:daarion.space created, room !QwHczWXgefDHBEVkTH:daarion.space created, all 4 values in .env on NODA1. Made-with: Cursor
152 lines
5.1 KiB
Python
152 lines
5.1 KiB
Python
"""
|
|
Tests for services/matrix-bridge-dagi/app/room_mapping.py
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
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.room_mapping import RoomMappingConfig, parse_room_map, RoomMapping # noqa: E402
|
|
|
|
ALLOWED = frozenset({"sofiia", "druid"})
|
|
ROOM1 = "!QwHczWXgefDHBEVkTH:daarion.space"
|
|
ROOM2 = "!AnotherRoom123:daarion.space"
|
|
|
|
|
|
# ── Parsing — valid ────────────────────────────────────────────────────────────
|
|
|
|
def test_parse_single_mapping():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
assert cfg.total_mappings == 1
|
|
assert cfg.mappings[0].agent_id == "sofiia"
|
|
assert cfg.mappings[0].room_id == ROOM1
|
|
|
|
|
|
def test_parse_multiple_mappings():
|
|
raw = f"sofiia:{ROOM1},druid:{ROOM2}"
|
|
cfg = parse_room_map(raw, ALLOWED)
|
|
assert cfg.total_mappings == 2
|
|
|
|
|
|
def test_parse_empty_string():
|
|
cfg = parse_room_map("", ALLOWED)
|
|
assert cfg.total_mappings == 0
|
|
|
|
|
|
def test_parse_whitespace_only():
|
|
cfg = parse_room_map(" ", ALLOWED)
|
|
assert cfg.total_mappings == 0
|
|
|
|
|
|
def test_parse_trailing_comma():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1},", ALLOWED)
|
|
assert cfg.total_mappings == 1
|
|
|
|
|
|
def test_parse_spaces_around_entries():
|
|
cfg = parse_room_map(f" sofiia:{ROOM1} , druid:{ROOM2} ", ALLOWED)
|
|
assert cfg.total_mappings == 2
|
|
|
|
|
|
# ── Parsing — invalid ─────────────────────────────────────────────────────────
|
|
|
|
def test_parse_missing_colon_raises():
|
|
with pytest.raises(ValueError, match="parse errors"):
|
|
parse_room_map("sofiia_no_colon", ALLOWED)
|
|
|
|
|
|
def test_parse_invalid_room_id_format_raises():
|
|
with pytest.raises(ValueError, match="invalid room_id format"):
|
|
# Room ID must start with !
|
|
parse_room_map(f"sofiia:#badroom:server", ALLOWED)
|
|
|
|
|
|
def test_parse_absolute_path_as_room_raises():
|
|
with pytest.raises(ValueError, match="invalid room_id format"):
|
|
parse_room_map("sofiia:/etc/passwd", ALLOWED)
|
|
|
|
|
|
def test_parse_empty_agent_id_raises():
|
|
with pytest.raises(ValueError, match="parse errors"):
|
|
parse_room_map(f":{ROOM1}", ALLOWED)
|
|
|
|
|
|
def test_parse_empty_room_id_raises():
|
|
with pytest.raises(ValueError, match="parse errors"):
|
|
parse_room_map("sofiia:", ALLOWED)
|
|
|
|
|
|
# ── agent_for_room ────────────────────────────────────────────────────────────
|
|
|
|
def test_agent_for_room_found():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
assert cfg.agent_for_room(ROOM1) == "sofiia"
|
|
|
|
|
|
def test_agent_for_room_not_found():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
assert cfg.agent_for_room("!unknownroom:server") is None
|
|
|
|
|
|
def test_agent_for_room_not_allowed():
|
|
"""Agent in mapping but not in allowed_agents → None."""
|
|
cfg = parse_room_map(f"druid:{ROOM1}", frozenset({"sofiia"})) # druid not allowed
|
|
# mapping is accepted but agent_for_room returns None
|
|
assert cfg.agent_for_room(ROOM1) is None
|
|
|
|
|
|
def test_agent_for_room_allowed_when_in_set():
|
|
cfg = parse_room_map(f"druid:{ROOM1}", frozenset({"druid"}))
|
|
assert cfg.agent_for_room(ROOM1) == "druid"
|
|
|
|
|
|
# ── rooms_for_agent ───────────────────────────────────────────────────────────
|
|
|
|
def test_rooms_for_agent_single():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
assert cfg.rooms_for_agent("sofiia") == [ROOM1]
|
|
|
|
|
|
def test_rooms_for_agent_multiple():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1},sofiia:{ROOM2}", ALLOWED)
|
|
rooms = cfg.rooms_for_agent("sofiia")
|
|
assert ROOM1 in rooms
|
|
assert ROOM2 in rooms
|
|
|
|
|
|
def test_rooms_for_agent_unknown():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
assert cfg.rooms_for_agent("nonexistent") == []
|
|
|
|
|
|
# ── as_summary ────────────────────────────────────────────────────────────────
|
|
|
|
def test_summary_contains_expected_fields():
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
summary = cfg.as_summary()
|
|
assert len(summary) == 1
|
|
entry = summary[0]
|
|
assert entry["room_id"] == ROOM1
|
|
assert entry["agent_id"] == "sofiia"
|
|
assert entry["allowed"] is True
|
|
|
|
|
|
def test_summary_allowed_false_for_unknown_agent():
|
|
cfg = parse_room_map(f"druid:{ROOM1}", frozenset({"sofiia"}))
|
|
summary = cfg.as_summary()
|
|
assert summary[0]["allowed"] is False
|
|
|
|
|
|
def test_summary_no_tokens_in_output():
|
|
"""Access tokens must never appear in summary."""
|
|
cfg = parse_room_map(f"sofiia:{ROOM1}", ALLOWED)
|
|
summary = cfg.as_summary()
|
|
for entry in summary:
|
|
assert "token" not in str(entry).lower()
|
|
assert "secret" not in str(entry).lower()
|