From 79db053b38fc7025901d6b049d9065b49eedcc39 Mon Sep 17 00:00:00 2001 From: Apple Date: Thu, 5 Mar 2026 01:21:07 -0800 Subject: [PATCH] feat(matrix-bridge-dagi): support N rooms in BRIDGE_ROOM_MAP, reject duplicate room_id (M2.0) Made-with: Cursor --- .../matrix-bridge-dagi/app/room_mapping.py | 12 ++- tests/test_matrix_bridge_room_mapping.py | 76 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/services/matrix-bridge-dagi/app/room_mapping.py b/services/matrix-bridge-dagi/app/room_mapping.py index 28cd13a0..90ce9a38 100644 --- a/services/matrix-bridge-dagi/app/room_mapping.py +++ b/services/matrix-bridge-dagi/app/room_mapping.py @@ -1,5 +1,5 @@ """ -Room-to-Agent Mapping — Phase M1 +Room-to-Agent Mapping — Phase M2.0 (N rooms, 1 agent per room) Parses BRIDGE_ROOM_MAP env var and provides: - room_id → agent_id lookup @@ -148,6 +148,16 @@ def parse_room_map(raw: str, allowed_agents: FrozenSet[str]) -> RoomMappingConfi if errors: raise ValueError(f"BRIDGE_ROOM_MAP parse errors: {'; '.join(errors)}") + # M2.0: fail fast on duplicate room_id (1 room must map to exactly 1 agent) + seen_rooms: Dict[str, str] = {} + for m in mappings: + if m.room_id in seen_rooms: + raise ValueError( + f"Duplicate room_id {m.room_id!r}: already bound to agent " + f"{seen_rooms[m.room_id]!r}, cannot rebind to {m.agent_id!r}" + ) + seen_rooms[m.room_id] = m.agent_id + config = RoomMappingConfig(mappings=mappings, allowed_agents=allowed_agents) logger.info( "Room mapping loaded: %d entries, allowed_agents=%s", diff --git a/tests/test_matrix_bridge_room_mapping.py b/tests/test_matrix_bridge_room_mapping.py index 5f4ed882..c35a7e7c 100644 --- a/tests/test_matrix_bridge_room_mapping.py +++ b/tests/test_matrix_bridge_room_mapping.py @@ -149,3 +149,79 @@ def test_summary_no_tokens_in_output(): for entry in summary: assert "token" not in str(entry).lower() assert "secret" not in str(entry).lower() + + +# ── M2.0: N rooms, duplicate validation, multi-room lookup ──────────────────── + +ROOM3 = "!ThirdRoom456:daarion.space" +ROOM4 = "!FourthRoom789:daarion.space" +ROOM5 = "!FifthRoom000:daarion.space" +ALLOWED_MULTI = frozenset({"sofiia", "druid", "helion", "nutra", "alateya"}) + + +def test_parse_five_rooms(): + """N rooms (up to 5) should all parse correctly.""" + raw = ( + f"sofiia:{ROOM1},druid:{ROOM2},helion:{ROOM3}," + f"nutra:{ROOM4},alateya:{ROOM5}" + ) + cfg = parse_room_map(raw, ALLOWED_MULTI) + assert cfg.total_mappings == 5 + assert cfg.agent_for_room(ROOM1) == "sofiia" + assert cfg.agent_for_room(ROOM2) == "druid" + assert cfg.agent_for_room(ROOM3) == "helion" + assert cfg.agent_for_room(ROOM4) == "nutra" + assert cfg.agent_for_room(ROOM5) == "alateya" + + +def test_duplicate_room_id_raises(): + """Same room_id bound to two agents must raise ValueError (M2.0 fail-fast).""" + raw = f"sofiia:{ROOM1},druid:{ROOM1}" + with pytest.raises(ValueError, match="Duplicate room_id"): + parse_room_map(raw, ALLOWED_MULTI) + + +def test_duplicate_room_id_same_agent_raises(): + """Even same agent repeated for same room must raise — 1 room = 1 agent.""" + raw = f"sofiia:{ROOM1},sofiia:{ROOM1}" + with pytest.raises(ValueError, match="Duplicate room_id"): + parse_room_map(raw, ALLOWED_MULTI) + + +def test_multi_room_o1_lookup(): + """agent_for_room must return correct agent for each of N rooms (O(1) index).""" + raw = f"sofiia:{ROOM1},druid:{ROOM2},helion:{ROOM3}" + cfg = parse_room_map(raw, ALLOWED_MULTI) + assert cfg.agent_for_room(ROOM1) == "sofiia" + assert cfg.agent_for_room(ROOM2) == "druid" + assert cfg.agent_for_room(ROOM3) == "helion" + assert cfg.agent_for_room("!unknown:server") is None + + +def test_rooms_for_agent_multi_room(): + """rooms_for_agent returns all rooms bound to a given agent.""" + raw = f"sofiia:{ROOM1},sofiia:{ROOM2},druid:{ROOM3}" + allowed = frozenset({"sofiia", "druid"}) + cfg = parse_room_map(raw, allowed) + sofiia_rooms = cfg.rooms_for_agent("sofiia") + assert set(sofiia_rooms) == {ROOM1, ROOM2} + assert cfg.rooms_for_agent("druid") == [ROOM3] + + +def test_multi_room_summary_count(): + """as_summary() must return one entry per mapping.""" + raw = f"sofiia:{ROOM1},druid:{ROOM2},helion:{ROOM3}" + cfg = parse_room_map(raw, ALLOWED_MULTI) + summary = cfg.as_summary() + assert len(summary) == 3 + room_ids = {s["room_id"] for s in summary} + assert room_ids == {ROOM1, ROOM2, ROOM3} + + +def test_multi_room_unknown_agent_filtered(): + """agent_for_room returns None if agent not in allowed_agents (even if mapping exists).""" + raw = f"sofiia:{ROOM1},unknown_bot:{ROOM2}" + allowed = frozenset({"sofiia"}) # unknown_bot not allowed + cfg = parse_room_map(raw, allowed) + assert cfg.agent_for_room(ROOM1) == "sofiia" + assert cfg.agent_for_room(ROOM2) is None # not in allowed_agents