feat: voice profiles, rubberband FX, per-bot plugin filtering
- Add rubberband package to container for pitch-shifting FX - Split FX chain: rubberband CLI for pitch, ffmpeg for filters - Configurable voice profile (voice, fx, piper params) in [voice] - Extra bots inherit voice config (minus trigger) for own TTS - Greeting is voice-only, spoken directly by the greeting bot - Per-bot only_plugins/except_plugins filtering on Mumble - Alias plugin, core plugin tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
92
tests/test_core.py
Normal file
92
tests/test_core.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Tests for the core plugin."""
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# -- Load plugin module directly ---------------------------------------------
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("core", "plugins/core.py")
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["core"] = _mod
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
|
||||
# -- Fakes -------------------------------------------------------------------
|
||||
|
||||
|
||||
class _FakeRegistry:
|
||||
def __init__(self):
|
||||
self._bots: dict = {}
|
||||
|
||||
|
||||
class _FakeBot:
|
||||
def __init__(self, *, mumble: bool = False):
|
||||
self.replied: list[str] = []
|
||||
self.registry = _FakeRegistry()
|
||||
self.nick = "derp"
|
||||
self._receive_sound = False
|
||||
if mumble:
|
||||
self._mumble = MagicMock()
|
||||
|
||||
async def reply(self, message, text: str) -> None:
|
||||
self.replied.append(text)
|
||||
|
||||
|
||||
def _make_listener():
|
||||
"""Create a fake listener bot (merlin) with _receive_sound=True."""
|
||||
listener = _FakeBot(mumble=True)
|
||||
listener.nick = "merlin"
|
||||
listener._receive_sound = True
|
||||
return listener
|
||||
|
||||
|
||||
class _Msg:
|
||||
def __init__(self, text="!deaf"):
|
||||
self.text = text
|
||||
self.nick = "Alice"
|
||||
self.target = "0"
|
||||
self.is_channel = True
|
||||
self.prefix = "Alice"
|
||||
|
||||
|
||||
# -- Tests -------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDeafCommand:
|
||||
def test_deaf_targets_listener(self):
|
||||
"""!deaf toggles the listener bot (merlin), not the calling bot."""
|
||||
bot = _FakeBot(mumble=True)
|
||||
listener = _make_listener()
|
||||
bot.registry._bots = {"derp": bot, "merlin": listener}
|
||||
listener._mumble.users.myself.get.return_value = False
|
||||
msg = _Msg(text="!deaf")
|
||||
asyncio.run(_mod.cmd_deaf(bot, msg))
|
||||
listener._mumble.users.myself.deafen.assert_called_once()
|
||||
assert any("merlin" in r and "deafened" in r for r in bot.replied)
|
||||
|
||||
def test_deaf_toggle_off(self):
|
||||
bot = _FakeBot(mumble=True)
|
||||
listener = _make_listener()
|
||||
bot.registry._bots = {"derp": bot, "merlin": listener}
|
||||
listener._mumble.users.myself.get.return_value = True
|
||||
msg = _Msg(text="!deaf")
|
||||
asyncio.run(_mod.cmd_deaf(bot, msg))
|
||||
listener._mumble.users.myself.undeafen.assert_called_once()
|
||||
listener._mumble.users.myself.unmute.assert_called_once()
|
||||
assert any("merlin" in r and "undeafened" in r for r in bot.replied)
|
||||
|
||||
def test_deaf_non_mumble_silent(self):
|
||||
bot = _FakeBot(mumble=False)
|
||||
msg = _Msg(text="!deaf")
|
||||
asyncio.run(_mod.cmd_deaf(bot, msg))
|
||||
assert bot.replied == []
|
||||
|
||||
def test_deaf_fallback_no_listener(self):
|
||||
"""Falls back to calling bot when no listener is registered."""
|
||||
bot = _FakeBot(mumble=True)
|
||||
bot._mumble.users.myself.get.return_value = False
|
||||
msg = _Msg(text="!deaf")
|
||||
asyncio.run(_mod.cmd_deaf(bot, msg))
|
||||
bot._mumble.users.myself.deafen.assert_called_once()
|
||||
Reference in New Issue
Block a user