diff --git a/nix/tui.nix b/nix/tui.nix index b8ead660f77c..31d5220274e2 100644 --- a/nix/tui.nix +++ b/nix/tui.nix @@ -4,7 +4,7 @@ let src = ../ui-tui; npmDeps = pkgs.fetchNpmDeps { inherit src; - hash = "sha256-kJdrhcyCtRTecQBMYbv05ZBD0trnKRbpKhej5eGDJpw="; + hash = "sha256-q3Dqx7B9AK/H7ji/XoMkLOxUNH0uTUqoemu+hSiqr5I="; }; npm = hermesNpmLib.mkNpmPassthru { folder = "ui-tui"; attr = "tui"; pname = "hermes-tui"; }; diff --git a/nix/web.nix b/nix/web.nix index 3bbc2998d5ed..d577f41126e8 100644 --- a/nix/web.nix +++ b/nix/web.nix @@ -4,7 +4,7 @@ let src = ../web; npmDeps = pkgs.fetchNpmDeps { inherit src; - hash = "sha256-ZIcAGppxrXBIIdRgV8V2HHFVkwzFjwUgLLfc5wDDLo8="; + hash = "sha256-peA7M8lvRRVnsM6vl4QbYEr0ElEaGU4zsCENnq8TLBc="; }; npm = hermesNpmLib.mkNpmPassthru { folder = "web"; attr = "web"; pname = "hermes-web"; }; diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 410e00e48558..87346300125f 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -1906,13 +1906,13 @@ async def join_voice_channel(self, channel) -> bool: try: import nacl # noqa: F401 — presence check only except ImportError: - logger.warning( - "Discord voice channel join failed: PyNaCl is not installed. " - "The `voice` extra no longer ships PyNaCl (vulnerable pin). " - "Install it manually (`pip install PyNaCl>=1.6.2`) to use " - "Discord voice channels." - ) - return False + # Raise so GatewayRunner._handle_voice_channel_join can surface the + # PyNaCl-specific install guidance (return False falls through to the + # generic "Check bot permissions" message — Codex PR #25). + raise RuntimeError( + "PyNaCl is not installed. The voice extra no longer ships PyNaCl " + "(vulnerable pin). Install manually: pip install PyNaCl>=1.6.2" + ) from None async with self._voice_locks.setdefault(guild_id, asyncio.Lock()): # Already connected in this guild? diff --git a/tests/gateway/test_discord_race_polish.py b/tests/gateway/test_discord_race_polish.py index 95e09c0303ff..dbbb77a0d1d1 100644 --- a/tests/gateway/test_discord_race_polish.py +++ b/tests/gateway/test_discord_race_polish.py @@ -63,7 +63,7 @@ async def slow_connect(self): from plugins.platforms.discord import adapter as discord_mod # Ensure the PyNaCl guard inside join_voice_channel is a no-op for this # test (the test venv may not have nacl). Use patch.dict so the mock - # doesn't leak into other test files in the same pytest session. + # does not leak into other test files in the same pytest session. with patch.dict("sys.modules", {"nacl": MagicMock()}): with patch.object(discord_mod, "VoiceReceiver", MagicMock(return_value=MagicMock(start=lambda: None))): @@ -88,11 +88,11 @@ async def slow_connect(self): # --------------------------------------------------------------------------- @pytest.mark.asyncio -async def test_join_voice_returns_false_when_pynacl_missing(): - """When PyNaCl is not installed, join_voice_channel must fail fast with - a logged warning instead of crashing inside channel.connect() with an - opaque missing-module error. The `voice` extra no longer ships PyNaCl - (vulnerable pin), so this guard is the user-facing safety net.""" +async def test_join_voice_raises_when_pynacl_missing(): + """When PyNaCl is not installed, join_voice_channel must raise RuntimeError + so GatewayRunner._handle_voice_channel_join can surface the PyNaCl-specific + install guidance. Returning False would fall through to the generic + "Check bot permissions" message instead.""" import builtins adapter = _make_adapter() @@ -108,9 +108,9 @@ def _block_nacl(name, *args, **kwargs): channel.guild.id = 123 with patch("builtins.__import__", side_effect=_block_nacl): - result = await adapter.join_voice_channel(channel) + with pytest.raises(RuntimeError, match="PyNaCl is not installed"): + await adapter.join_voice_channel(channel) - assert result is False # channel.connect() must NOT have been called — the guard fires before it. channel.connect.assert_not_called() diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 03fafd4ae0c5..5553300e12ae 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -538,7 +538,12 @@ def active_features() -> list[str]: """ active = [] for feature, specs in LAZY_DEPS.items(): - if any(_is_present(s) for s in specs): + # Check only the first spec (the primary/unique package for this feature). + # Checking any spec would cause false activations when a transitive package + # (e.g. aiohttp, cbor2, starlette) is installed by a different feature — + # see Codex review on PR #25. By convention the first tuple element must be + # a package installed exclusively by this feature. + if _is_present(specs[0]): active.append(feature) return active