"""Comprehensive tests for gateway.router module. Tests cover: - normalize_binding: string values, wildcard '*', dict values with key expansion - _assign_tier: all seven tier levels - BindingRouter.resolve: matching logic, tier ordering, AND semantics, defaults - Edge cases: empty bindings, unknown platforms """ from __future__ import annotations import pytest from gateway.router import Binding, BindingRouter, normalize_binding, _assign_tier # ═══════════════════════════════════════════════════════════════════════ # normalize_binding # ═══════════════════════════════════════════════════════════════════════ class TestNormalizeBinding: """Tests for the normalize_binding helper.""" # ── platform string value (specific chat_id) ───────────────────── def test_platform_string_sets_chat_id(self): b = normalize_binding({"agent": "coder", "telegram": "-100123"}) assert b.agent_id == "coder" assert b.match == {"platform": "telegram", "chat_id": "-100123"} def test_platform_string_discord(self): b = normalize_binding({"agent": "bot", "discord": "999"}) assert b.match == {"platform": "discord", "chat_id": "999"} def test_platform_string_slack(self): b = normalize_binding({"agent": "helper", "slack": "C01234"}) assert b.match == {"platform": "slack", "chat_id": "C01234"} # ── platform wildcard '*' ──────────────────────────────────────── def test_platform_wildcard_sets_platform_only(self): b = normalize_binding({"agent": "assistant", "whatsapp": "*"}) assert b.agent_id == "assistant" assert b.match == {"platform": "whatsapp"} def test_wildcard_has_tier_6(self): b = normalize_binding({"agent": "a", "telegram": "*"}) assert b.tier == 6 # ── platform dict value with key expansion ─────────────────────── def test_dict_guild_expansion(self): b = normalize_binding({"agent": "a", "discord": {"guild": "123"}}) assert b.match["guild_id"] == "123" assert "guild" not in b.match def test_dict_type_expansion(self): b = normalize_binding({"agent": "a", "discord": {"type": "channel"}}) assert b.match["chat_type"] == "channel" assert "type" not in b.match def test_dict_team_expansion(self): b = normalize_binding({"agent": "a", "slack": {"team": "T999"}}) assert b.match["team_id"] == "T999" assert "team" not in b.match def test_dict_peer_expansion(self): b = normalize_binding({"agent": "a", "telegram": {"peer": "user42"}}) assert b.match["peer"] == "user42" def test_dict_multiple_expansions(self): b = normalize_binding({ "agent": "coder", "discord": {"guild": "123", "type": "channel"}, }) assert b.match == { "platform": "discord", "guild_id": "123", "chat_type": "channel", } def test_dict_values_stringified(self): b = normalize_binding({"agent": "a", "discord": {"guild": 123}}) assert b.match["guild_id"] == "123" def test_dict_passthrough_expanded_keys(self): """Keys already in expanded form are passed through as-is.""" b = normalize_binding({"agent": "a", "discord": {"guild_id": "555"}}) assert b.match["guild_id"] == "555" # ── agent key variants ─────────────────────────────────────────── def test_agent_id_key_variant(self): b = normalize_binding({"agent_id": "x", "telegram": "*"}) assert b.agent_id == "x" def test_missing_agent_raises(self): with pytest.raises(ValueError, match="missing 'agent'"): normalize_binding({"telegram": "*"}) # ── unsupported value type ─────────────────────────────────────── def test_unsupported_value_type_raises(self): with pytest.raises(TypeError, match="Unsupported value type"): normalize_binding({"agent": "a", "telegram": 42}) # ── no platform key → empty match ──────────────────────────────── def test_no_platform_key_gives_empty_match(self): b = normalize_binding({"agent": "fallback"}) assert b.match == {} assert b.tier == 7 # ── only first platform key is used ────────────────────────────── def test_only_one_platform_used(self): """Even if multiple platform keys exist, only one is consumed.""" b = normalize_binding({"agent": "a", "telegram": "*", "discord": "*"}) # We can't predict which one wins (set iteration order), but the # match should contain exactly one platform key. assert "platform" in b.match assert b.match["platform"] in {"telegram", "discord"} # ═══════════════════════════════════════════════════════════════════════ # _assign_tier # ═══════════════════════════════════════════════════════════════════════ class TestAssignTier: """Tests for _assign_tier: all 7 tier levels.""" def test_tier_1_platform_chat_id(self): assert _assign_tier({"platform": "telegram", "chat_id": "-100"}) == 1 def test_tier_1_chat_id_without_platform(self): """chat_id alone still gets tier 1 (it's the key presence that matters).""" assert _assign_tier({"chat_id": "-100"}) == 1 def test_tier_2_platform_peer(self): assert _assign_tier({"platform": "telegram", "peer": "user42"}) == 2 def test_tier_3_platform_guild_chat_type(self): assert _assign_tier({ "platform": "discord", "guild_id": "123", "chat_type": "channel", }) == 3 def test_tier_4_platform_guild_id(self): assert _assign_tier({"platform": "discord", "guild_id": "123"}) == 4 def test_tier_4_platform_team_id(self): assert _assign_tier({"platform": "slack", "team_id": "T01"}) == 4 def test_tier_5_platform_chat_type(self): assert _assign_tier({"platform": "telegram", "chat_type": "group"}) == 5 def test_tier_6_platform_only(self): assert _assign_tier({"platform": "telegram"}) == 6 def test_tier_7_empty(self): assert _assign_tier({}) == 7 # ── precedence checks ──────────────────────────────────────────── def test_chat_id_beats_peer(self): """If both chat_id and peer are present, tier 1 wins.""" assert _assign_tier({ "platform": "telegram", "chat_id": "123", "peer": "user42", }) == 1 def test_peer_beats_guild(self): assert _assign_tier({ "platform": "discord", "peer": "user42", "guild_id": "123", }) == 2 def test_guild_plus_chat_type_beats_guild_alone(self): tier_combined = _assign_tier({ "platform": "discord", "guild_id": "123", "chat_type": "channel", }) tier_guild_only = _assign_tier({ "platform": "discord", "guild_id": "123", }) assert tier_combined < tier_guild_only # lower = more specific # ═══════════════════════════════════════════════════════════════════════ # BindingRouter.resolve # ═══════════════════════════════════════════════════════════════════════ class TestBindingRouterResolve: """Tests for BindingRouter.resolve method.""" # ── exact chat_id match (tier 1) ───────────────────────────────── def test_exact_chat_id_match(self): router = BindingRouter( [{"agent": "coder", "telegram": "-100123"}], default_agent_id="default", ) result = router.resolve(platform="telegram", chat_id="-100123") assert result == "coder" def test_chat_id_no_match_falls_to_default(self): router = BindingRouter( [{"agent": "coder", "telegram": "-100123"}], default_agent_id="default", ) result = router.resolve(platform="telegram", chat_id="-999") assert result == "default" # ── peer match (tier 2) ────────────────────────────────────────── def test_peer_match(self): router = BindingRouter( [{"agent": "dm_bot", "telegram": {"peer": "user42"}}], default_agent_id="default", ) # resolve doesn't have a peer kwarg, so peer should be in match # but resolve takes user_id, not peer. Let me check the match logic. # Actually looking at the code, resolve() kwargs don't include 'peer', # so a peer binding can never match via resolve() directly unless # peer is mapped to some kwarg. Let me re-check... # The _matches method checks binding.match keys against kwargs. # kwargs has: platform, chat_id, chat_type, user_id, guild_id, team_id # So 'peer' in binding.match won't match any kwarg → never matches. # This seems like a design issue, but let's test the actual behavior. result = router.resolve(platform="telegram", user_id="user42") # peer != user_id in kwargs, so this won't match assert result == "default" # ── platform wildcard match (tier 6) ───────────────────────────── def test_platform_wildcard_match(self): router = BindingRouter( [{"agent": "assistant", "telegram": "*"}], default_agent_id="default", ) result = router.resolve(platform="telegram", chat_id="anything") assert result == "assistant" def test_platform_wildcard_no_match_different_platform(self): router = BindingRouter( [{"agent": "assistant", "telegram": "*"}], default_agent_id="default", ) result = router.resolve(platform="discord") assert result == "default" # ── default fallback ───────────────────────────────────────────── def test_default_fallback_no_bindings(self): router = BindingRouter([], default_agent_id="fallback") result = router.resolve(platform="telegram", chat_id="123") assert result == "fallback" def test_default_fallback_no_match(self): router = BindingRouter( [{"agent": "coder", "discord": "999"}], default_agent_id="fallback", ) result = router.resolve(platform="telegram", chat_id="123") assert result == "fallback" # ── tier ordering: more specific wins ──────────────────────────── def test_chat_id_beats_platform_wildcard(self): """Tier 1 (chat_id) should win over tier 6 (platform wildcard).""" router = BindingRouter( [ {"agent": "general", "telegram": "*"}, {"agent": "specific", "telegram": "-100123"}, ], default_agent_id="default", ) result = router.resolve(platform="telegram", chat_id="-100123") assert result == "specific" def test_guild_chat_type_beats_guild_only(self): """Tier 3 should win over tier 4.""" router = BindingRouter( [ {"agent": "guild_agent", "discord": {"guild": "123"}}, {"agent": "channel_agent", "discord": {"guild": "123", "type": "channel"}}, ], default_agent_id="default", ) result = router.resolve( platform="discord", guild_id="123", chat_type="channel", ) assert result == "channel_agent" def test_guild_beats_chat_type_only(self): """Tier 4 should win over tier 5.""" router = BindingRouter( [ {"agent": "type_agent", "discord": {"type": "channel"}}, {"agent": "guild_agent", "discord": {"guild": "123"}}, ], default_agent_id="default", ) result = router.resolve( platform="discord", guild_id="123", chat_type="channel", ) assert result == "guild_agent" def test_chat_type_beats_platform_only(self): """Tier 5 should win over tier 6.""" router = BindingRouter( [ {"agent": "platform_agent", "telegram": "*"}, {"agent": "group_agent", "telegram": {"type": "group"}}, ], default_agent_id="default", ) result = router.resolve(platform="telegram", chat_type="group") assert result == "group_agent" def test_chat_id_beats_guild_plus_chat_type(self): """Tier 1 beats tier 3.""" router = BindingRouter( [ {"agent": "guild_type", "discord": {"guild": "123", "type": "channel"}}, {"agent": "exact", "discord": "chat999"}, ], default_agent_id="default", ) result = router.resolve( platform="discord", chat_id="chat999", guild_id="123", chat_type="channel", ) assert result == "exact" # ── within-tier first-match-wins ───────────────────────────────── def test_same_tier_first_match_wins(self): """Two tier-6 bindings: the first one listed should win.""" router = BindingRouter( [ {"agent": "first", "telegram": "*"}, {"agent": "second", "telegram": "*"}, ], default_agent_id="default", ) result = router.resolve(platform="telegram") assert result == "first" def test_same_tier_first_match_wins_chat_id(self): """Two tier-1 bindings for different chat_ids.""" router = BindingRouter( [ {"agent": "first", "telegram": "aaa"}, {"agent": "second", "telegram": "bbb"}, ], default_agent_id="default", ) assert router.resolve(platform="telegram", chat_id="aaa") == "first" assert router.resolve(platform="telegram", chat_id="bbb") == "second" # ── AND semantics: all fields must match ───────────────────────── def test_and_semantics_guild_must_match(self): """Binding requires guild_id=123; different guild should not match.""" router = BindingRouter( [{"agent": "guild_bot", "discord": {"guild": "123"}}], default_agent_id="default", ) assert router.resolve(platform="discord", guild_id="999") == "default" def test_and_semantics_all_fields_required(self): """Binding requires guild_id AND chat_type; missing one → no match.""" router = BindingRouter( [{"agent": "combo", "discord": {"guild": "123", "type": "channel"}}], default_agent_id="default", ) # Only guild_id, no chat_type → should NOT match assert router.resolve(platform="discord", guild_id="123") == "default" # Only chat_type, no guild_id → should NOT match assert router.resolve(platform="discord", chat_type="channel") == "default" # Both → should match assert router.resolve( platform="discord", guild_id="123", chat_type="channel", ) == "combo" def test_and_semantics_platform_must_match(self): """Binding for telegram should not match discord.""" router = BindingRouter( [{"agent": "tg", "telegram": "*"}], default_agent_id="default", ) assert router.resolve(platform="discord") == "default" # ── no bindings uses default ───────────────────────────────────── def test_no_bindings_returns_default(self): router = BindingRouter([], default_agent_id="my_default") assert router.resolve(platform="telegram") == "my_default" def test_no_bindings_returns_default_with_all_kwargs(self): router = BindingRouter([], default_agent_id="my_default") assert router.resolve( platform="telegram", chat_id="123", chat_type="group", user_id="u1", guild_id="g1", team_id="t1", ) == "my_default" # ═══════════════════════════════════════════════════════════════════════ # Edge cases # ═══════════════════════════════════════════════════════════════════════ class TestEdgeCases: """Edge case tests.""" def test_empty_bindings_list(self): router = BindingRouter([], default_agent_id="default") assert router.resolve(platform="telegram") == "default" def test_unknown_platform_falls_to_default(self): """Platform not in PLATFORM_NAMES doesn't match any binding.""" router = BindingRouter( [{"agent": "a", "telegram": "*"}], default_agent_id="default", ) assert router.resolve(platform="matrix") == "default" def test_unknown_platform_in_binding_ignored(self): """A binding with an unknown platform key produces empty match.""" b = normalize_binding({"agent": "a", "matrix": "*"}) assert b.match == {} assert b.tier == 7 def test_binding_dataclass_frozen(self): """Binding is frozen; can't modify fields after creation.""" b = Binding(agent_id="a", match={"platform": "telegram"}, tier=6) with pytest.raises(AttributeError): b.agent_id = "b" # type: ignore[misc] def test_binding_default_tier(self): """Default tier is 7.""" b = Binding(agent_id="a") assert b.tier == 7 assert b.match == {} def test_multiple_platforms_in_config(self): """Router handles multiple different platforms correctly.""" router = BindingRouter( [ {"agent": "tg_bot", "telegram": "*"}, {"agent": "dc_bot", "discord": "*"}, {"agent": "sl_bot", "slack": "*"}, ], default_agent_id="default", ) assert router.resolve(platform="telegram") == "tg_bot" assert router.resolve(platform="discord") == "dc_bot" assert router.resolve(platform="slack") == "sl_bot" assert router.resolve(platform="whatsapp") == "default" def test_bindings_sorted_by_tier(self): """Internal bindings list is sorted by tier (most specific first).""" router = BindingRouter( [ {"agent": "platform", "telegram": "*"}, # tier 6 {"agent": "exact", "telegram": "123"}, # tier 1 {"agent": "guild", "discord": {"guild": "1"}}, # tier 4 ], default_agent_id="default", ) tiers = [b.tier for b in router._bindings] assert tiers == sorted(tiers) def test_team_id_match(self): """Binding with team_id matches when team_id is provided.""" router = BindingRouter( [{"agent": "slack_team", "slack": {"team": "T01"}}], default_agent_id="default", ) assert router.resolve(platform="slack", team_id="T01") == "slack_team" assert router.resolve(platform="slack", team_id="T99") == "default" def test_complex_routing_scenario(self): """Full scenario with multiple tiers competing.""" router = BindingRouter( [ {"agent": "fallback_tg", "telegram": "*"}, {"agent": "dev_chat", "telegram": "-100999"}, {"agent": "discord_general", "discord": "*"}, {"agent": "discord_guild", "discord": {"guild": "G1"}}, {"agent": "discord_guild_channel", "discord": {"guild": "G1", "type": "text"}}, ], default_agent_id="global_default", ) # Telegram exact chat assert router.resolve( platform="telegram", chat_id="-100999", ) == "dev_chat" # Telegram other chat → wildcard assert router.resolve( platform="telegram", chat_id="-100000", ) == "fallback_tg" # Discord exact guild + type assert router.resolve( platform="discord", guild_id="G1", chat_type="text", ) == "discord_guild_channel" # Discord guild only (no type) assert router.resolve( platform="discord", guild_id="G1", ) == "discord_guild" # Discord other guild → platform wildcard assert router.resolve( platform="discord", guild_id="OTHER", ) == "discord_general" # Unknown platform assert router.resolve(platform="whatsapp") == "global_default" def test_chat_type_alone_binding(self): """Tier 5: platform + chat_type only.""" router = BindingRouter( [{"agent": "group_handler", "telegram": {"type": "group"}}], default_agent_id="default", ) assert router.resolve( platform="telegram", chat_type="group", ) == "group_handler" assert router.resolve( platform="telegram", chat_type="private", ) == "default" def test_resolve_with_none_values(self): """None values in kwargs should not match binding requirements.""" router = BindingRouter( [{"agent": "guild_bot", "discord": {"guild": "123"}}], default_agent_id="default", ) # guild_id defaults to None assert router.resolve(platform="discord") == "default"