diff --git a/gateway/config.py b/gateway/config.py index fa64b9046d..2e0e3276b7 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -845,6 +845,16 @@ def load_gateway_config() -> GatewayConfig: ): if yaml_key in allow_mentions_cfg and not os.getenv(env_key): os.environ[env_key] = str(allow_mentions_cfg[yaml_key]).lower() + # reply_to_mode: top-level preferred, falls back to extra.reply_to_mode + # YAML 1.1 parses bare 'off' as boolean False — coerce to string "off". + _discord_extra = discord_cfg.get("extra") if isinstance(discord_cfg.get("extra"), dict) else {} + _discord_rtm = ( + discord_cfg["reply_to_mode"] if "reply_to_mode" in discord_cfg + else _discord_extra.get("reply_to_mode") + ) + if _discord_rtm is not None and not os.getenv("DISCORD_REPLY_TO_MODE"): + _rtm_str = "off" if _discord_rtm is False else str(_discord_rtm).lower() + os.environ["DISCORD_REPLY_TO_MODE"] = _rtm_str # Bridge top-level require_mention to Telegram when the telegram: section # does not already provide one. Users often write "require_mention: true" @@ -881,6 +891,16 @@ def load_gateway_config() -> GatewayConfig: os.environ["TELEGRAM_REACTIONS"] = str(telegram_cfg["reactions"]).lower() if "proxy_url" in telegram_cfg and not os.getenv("TELEGRAM_PROXY"): os.environ["TELEGRAM_PROXY"] = str(telegram_cfg["proxy_url"]).strip() + # reply_to_mode: top-level preferred, falls back to extra.reply_to_mode + # YAML 1.1 parses bare 'off' as boolean False — coerce to string "off". + _telegram_extra = telegram_cfg.get("extra") if isinstance(telegram_cfg.get("extra"), dict) else {} + _telegram_rtm = ( + telegram_cfg["reply_to_mode"] if "reply_to_mode" in telegram_cfg + else _telegram_extra.get("reply_to_mode") + ) + if _telegram_rtm is not None and not os.getenv("TELEGRAM_REPLY_TO_MODE"): + _rtm_str = "off" if _telegram_rtm is False else str(_telegram_rtm).lower() + os.environ["TELEGRAM_REPLY_TO_MODE"] = _rtm_str allowed_users = telegram_cfg.get("allow_from") if allowed_users is not None and not os.getenv("TELEGRAM_ALLOWED_USERS"): if isinstance(allowed_users, list): diff --git a/tests/gateway/test_discord_reply_mode.py b/tests/gateway/test_discord_reply_mode.py index 9060fe2940..64e27a27aa 100644 --- a/tests/gateway/test_discord_reply_mode.py +++ b/tests/gateway/test_discord_reply_mode.py @@ -15,7 +15,7 @@ from unittest.mock import MagicMock, AsyncMock, patch import pytest -from gateway.config import PlatformConfig, GatewayConfig, Platform, _apply_env_overrides +from gateway.config import PlatformConfig, GatewayConfig, Platform, _apply_env_overrides, load_gateway_config def _ensure_discord_mock(): @@ -396,3 +396,67 @@ class TestReplyToText: event = reply_text_adapter.handle_message.await_args.args[0] assert event.reply_to_message_id == "555" assert event.reply_to_text is None + + +class TestYamlConfigLoading: + """Tests for reply_to_mode loaded from config.yaml discord section.""" + + def _write_config(self, tmp_path, content: str): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text(content, encoding="utf-8") + return hermes_home + + def test_top_level_reply_to_mode_off(self, tmp_path, monkeypatch): + """YAML 1.1 parses bare 'off' as boolean False — must map back to 'off'.""" + hermes_home = self._write_config(tmp_path, "discord:\n reply_to_mode: off\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("DISCORD_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("DISCORD_REPLY_TO_MODE") == "off" + + def test_top_level_reply_to_mode_all(self, tmp_path, monkeypatch): + hermes_home = self._write_config(tmp_path, "discord:\n reply_to_mode: all\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("DISCORD_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("DISCORD_REPLY_TO_MODE") == "all" + + def test_extra_reply_to_mode_off(self, tmp_path, monkeypatch): + """discord.extra.reply_to_mode is also honoured.""" + hermes_home = self._write_config( + tmp_path, "discord:\n extra:\n reply_to_mode: \"off\"\n" + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("DISCORD_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("DISCORD_REPLY_TO_MODE") == "off" + + def test_env_var_takes_precedence_over_yaml(self, tmp_path, monkeypatch): + """Existing DISCORD_REPLY_TO_MODE env var is not overwritten by YAML.""" + hermes_home = self._write_config(tmp_path, "discord:\n reply_to_mode: all\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setenv("DISCORD_REPLY_TO_MODE", "first") + + load_gateway_config() + + assert os.environ.get("DISCORD_REPLY_TO_MODE") == "first" + + def test_top_level_takes_precedence_over_extra(self, tmp_path, monkeypatch): + """discord.reply_to_mode wins over discord.extra.reply_to_mode.""" + hermes_home = self._write_config( + tmp_path, + "discord:\n reply_to_mode: all\n extra:\n reply_to_mode: \"off\"\n", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("DISCORD_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("DISCORD_REPLY_TO_MODE") == "all" diff --git a/tests/gateway/test_telegram_reply_mode.py b/tests/gateway/test_telegram_reply_mode.py index a433b18016..1389736fe9 100644 --- a/tests/gateway/test_telegram_reply_mode.py +++ b/tests/gateway/test_telegram_reply_mode.py @@ -11,7 +11,7 @@ from unittest.mock import MagicMock, AsyncMock, patch import pytest -from gateway.config import PlatformConfig, GatewayConfig, Platform, _apply_env_overrides +from gateway.config import PlatformConfig, GatewayConfig, Platform, _apply_env_overrides, load_gateway_config def _ensure_telegram_mock(): @@ -240,3 +240,67 @@ class TestEnvVarOverride: with patch.dict(os.environ, {"TELEGRAM_REPLY_TO_MODE": ""}, clear=False): _apply_env_overrides(config) assert config.platforms[Platform.TELEGRAM].reply_to_mode == "first" + + +class TestTelegramYamlConfigLoading: + """Tests for reply_to_mode loaded from config.yaml telegram section.""" + + def _write_config(self, tmp_path, content: str): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text(content, encoding="utf-8") + return hermes_home + + def test_top_level_reply_to_mode_off(self, tmp_path, monkeypatch): + """YAML 1.1 parses bare 'off' as boolean False — must map back to 'off'.""" + hermes_home = self._write_config(tmp_path, "telegram:\n reply_to_mode: off\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("TELEGRAM_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("TELEGRAM_REPLY_TO_MODE") == "off" + + def test_top_level_reply_to_mode_all(self, tmp_path, monkeypatch): + hermes_home = self._write_config(tmp_path, "telegram:\n reply_to_mode: all\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("TELEGRAM_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("TELEGRAM_REPLY_TO_MODE") == "all" + + def test_extra_reply_to_mode_off(self, tmp_path, monkeypatch): + """telegram.extra.reply_to_mode is also honoured.""" + hermes_home = self._write_config( + tmp_path, "telegram:\n extra:\n reply_to_mode: \"off\"\n" + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("TELEGRAM_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("TELEGRAM_REPLY_TO_MODE") == "off" + + def test_env_var_takes_precedence_over_yaml(self, tmp_path, monkeypatch): + """Existing TELEGRAM_REPLY_TO_MODE env var is not overwritten by YAML.""" + hermes_home = self._write_config(tmp_path, "telegram:\n reply_to_mode: all\n") + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setenv("TELEGRAM_REPLY_TO_MODE", "first") + + load_gateway_config() + + assert os.environ.get("TELEGRAM_REPLY_TO_MODE") == "first" + + def test_top_level_takes_precedence_over_extra(self, tmp_path, monkeypatch): + """telegram.reply_to_mode wins over telegram.extra.reply_to_mode.""" + hermes_home = self._write_config( + tmp_path, + "telegram:\n reply_to_mode: all\n extra:\n reply_to_mode: \"off\"\n", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("TELEGRAM_REPLY_TO_MODE", raising=False) + + load_gateway_config() + + assert os.environ.get("TELEGRAM_REPLY_TO_MODE") == "all"