From 7be73fcf19e4a139b3c1c483bdebfaec268d09ea Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:24:36 +0800 Subject: [PATCH] fix(channels): let UI runtime channel config win over config.yaml (#3674) * fix(channels): let UI runtime channel config win over config.yaml Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> * test(channels): update assertion to expect runtime/UI value to win over yaml The test was written when yaml took precedence over runtime config. This PR inverts that precedence so UI-entered credentials win; the assertion now correctly reflects that runtime value (xapp-ui) beats the yaml value (xapp) on a shared key. --------- Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> --- backend/app/channels/runtime_config_store.py | 5 +-- backend/tests/test_channels.py | 3 +- .../test_runtime_channel_config_merge.py | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_runtime_channel_config_merge.py diff --git a/backend/app/channels/runtime_config_store.py b/backend/app/channels/runtime_config_store.py index a964c860f..2c6e599b3 100644 --- a/backend/app/channels/runtime_config_store.py +++ b/backend/app/channels/runtime_config_store.py @@ -125,9 +125,8 @@ def merge_runtime_channel_configs( channels_config.pop(provider, None) continue existing = channels_config.get(provider) - merged = dict(runtime_config) - if isinstance(existing, dict): - merged.update(existing) + merged = dict(existing) if isinstance(existing, dict) else {} + merged.update(runtime_config) channels_config[provider] = merged diff --git a/backend/tests/test_channels.py b/backend/tests/test_channels.py index ed17b1411..a92a6f34d 100644 --- a/backend/tests/test_channels.py +++ b/backend/tests/test_channels.py @@ -4528,7 +4528,8 @@ class TestChannelService: service = ChannelService.from_app_config(app_config) assert service._config["telegram"]["bot_token"] == "telegram-token" - assert service._config["slack"]["app_token"] == "xapp" + # The runtime (UI-entered) value must win over the yaml value. + assert service._config["slack"]["app_token"] == "xapp-ui" assert service._config["discord"]["bot_token"] == "discord-bot-token" def test_from_app_config_loads_persisted_runtime_channel_config(self, monkeypatch, tmp_path): diff --git a/backend/tests/test_runtime_channel_config_merge.py b/backend/tests/test_runtime_channel_config_merge.py new file mode 100644 index 000000000..083691e82 --- /dev/null +++ b/backend/tests/test_runtime_channel_config_merge.py @@ -0,0 +1,43 @@ +"""Precedence tests for merge_runtime_channel_configs (pure, no event loop).""" + +from __future__ import annotations + +from types import SimpleNamespace + +from app.channels.runtime_config_store import merge_runtime_channel_configs + + +def _store(data): + return SimpleNamespace(load_all=lambda: data) + + +def test_runtime_config_wins_over_yaml_on_shared_keys(): + # The runtime store holds credentials entered from the UI, which exist so a + # deployment can configure a channel "without needing a config.yaml edit". + # When the same provider is also present in config.yaml, the UI value the + # user just saved must win; the yaml value must not silently override it. + channels_config = { + "telegram": {"bot_token": "yaml_token", "webhook_secret": "from_yaml"}, + } + connections = SimpleNamespace(enabled=True, telegram=SimpleNamespace(enabled=True)) + store = _store({"telegram": {"bot_token": "user_token", "bot_username": "mybot"}}) + + merge_runtime_channel_configs(channels_config, connections, store=store) + + merged = channels_config["telegram"] + # UI-entered value wins on the shared key + assert merged["bot_token"] == "user_token" + # runtime-only key is added + assert merged["bot_username"] == "mybot" + # yaml-only key the UI never set is preserved + assert merged["webhook_secret"] == "from_yaml" + + +def test_runtime_only_provider_is_added(): + channels_config: dict = {} + connections = SimpleNamespace(enabled=True, telegram=SimpleNamespace(enabled=True)) + store = _store({"telegram": {"bot_token": "user_token"}}) + + merge_runtime_channel_configs(channels_config, connections, store=store) + + assert channels_config["telegram"] == {"bot_token": "user_token"}