From baae72d87693555d8fb4521ec800c2e379beada6 Mon Sep 17 00:00:00 2001 From: huyengiang101086-spec Date: Thu, 1 Oct 2026 10:51:37 +0800 Subject: [PATCH] fix(channels): point the Discord missing-dependency hint at the discord extra (#6054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(scripts): cover discord extra auto-detection The discord mapping in scripts/detect_uv_extras.py worked but was neither listed in the "currently maps" docstring nor covered by a test, unlike the buzz mapping that grew up beside it. * fix(channels): point the Discord missing-dependency hint at the discord extra discord.py is the only IM transport that is not a core dependency — it ships in the optional `discord` extra, which detect_uv_extras.py, backend/Dockerfile and deploy.sh already expand. Its ImportError hint told users to run `uv add discord.py` instead, which rewrites pyproject.toml/uv.lock in a tree every restart syncs with `--locked`. Mirror the buzz hint wording and note the extra in config.example.yaml next to the buzz block. --------- Co-authored-by: huyengiang101086-spec <270684046+huyengiang101086-spec@users.noreply.github.com> --- backend/app/channels/discord.py | 2 +- backend/tests/test_detect_uv_extras.py | 29 ++++++++++++++++++++++++++ backend/tests/test_discord_channel.py | 17 +++++++++++++++ config.example.yaml | 2 ++ scripts/detect_uv_extras.py | 1 + 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/backend/app/channels/discord.py b/backend/app/channels/discord.py index 784dc5cb6a..213f067626 100644 --- a/backend/app/channels/discord.py +++ b/backend/app/channels/discord.py @@ -108,7 +108,7 @@ class DiscordChannel(Channel): try: import discord except ImportError: - logger.error("discord.py is not installed. Install it with: uv add discord.py") + logger.error("discord.py is not installed. The Discord channel needs the 'discord' extra: run `cd backend && uv sync --extra discord`.") return if not self._bot_token: diff --git a/backend/tests/test_detect_uv_extras.py b/backend/tests/test_detect_uv_extras.py index 8c99d903f1..df5364c3c7 100644 --- a/backend/tests/test_detect_uv_extras.py +++ b/backend/tests/test_detect_uv_extras.py @@ -285,6 +285,35 @@ def test_detect_from_config_ignores_commented_buzz_block(tmp_path): assert detect.detect_from_config(cfg) == [] +def test_detect_from_config_discord_via_channels_enabled(tmp_path): + cfg = tmp_path / "config.yaml" + cfg.write_text( + "channels:\n discord:\n enabled: true\n bot_token: $DISCORD_BOT_TOKEN\n", + ) + assert detect.detect_from_config(cfg) == ["discord"] + + +def test_detect_from_config_discord_disabled_returns_no_extras(tmp_path): + cfg = tmp_path / "config.yaml" + cfg.write_text("channels:\n discord:\n enabled: false\n") + assert detect.detect_from_config(cfg) == [] + + +def test_detect_from_config_ignores_commented_discord_block(tmp_path): + """Mirrors the commented example block shipped in config.example.yaml.""" + cfg = tmp_path / "config.yaml" + cfg.write_text( + "# channels:\n# discord:\n# enabled: false\n# bot_token: $DISCORD_BOT_TOKEN\ndatabase:\n backend: sqlite\n", + ) + assert detect.detect_from_config(cfg) == [] + + +def test_detect_from_config_combines_discord_with_postgres(tmp_path): + cfg = tmp_path / "config.yaml" + cfg.write_text("database:\n backend: postgres\nchannels:\n discord:\n enabled: true\n") + assert detect.detect_from_config(cfg) == ["discord", "postgres"] + + def test_detect_from_config_memory_stream_bridge_returns_no_extras(tmp_path): cfg = tmp_path / "config.yaml" cfg.write_text("stream_bridge:\n type: memory\n queue_maxsize: 256\n") diff --git a/backend/tests/test_discord_channel.py b/backend/tests/test_discord_channel.py index 5304f11b6c..810f688572 100644 --- a/backend/tests/test_discord_channel.py +++ b/backend/tests/test_discord_channel.py @@ -6,6 +6,7 @@ import asyncio import builtins import gc import json +import sys import threading import weakref from types import SimpleNamespace @@ -34,6 +35,22 @@ def test_discord_channel_init() -> None: assert channel.name == "discord" +@pytest.mark.asyncio +async def test_start_without_discord_module_points_at_the_extra(caplog) -> None: + """discord.py ships in the optional ``discord`` extra, so the missing-dependency + hint must send the operator to ``uv sync --extra discord`` — the same command + ``scripts/detect_uv_extras.py`` and the Docker builds already use — rather than + ``uv add``, which would rewrite pyproject.toml and diverge from ``uv sync --locked``.""" + channel = DiscordChannel(bus=MessageBus(), config={"bot_token": "token"}) + + with caplog.at_level("ERROR", logger="app.channels.discord"), patch.dict(sys.modules, {"discord": None}): + await channel.start() + + assert any("uv sync --extra discord" in record.message for record in caplog.records) + assert not any("uv add" in record.message for record in caplog.records) + assert channel._running is False + + # --------------------------------------------------------------------------- # thread-mapping persistence across restart (#2897) # --------------------------------------------------------------------------- diff --git a/config.example.yaml b/config.example.yaml index 013ee2644e..068be4033a 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2985,6 +2985,8 @@ run_ownership: # allowed_users: [] # empty = allow all # card_template_id: "" # Optional: AI Card template ID for streaming updates # +# # Discord joins as a bot; requires the `discord` dependency extra +# # (uv sync --extra discord) for discord.py. # discord: # enabled: false # bot_token: $DISCORD_BOT_TOKEN diff --git a/scripts/detect_uv_extras.py b/scripts/detect_uv_extras.py index eaee579514..09d3af3629 100755 --- a/scripts/detect_uv_extras.py +++ b/scripts/detect_uv_extras.py @@ -13,6 +13,7 @@ Order of resolution: - stream_bridge.type == redis -> redis - tools[].name == browser_navigate -> browser - sandbox.ownership.type == redis -> redis + - channels.discord.enabled == true -> discord - channels.buzz.enabled == true -> buzz - models[].use == langchain_ollama:* -> ollama 3. Runtime environment toggles that enable optional backends: