diff --git a/gateway/config.py b/gateway/config.py index 4a753c6773b..a85221f5c2f 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -1188,8 +1188,12 @@ def load_gateway_config() -> GatewayConfig: # Map config.yaml keys → GatewayConfig.from_dict() schema. # Each key overwrites whatever gateway.json may have set. + # Precedence contract: key-presence at the TOP LEVEL wins; the + # nested gateway.* form is consulted only when the top-level key + # is absent (not merely falsy/mistyped), so a present-but-empty + # top-level value is never silently replaced by the nested one. sr = yaml_cfg.get("session_reset") - if not (sr and isinstance(sr, dict)) and isinstance(gateway_section, dict): + if "session_reset" not in yaml_cfg and isinstance(gateway_section, dict): sr = gateway_section.get("session_reset") if sr and isinstance(sr, dict): gw_data["default_reset_policy"] = sr @@ -1208,7 +1212,7 @@ def load_gateway_config() -> GatewayConfig: ) stt_cfg = yaml_cfg.get("stt") - if not isinstance(stt_cfg, dict) and isinstance(gateway_section, dict): + if "stt" not in yaml_cfg and isinstance(gateway_section, dict): stt_cfg = gateway_section.get("stt") if isinstance(stt_cfg, dict): gw_data["stt"] = stt_cfg diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 562b2a3e77c..c43aaa2563d 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -827,6 +827,48 @@ class TestLoadGatewayConfig: assert config.always_log_local is True + def test_present_empty_top_level_session_reset_blocks_nested_fallback(self, tmp_path, monkeypatch): + """Key-presence precedence: a present (even empty) top-level + session_reset must NOT be replaced by gateway.session_reset — + the fallback fires only when the top-level key is absent.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "session_reset: {}\n" + "gateway:\n" + " session_reset:\n" + " mode: idle\n" + " idle_minutes: 30\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + # The nested value must not leak through the present top-level key. + assert config.default_reset_policy.mode != "idle" + + def test_present_top_level_stt_blocks_nested_fallback(self, tmp_path, monkeypatch): + """Key-presence precedence for stt: a present top-level stt (even + mistyped/non-dict) must not be replaced by gateway.stt.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "stt: {}\n" + "gateway:\n" + " stt:\n" + " enabled: false\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + # gateway.stt.enabled=false must NOT win over the present top-level stt. + assert config.stt_enabled is True + def test_relay_platform_enabled_from_env_url(self, tmp_path, monkeypatch): """GATEWAY_RELAY_URL must enable Platform.RELAY in config.platforms so start_gateway()'s connect loop actually dials the connector. Registering