- 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>
213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
"""Tests for the alias plugin."""
|
|
|
|
import asyncio
|
|
import importlib.util
|
|
import sys
|
|
|
|
from derp.plugin import PluginRegistry
|
|
|
|
# -- Load plugin module directly ---------------------------------------------
|
|
|
|
_spec = importlib.util.spec_from_file_location("alias", "plugins/alias.py")
|
|
_mod = importlib.util.module_from_spec(_spec)
|
|
sys.modules["alias"] = _mod
|
|
_spec.loader.exec_module(_mod)
|
|
|
|
|
|
# -- Fakes -------------------------------------------------------------------
|
|
|
|
|
|
class _FakeState:
|
|
def __init__(self):
|
|
self._store: dict[str, dict[str, str]] = {}
|
|
|
|
def get(self, ns: str, key: str) -> str | None:
|
|
return self._store.get(ns, {}).get(key)
|
|
|
|
def set(self, ns: str, key: str, value: str) -> None:
|
|
self._store.setdefault(ns, {})[key] = value
|
|
|
|
def delete(self, ns: str, key: str) -> bool:
|
|
if ns in self._store and key in self._store[ns]:
|
|
del self._store[ns][key]
|
|
return True
|
|
return False
|
|
|
|
def keys(self, ns: str) -> list[str]:
|
|
return list(self._store.get(ns, {}).keys())
|
|
|
|
def clear(self, ns: str) -> int:
|
|
count = len(self._store.get(ns, {}))
|
|
self._store.pop(ns, None)
|
|
return count
|
|
|
|
|
|
class _FakeBot:
|
|
def __init__(self, *, admin: bool = False):
|
|
self.replied: list[str] = []
|
|
self.state = _FakeState()
|
|
self.registry = PluginRegistry()
|
|
self._admin = admin
|
|
|
|
async def reply(self, message, text: str) -> None:
|
|
self.replied.append(text)
|
|
|
|
def _is_admin(self, message) -> bool:
|
|
return self._admin
|
|
|
|
|
|
class _Msg:
|
|
def __init__(self, text="!alias"):
|
|
self.text = text
|
|
self.nick = "Alice"
|
|
self.target = "#test"
|
|
self.is_channel = True
|
|
self.prefix = "Alice!~alice@host"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestAliasAdd
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAliasAdd:
|
|
def test_add_creates_alias(self):
|
|
bot = _FakeBot()
|
|
# Register a target command
|
|
async def _noop(b, m): pass
|
|
bot.registry.register_command("skip", _noop, plugin="music")
|
|
msg = _Msg(text="!alias add s skip")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert bot.state.get("alias", "s") == "skip"
|
|
assert any("s -> skip" in r for r in bot.replied)
|
|
|
|
def test_add_rejects_existing_command(self):
|
|
bot = _FakeBot()
|
|
async def _noop(b, m): pass
|
|
bot.registry.register_command("skip", _noop, plugin="music")
|
|
msg = _Msg(text="!alias add skip stop")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("already a registered command" in r for r in bot.replied)
|
|
assert bot.state.get("alias", "skip") is None
|
|
|
|
def test_add_rejects_chaining(self):
|
|
bot = _FakeBot()
|
|
async def _noop(b, m): pass
|
|
bot.registry.register_command("skip", _noop, plugin="music")
|
|
bot.state.set("alias", "sk", "skip")
|
|
msg = _Msg(text="!alias add x sk")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("no chaining" in r for r in bot.replied)
|
|
|
|
def test_add_rejects_unknown_target(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias add s nonexistent")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("unknown command" in r for r in bot.replied)
|
|
|
|
def test_add_lowercases_name(self):
|
|
bot = _FakeBot()
|
|
async def _noop(b, m): pass
|
|
bot.registry.register_command("skip", _noop, plugin="music")
|
|
msg = _Msg(text="!alias add S skip")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert bot.state.get("alias", "s") == "skip"
|
|
|
|
def test_add_missing_args(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias add s")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Usage" in r for r in bot.replied)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestAliasDel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAliasDel:
|
|
def test_del_removes_alias(self):
|
|
bot = _FakeBot()
|
|
bot.state.set("alias", "s", "skip")
|
|
msg = _Msg(text="!alias del s")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert bot.state.get("alias", "s") is None
|
|
assert any("removed" in r for r in bot.replied)
|
|
|
|
def test_del_nonexistent(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias del x")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("no alias" in r for r in bot.replied)
|
|
|
|
def test_del_missing_name(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias del")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Usage" in r for r in bot.replied)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestAliasList
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAliasList:
|
|
def test_list_empty(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias list")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("No aliases" in r for r in bot.replied)
|
|
|
|
def test_list_shows_entries(self):
|
|
bot = _FakeBot()
|
|
bot.state.set("alias", "s", "skip")
|
|
bot.state.set("alias", "np", "nowplaying")
|
|
msg = _Msg(text="!alias list")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("s -> skip" in r for r in bot.replied)
|
|
assert any("np -> nowplaying" in r for r in bot.replied)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestAliasClear
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAliasClear:
|
|
def test_clear_as_admin(self):
|
|
bot = _FakeBot(admin=True)
|
|
bot.state.set("alias", "s", "skip")
|
|
bot.state.set("alias", "np", "nowplaying")
|
|
msg = _Msg(text="!alias clear")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Cleared 2" in r for r in bot.replied)
|
|
assert bot.state.keys("alias") == []
|
|
|
|
def test_clear_denied_non_admin(self):
|
|
bot = _FakeBot(admin=False)
|
|
bot.state.set("alias", "s", "skip")
|
|
msg = _Msg(text="!alias clear")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Permission denied" in r for r in bot.replied)
|
|
assert bot.state.get("alias", "s") == "skip"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestAliasUsage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAliasUsage:
|
|
def test_no_subcommand(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Usage" in r for r in bot.replied)
|
|
|
|
def test_unknown_subcommand(self):
|
|
bot = _FakeBot()
|
|
msg = _Msg(text="!alias foo")
|
|
asyncio.run(_mod.cmd_alias(bot, msg))
|
|
assert any("Usage" in r for r in bot.replied)
|