From da131aef3af079ed8123c68fc4fd248437113394 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:08:39 -0700 Subject: [PATCH] =?UTF-8?q?feat(slack):=20bridge=20slack.ignored=5Fchannel?= =?UTF-8?q?s=20through=20the=20YAML=E2=86=92env=20config=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the #51899 pick, folding in the config-bridge half of the competing PR #46925 (@bhanusharma, earliest submitter for the ignored-channel gate): - _apply_yaml_config: translate config.yaml slack.ignored_channels into SLACK_IGNORED_CHANNELS (list or CSV), env-var-wins like every other bridged Slack key. - SlackAdapter._slack_ignored_channels / gateway.run's _slack_ignored_channels_from_gateway_config: fall back to the SLACK_IGNORED_CHANNELS env var when PlatformConfig.extra carries no value, so top-level slack: blocks (which flow through the env bridge, not extra) are honored at both the adapter and runner gates. - conftest: force-clear SLACK_ALLOWED_CHANNELS / SLACK_IGNORED_CHANNELS / SLACK_DISABLE_DMS between tests (config-loader side-effect leak class). - Tests: env-bridge translation + precedence in test_config.py, env fallback + extra-wins in test_slack_runner_ignored_channels.py. Credit: #46925 by @bhanusharma proposed the same gate with the YAML→env bridge; #51899 (picked as the base) carries the wider outbound/runner coverage. Closes #46925 as consolidated here with first-submitter credit. --- gateway/run.py | 5 +++ plugins/platforms/slack/adapter.py | 9 ++++- tests/conftest.py | 3 ++ tests/gateway/test_config.py | 36 +++++++++++++++++++ .../test_slack_runner_ignored_channels.py | 20 +++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/gateway/run.py b/gateway/run.py index 76eca5a8ebf8..1c7a4359a48a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -966,6 +966,11 @@ def _slack_ignored_channels_from_gateway_config(config: Any) -> set[str]: raw = None if platform_cfg is not None: raw = getattr(platform_cfg, "extra", {}).get("ignored_channels") + if raw is None: + # Top-level ``slack.ignored_channels`` config flows through the + # plugin's YAML→env bridge (SLACK_IGNORED_CHANNELS) rather than + # PlatformConfig.extra — honor it here too (#46925). + raw = os.getenv("SLACK_IGNORED_CHANNELS") or None return _csv_or_list_to_set(raw) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 2be9668576ff..5918c9641c72 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -8915,6 +8915,12 @@ def _apply_yaml_config(yaml_cfg: dict, slack_cfg: dict) -> dict | None: if isinstance(ac, list): ac = ",".join(str(v) for v in ac) os.environ["SLACK_ALLOWED_CHANNELS"] = str(ac) + # ignored_channels: blacklist channels where Slack must never respond. + ic = slack_cfg.get("ignored_channels") + if ic is not None and not os.getenv("SLACK_IGNORED_CHANNELS"): + if isinstance(ic, list): + ic = ",".join(str(v) for v in ic) + os.environ["SLACK_IGNORED_CHANNELS"] = str(ic) return None # all settings flow through env; nothing to merge into extras @@ -8952,7 +8958,8 @@ def register(ctx) -> None: # YAML→env config bridge — owns the translation of config.yaml slack: # keys (require_mention, strict_mention, ignore_other_user_mentions, # thread_require_mention, allow_bots, free_response_channels, - # reactions, disable_dms, allowed_channels) into SLACK_* env vars that + # reactions, disable_dms, allowed_channels, ignored_channels) into + # SLACK_* env vars that # the adapter reads via os.getenv(). Replaces the # hardcoded block in gateway/config.py. Hook contract: #24849. apply_yaml_config_fn=_apply_yaml_config, diff --git a/tests/conftest.py b/tests/conftest.py index 1ad1260469b7..1fcd1bd4abbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -321,6 +321,9 @@ _HERMES_BEHAVIORAL_VARS = frozenset({ "SLACK_IGNORE_OTHER_USER_MENTIONS", "SLACK_REQUIRE_MENTION_CHANNELS", "SLACK_FREE_RESPONSE_CHANNELS", + "SLACK_ALLOWED_CHANNELS", + "SLACK_IGNORED_CHANNELS", + "SLACK_DISABLE_DMS", "SLACK_ALLOW_BOTS", "SLACK_REACTIONS", "DISCORD_REQUIRE_MENTION", diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 9254e9ada605..45091a2698cf 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -592,6 +592,42 @@ class TestLoadGatewayConfig: assert os.getenv("SLACK_DISABLE_DMS") == "true" + def test_slack_ignored_channels_config_sets_env_bridge(self, tmp_path, monkeypatch): + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "slack:\n" + " ignored_channels:\n" + " - C0123456789\n" + " - C0987654321\n", + encoding="utf-8", + ) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("SLACK_IGNORED_CHANNELS", raising=False) + + load_gateway_config() + + assert os.getenv("SLACK_IGNORED_CHANNELS") == "C0123456789,C0987654321" + + def test_slack_ignored_channels_env_takes_precedence(self, tmp_path, monkeypatch): + """An explicit SLACK_IGNORED_CHANNELS env var must not be overwritten + by the config.yaml bridge.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "slack:\n" + " ignored_channels: C_FROM_YAML\n", + encoding="utf-8", + ) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setenv("SLACK_IGNORED_CHANNELS", "C_FROM_ENV") + + load_gateway_config() + + assert os.getenv("SLACK_IGNORED_CHANNELS") == "C_FROM_ENV" + def test_typing_status_text_from_toplevel_platform_block(self, tmp_path, monkeypatch): """A top-level ``slack:`` block reaches PlatformConfig via the shared-key bridge (bridged into extra, then the from_dict extra diff --git a/tests/gateway/test_slack_runner_ignored_channels.py b/tests/gateway/test_slack_runner_ignored_channels.py index c32d9bad1783..af880c2db593 100644 --- a/tests/gateway/test_slack_runner_ignored_channels.py +++ b/tests/gateway/test_slack_runner_ignored_channels.py @@ -114,3 +114,23 @@ async def test_platform_notice_suppressed_for_slack_ignored_channel(): adapter.send.assert_not_called() adapter.send_private_notice.assert_not_called() + + +def test_slack_ignored_channels_env_bridge_fallback(monkeypatch): + """SLACK_IGNORED_CHANNELS (set by the plugin's YAML→env bridge) is + honored when PlatformConfig.extra carries no ignored_channels (#46925).""" + monkeypatch.setenv("SLACK_IGNORED_CHANNELS", "C_ENV1, C_ENV2") + config = _config_with_slack_extra({}) + + assert _slack_ignored_channels_from_gateway_config(config) == {"C_ENV1", "C_ENV2"} + assert _is_slack_ignored_channel(config, "C_ENV1") + assert not _is_slack_ignored_channel(config, "C_OTHER") + + +def test_slack_ignored_channels_extra_wins_over_env(monkeypatch): + """Explicit PlatformConfig.extra config takes precedence over the env + bridge fallback.""" + monkeypatch.setenv("SLACK_IGNORED_CHANNELS", "C_ENV") + config = _config_with_slack_extra({"ignored_channels": ["C_CFG"]}) + + assert _slack_ignored_channels_from_gateway_config(config) == {"C_CFG"}