fix(gateway): load reply_to_mode from config.yaml for Discord and Telegram

The YAML-to-env-var bridge in load_gateway_config() mapped every Discord
and Telegram config key (require_mention, auto_thread, reactions, etc.)
except reply_to_mode. Users setting discord.reply_to_mode or
telegram.reply_to_mode in ~/.hermes/config.yaml got no effect — the
adapter only read the env var, which nothing populated from YAML.

Add the missing bridge for both platforms, following the existing pattern.
Top-level <platform>.reply_to_mode preferred, falls back to
<platform>.extra.reply_to_mode, env var never overwritten. Handles YAML
1.1 bare `off` → Python False coercion.

This is a re-submission of the work from #9837 and #13930, which both
implemented the same fix but neither landed (see co-authors below).

Co-authored-by: Matteo De Agazio <hypnosis.mda@gmail.com>
Co-authored-by: ishardo <239075732+ishardo@users.noreply.github.com>
This commit is contained in:
Asher Morse 2026-04-28 13:55:32 -05:00 committed by Teknium
parent 354502ee48
commit 6b76ea4707
3 changed files with 150 additions and 2 deletions

View file

@ -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"

View file

@ -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"