mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
PR #12558 was heavy for what the fix actually is — essay-length comments, a dedicated helper method where a setdefault would do, and a source-inspection test with no real behavior coverage. The genuine code change is ~5 lines of new logic (1 field, 2 async with, an on_ready wait block). Trimmed: - Replaced the 12-line _voice_lock_for helper with a setdefault one-liner at each call site (join_voice_channel, leave_voice_channel). - Collapsed the 12-line comment on on_message's _ready_event wait to 3 lines. Dropped the warning log on timeout — pass-on-timeout is fine; if on_ready hangs that long, the bot is already broken and the log wouldn't help. - Dropped the source-inspection test (greps the module source for expected substrings). It was low-value scaffolding; the voice-serialization test covers actual behavior. Net: -73 lines vs PR #12558. Same two guarantees preserved, same test passes (verified by stashing the fix and confirming failure).
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""Discord adapter race polish: concurrent join_voice_channel must not
|
|
double-invoke channel.connect() on the same guild."""
|
|
|
|
import asyncio
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
|
|
|
|
def _make_adapter():
|
|
from gateway.platforms.discord import DiscordAdapter
|
|
|
|
adapter = object.__new__(DiscordAdapter)
|
|
adapter._platform = Platform.DISCORD
|
|
adapter.config = PlatformConfig(enabled=True, token="t")
|
|
adapter._ready_event = asyncio.Event()
|
|
adapter._allowed_user_ids = set()
|
|
adapter._allowed_role_ids = set()
|
|
adapter._voice_clients = {}
|
|
adapter._voice_locks = {}
|
|
adapter._voice_receivers = {}
|
|
adapter._voice_listen_tasks = {}
|
|
adapter._voice_timeout_tasks = {}
|
|
adapter._voice_text_channels = {}
|
|
adapter._voice_sources = {}
|
|
adapter._client = MagicMock()
|
|
return adapter
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_joins_do_not_double_connect():
|
|
"""Two concurrent join_voice_channel calls on the same guild must
|
|
serialize through the per-guild lock — only ONE channel.connect()
|
|
actually fires; the second sees the _voice_clients entry the first
|
|
just installed."""
|
|
adapter = _make_adapter()
|
|
|
|
connect_count = [0]
|
|
release = asyncio.Event()
|
|
|
|
class FakeVC:
|
|
def __init__(self, channel):
|
|
self.channel = channel
|
|
|
|
def is_connected(self):
|
|
return True
|
|
|
|
async def move_to(self, _channel):
|
|
return None
|
|
|
|
async def slow_connect(self):
|
|
connect_count[0] += 1
|
|
await release.wait()
|
|
return FakeVC(self)
|
|
|
|
channel = MagicMock()
|
|
channel.id = 111
|
|
channel.guild.id = 42
|
|
channel.connect = lambda: slow_connect(channel)
|
|
|
|
from gateway.platforms import discord as discord_mod
|
|
with patch.object(discord_mod, "VoiceReceiver",
|
|
MagicMock(return_value=MagicMock(start=lambda: None))):
|
|
with patch.object(discord_mod.asyncio, "ensure_future",
|
|
lambda _c: asyncio.create_task(asyncio.sleep(0))):
|
|
t1 = asyncio.create_task(adapter.join_voice_channel(channel))
|
|
t2 = asyncio.create_task(adapter.join_voice_channel(channel))
|
|
await asyncio.sleep(0.05)
|
|
release.set()
|
|
r1, r2 = await asyncio.gather(t1, t2)
|
|
|
|
assert connect_count[0] == 1, (
|
|
f"expected 1 channel.connect() call, got {connect_count[0]} — "
|
|
"per-guild lock is not serializing join_voice_channel"
|
|
)
|
|
assert r1 is True and r2 is True
|
|
assert 42 in adapter._voice_clients
|