diff --git a/gateway/run.py b/gateway/run.py index 4f82c6895a..3bf9d1605b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4311,11 +4311,11 @@ class GatewayRunner( chat_id=source.chat_id, thread_id=getattr(source, "thread_id", None), parent_chat_id=getattr(source, "parent_chat_id", None), adapter_profile=adapter_profile, user_id=getattr(source, "user_id", None)) - except Exception: + except Exception as exc: logger.warning( - "Profile route matching failed for %s/%s, falling back to default", + "Rejecting %s/%s: profile route matching failed", source.platform, source.chat_id, exc_info=True) - return None + raise ProfileRouteRejected("matcher") from exc if matched: try: served = {name for name, _home in _multiplex_profile_homes(config)} diff --git a/tests/gateway/test_profile_resolution.py b/tests/gateway/test_profile_resolution.py index 1a57387126..d3dbc2031c 100644 --- a/tests/gateway/test_profile_resolution.py +++ b/tests/gateway/test_profile_resolution.py @@ -89,10 +89,10 @@ class TestMissingProfileWarning: with patch("hermes_constants.get_hermes_home", return_value=Path("/hermes")): with caplog.at_level(logging.WARNING): result = mock_runner._resolve_profile_home_for_source(discord_source) - + # Should fall back to global HERMES_HOME assert result == Path("/hermes") - + # Should have logged a warning assert len(caplog.records) == 1 assert caplog.records[0].levelname == "WARNING" @@ -141,9 +141,10 @@ class TestRoutingConsultation: mock_get_dir.return_value = Path("/hermes/profiles/routed") mock_runner._profile_name_for_source = MagicMock(return_value="routed") - - mock_runner._resolve_profile_home_for_source(discord_source) - + + with patch("hermes_cli.profiles.profile_exists", return_value=True): + mock_runner._resolve_profile_home_for_source(discord_source) + # Should have called routing mock_runner._profile_name_for_source.assert_called_once_with(discord_source) @@ -412,6 +413,27 @@ class TestAdapterToSessionKeyIntegration: ) assert result is None + def test_matcher_failure_rejects_instead_of_serving_the_default_profile(self, mock_runner): + mock_runner.config.multiplex_profiles = True + mock_runner.config.profile_routes = [ + ProfileRoute(name="r", platform="discord", profile="routed", chat_id="c") + ] + with patch("gateway.profile_routing.match_profile_route", side_effect=RuntimeError("boom")): + with pytest.raises(ProfileRouteRejected): + mock_runner._profile_name_for_source( + SessionSource(platform=Platform.DISCORD, chat_id="c") + ) + + def test_plain_no_match_still_serves_the_active_profile(self, mock_runner): + # Only failures fail closed; an ordinary unrouted sender keeps the historical behaviour. + mock_runner.config.multiplex_profiles = True + mock_runner.config.profile_routes = [ + ProfileRoute(name="r", platform="discord", profile="routed", chat_id="other") + ] + source = SessionSource(platform=Platform.DISCORD, chat_id="c", user_id="nobody") + assert mock_runner._profile_name_for_source(source) is None + assert mock_runner._resolve_profile_home_for_source(source) is not None + @pytest.mark.asyncio async def test_direct_source_is_rejected_at_shared_ingress(self, mock_runner): mock_runner.config.multiplex_profiles = True