diff --git a/plugins/platforms/matrix/adapter.py b/plugins/platforms/matrix/adapter.py index ab83c4c6a0..ee3faaf72d 100644 --- a/plugins/platforms/matrix/adapter.py +++ b/plugins/platforms/matrix/adapter.py @@ -39,7 +39,8 @@ from typing import Any, Dict, Optional, Set from agent.secret_scope import get_secret from gateway.platforms._shared import ( - apply_yaml_bridge as _apply_yaml_bridge, get_scoped_secret as _get_scoped_secret, send_error + apply_yaml_bridge as _apply_yaml_bridge, extra_or_secret as _extra_or_secret, + get_scoped_secret as _get_scoped_secret, send_error ) try: @@ -319,10 +320,8 @@ MATRIX_MAX_MESSAGE_LENGTH_CEILING = 65535 def _resolve_max_message_length(config) -> int: """Resolve outbound chunk size from config, env, or plugin registry.""" - raw = (getattr(config, "extra", {}) or {}).get("max_message_length") - if raw is None: - raw = _get_scoped_secret("MATRIX_MAX_MESSAGE_LENGTH") - if raw is None: + raw = _extra_or_secret(getattr(config, "extra", None), "max_message_length", "MATRIX_MAX_MESSAGE_LENGTH", None) + if raw is None or not str(raw).strip(): with suppress(Exception): from gateway.platform_registry import platform_registry entry = platform_registry.get("matrix") @@ -477,12 +476,9 @@ def _csv_set(raw: Any) -> Set[str]: def _extra_csv_set(config, key: str, env_name: str) -> Set[str]: - """Resolve a room/user list from config.extra[key], else the env var.""" - raw = config.extra.get(key) - if raw is None or (not isinstance(raw, list) and not str(raw).strip()): - # Scoped read: under multiplex os.environ is the DEFAULT profile's room/user list. - raw = _get_scoped_secret(env_name, "").strip() - return _csv_set(raw) + """Resolve a room/user list from config.extra[key] (blank = unset), else the scoped env var — + under multiplex os.environ is the DEFAULT profile's room/user list.""" + return _csv_set(_extra_or_secret(config.extra, key, env_name)) def _recovery_key_output_path() -> Optional[Path]: @@ -871,10 +867,8 @@ class MatrixAdapter(BasePlatformAdapter): @staticmethod def _extra_truthy(config, key: str, env_name: str, default: str) -> bool: - """``config.extra[key]`` (YAML-bridged, per profile) else the env var, true/1/yes semantics.""" - configured = config.extra.get(key) - if configured is None: - return _env_truthy(env_name, default) + """``config.extra[key]`` (YAML-bridged, per profile; blank = unset) else the env var, true/1/yes.""" + configured = _extra_or_secret(config.extra, key, env_name, default) return configured if isinstance(configured, bool) else str(configured).lower() in ("true", "1", "yes") @staticmethod diff --git a/tests/gateway/test_matrix_mention.py b/tests/gateway/test_matrix_mention.py index cdc2f2f38d..db80355223 100644 --- a/tests/gateway/test_matrix_mention.py +++ b/tests/gateway/test_matrix_mention.py @@ -78,72 +78,6 @@ def _make_event( ) -@pytest.fixture -def matrix_free_rooms_scope(monkeypatch): - from agent import secret_scope - - was_multiplexed = secret_scope.is_multiplex_active() - monkeypatch.setenv("MATRIX_FREE_RESPONSE_ROOMS", "!process:example.org") - secret_scope.set_multiplex_active(True) - token = secret_scope.set_secret_scope( - {"MATRIX_FREE_RESPONSE_ROOMS": "!scoped-a:example.org, !scoped-b:example.org"} - ) - try: - yield - finally: - secret_scope.reset_secret_scope(token) - secret_scope.set_multiplex_active(was_multiplexed) - - -@pytest.mark.parametrize("configured_value", ["", " \t "]) -def test_matrix_free_response_rooms_blank_scalar_falls_back_to_scoped_value( - configured_value, - matrix_free_rooms_scope, -): - from plugins.platforms.matrix.adapter import _extra_csv_set - - config = PlatformConfig( - enabled=True, - extra={"free_response_rooms": configured_value}, - ) - - assert _extra_csv_set( - config, - "free_response_rooms", - "MATRIX_FREE_RESPONSE_ROOMS", - ) == {"!scoped-a:example.org", "!scoped-b:example.org"} - - -@pytest.mark.parametrize( - ("configured_value", "expected"), - [ - ("!configured:example.org", {"!configured:example.org"}), - ( - ["!configured-a:example.org", " !configured-b:example.org "], - {"!configured-a:example.org", "!configured-b:example.org"}, - ), - ], - ids=("scalar", "list"), -) -def test_matrix_free_response_rooms_explicit_values_override_scoped_fallback( - configured_value, - expected, - matrix_free_rooms_scope, -): - from plugins.platforms.matrix.adapter import _extra_csv_set - - config = PlatformConfig( - enabled=True, - extra={"free_response_rooms": configured_value}, - ) - - assert _extra_csv_set( - config, - "free_response_rooms", - "MATRIX_FREE_RESPONSE_ROOMS", - ) == expected - - # --------------------------------------------------------------------------- # Mention detection helpers # --------------------------------------------------------------------------- @@ -448,4 +382,3 @@ class TestMatrixConfigBridge: == "!room1:example.org,!room2:example.org" ) assert os.getenv("MATRIX_AUTO_THREAD") == "false" - diff --git a/tests/plugins/platforms/matrix/test_blank_config_env_fallback.py b/tests/plugins/platforms/matrix/test_blank_config_env_fallback.py new file mode 100644 index 0000000000..c8c88698f8 --- /dev/null +++ b/tests/plugins/platforms/matrix/test_blank_config_env_fallback.py @@ -0,0 +1,40 @@ +"""A present-but-blank ``matrix:`` key in config.yaml means "unset": the env-var fallback must fire +exactly as it does when the key is absent (0.21.2 started seeding blank YAML values into +``config.extra``, which flipped the precedence and silently disabled free-response rooms).""" + +import pytest + +from gateway.config import PlatformConfig + + +@pytest.mark.parametrize("blank", ["", " \t "]) +def test_blank_yaml_values_fall_through_to_env(monkeypatch, blank): + from plugins.platforms.matrix.adapter import MatrixAdapter, _extra_csv_set, _resolve_max_message_length + + monkeypatch.setenv("MATRIX_FREE_RESPONSE_ROOMS", "!home:example.org") + monkeypatch.setenv("MATRIX_MAX_MESSAGE_LENGTH", "9000") + monkeypatch.setenv("MATRIX_AUTO_THREAD", "false") + config = PlatformConfig(enabled=True, extra={ + "free_response_rooms": blank, "max_message_length": blank, "auto_thread": blank}) + + assert _extra_csv_set(config, "free_response_rooms", "MATRIX_FREE_RESPONSE_ROOMS") == {"!home:example.org"} + assert _resolve_max_message_length(config) == 9000 + assert MatrixAdapter._extra_truthy(config, "auto_thread", "MATRIX_AUTO_THREAD", "true") is False + + +def test_explicit_yaml_values_still_beat_env(monkeypatch): + from plugins.platforms.matrix.adapter import MatrixAdapter, _extra_csv_set, _resolve_max_message_length + + monkeypatch.setenv("MATRIX_FREE_RESPONSE_ROOMS", "!env:example.org") + monkeypatch.setenv("MATRIX_MAX_MESSAGE_LENGTH", "9000") + monkeypatch.setenv("MATRIX_AUTO_THREAD", "true") + config = PlatformConfig(enabled=True, extra={ + "free_response_rooms": ["!a:example.org", " !b:example.org "], "max_message_length": 4000, + "auto_thread": False}) + + assert _extra_csv_set(config, "free_response_rooms", "MATRIX_FREE_RESPONSE_ROOMS") == {"!a:example.org", "!b:example.org"} + assert _resolve_max_message_length(config) == 4000 + assert MatrixAdapter._extra_truthy(config, "auto_thread", "MATRIX_AUTO_THREAD", "true") is False + # An explicit empty list is a real "no rooms" value, not "unset". + assert _extra_csv_set(PlatformConfig(enabled=True, extra={"free_response_rooms": []}), + "free_response_rooms", "MATRIX_FREE_RESPONSE_ROOMS") == set()