fix: delegate !similar playback to music bot, not calling bot
When merlin ran !similar, music operations (fade, queue, play loop) targeted merlin instead of derp, causing audio to play over derp's stream. New _music_bot() helper resolves the DJ bot via active state or plugin filter config, so playback always routes to derp. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
@@ -21,14 +20,17 @@ _spec.loader.exec_module(_mod)
|
||||
class _FakeRegistry:
|
||||
def __init__(self):
|
||||
self._modules: dict = {}
|
||||
self._bots: dict = {}
|
||||
|
||||
|
||||
class _FakeBot:
|
||||
def __init__(self, *, api_key: str = "test-key"):
|
||||
def __init__(self, *, api_key: str = "test-key", name: str = "derp"):
|
||||
self.replied: list[str] = []
|
||||
self.config: dict = {"lastfm": {"api_key": api_key}} if api_key else {}
|
||||
self._pstate: dict = {}
|
||||
self._only_plugins: set[str] | None = None
|
||||
self.registry = _FakeRegistry()
|
||||
self._username = name
|
||||
|
||||
async def reply(self, message, text: str) -> None:
|
||||
self.replied.append(text)
|
||||
@@ -615,6 +617,47 @@ class TestCmdSimilar:
|
||||
asyncio.run(_mod.cmd_similar(bot, msg))
|
||||
assert any("Playing 1 similar" in r for r in bot.replied)
|
||||
|
||||
def test_cross_bot_delegates_to_music_bot(self):
|
||||
"""When merlin runs !similar, playback starts on derp (music bot)."""
|
||||
# derp = music bot (has active music state)
|
||||
derp = _FakeBot(api_key="test-key", name="derp")
|
||||
derp._only_plugins = {"music", "voice"}
|
||||
derp._pstate["music"] = {
|
||||
"current": _FakeTrack(title="Tool - Lateralus"),
|
||||
"queue": [],
|
||||
}
|
||||
# merlin = calling bot (no music plugin)
|
||||
merlin = _FakeBot(api_key="test-key", name="merlin")
|
||||
shared_reg = _FakeRegistry()
|
||||
shared_reg._bots = {"derp": derp, "merlin": merlin}
|
||||
derp.registry = shared_reg
|
||||
merlin.registry = shared_reg
|
||||
|
||||
artists = [{"name": "Deftones", "match": "0.8"}]
|
||||
fake_tracks = [_FakeTrack(url="http://yt/1", title="Song 1")]
|
||||
|
||||
music_mod = MagicMock()
|
||||
music_mod._ps.return_value = {
|
||||
"queue": [], "current": None, "task": None,
|
||||
}
|
||||
music_mod._fade_and_cancel = AsyncMock()
|
||||
music_mod._ensure_loop = MagicMock()
|
||||
shared_reg._modules["music"] = music_mod
|
||||
|
||||
msg = _Msg(text="!similar Tool")
|
||||
with patch.object(_mod, "_get_similar_tracks", return_value=[]), \
|
||||
patch.object(_mod, "_get_similar_artists", return_value=artists), \
|
||||
patch.object(_mod, "_resolve_playlist",
|
||||
return_value=fake_tracks):
|
||||
asyncio.run(_mod.cmd_similar(merlin, msg))
|
||||
|
||||
# Music ops must target derp, not merlin
|
||||
music_mod._fade_and_cancel.assert_called_once_with(derp, duration=3.0)
|
||||
music_mod._ensure_loop.assert_called_once_with(derp, fade_in=True)
|
||||
music_mod._ps.assert_called_with(derp)
|
||||
# Reply still goes through merlin (the bot the user talked to)
|
||||
assert any("Playing 1 similar" in r for r in merlin.replied)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestCmdTags
|
||||
@@ -694,6 +737,46 @@ class TestCmdTags:
|
||||
assert any("Lateralus:" in r for r in bot.replied)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestMusicBot
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMusicBot:
|
||||
def test_returns_self_when_active(self):
|
||||
"""Returns calling bot when it has active music state."""
|
||||
bot = _FakeBot()
|
||||
bot._pstate["music"] = {"current": _FakeTrack(title="X"), "queue": []}
|
||||
assert _mod._music_bot(bot) is bot
|
||||
|
||||
def test_returns_peer_with_active_state(self):
|
||||
"""Returns peer bot that has music playing."""
|
||||
derp = _FakeBot(name="derp")
|
||||
derp._pstate["music"] = {"current": _FakeTrack(title="X"), "queue": []}
|
||||
merlin = _FakeBot(name="merlin")
|
||||
reg = _FakeRegistry()
|
||||
reg._bots = {"derp": derp, "merlin": merlin}
|
||||
derp.registry = reg
|
||||
merlin.registry = reg
|
||||
assert _mod._music_bot(merlin) is derp
|
||||
|
||||
def test_falls_back_to_only_plugins(self):
|
||||
"""Returns bot with only_plugins containing 'music' when no active state."""
|
||||
derp = _FakeBot(name="derp")
|
||||
derp._only_plugins = {"music", "voice"}
|
||||
merlin = _FakeBot(name="merlin")
|
||||
reg = _FakeRegistry()
|
||||
reg._bots = {"derp": derp, "merlin": merlin}
|
||||
derp.registry = reg
|
||||
merlin.registry = reg
|
||||
assert _mod._music_bot(merlin) is derp
|
||||
|
||||
def test_returns_self_as_last_resort(self):
|
||||
"""Returns calling bot when no peer has music state or filters."""
|
||||
bot = _FakeBot()
|
||||
assert _mod._music_bot(bot) is bot
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestParseTitle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user