major-domo-v2/tests/test_utils_mentions.py
Cal Corum cf5df1a619 Fix custom command mentions not triggering notifications
- Use channel.send() instead of followup.send() for custom command output
  (webhook-based followup messages don't trigger mention notifications)
- Add ephemeral "Sending..." confirmation to satisfy interaction response
- Add utils/mentions.py for converting text @mentions to Discord format
- Add tests for mention conversion utility

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 15:45:38 -06:00

215 lines
6.6 KiB
Python

"""
Tests for the mention conversion utilities.
These tests verify that human-readable @mentions are properly converted
to Discord's ID-based format for triggering notifications.
"""
import pytest
from unittest.mock import MagicMock, AsyncMock
from utils.mentions import convert_mentions, convert_mentions_async
class TestConvertMentions:
"""Tests for the synchronous convert_mentions function."""
def test_converts_role_mention(self):
"""Test that @RoleName is converted to <@&role_id>."""
# Create mock guild with a role
mock_role = MagicMock()
mock_role.name = "Waikiki Whale Sharks"
mock_role.id = 123456789
mock_guild = MagicMock()
mock_guild.roles = [mock_role]
mock_guild.members = []
content = "@Waikiki Whale Sharks"
result = convert_mentions(content, mock_guild)
assert result == "<@&123456789>"
def test_converts_role_mention_case_insensitive(self):
"""Test that role matching is case-insensitive."""
mock_role = MagicMock()
mock_role.name = "Waikiki Whale Sharks"
mock_role.id = 123456789
mock_guild = MagicMock()
mock_guild.roles = [mock_role]
mock_guild.members = []
content = "@waikiki whale sharks"
result = convert_mentions(content, mock_guild)
assert result == "<@&123456789>"
def test_converts_user_mention_by_display_name(self):
"""Test that @DisplayName is converted to <@user_id>."""
mock_member = MagicMock()
mock_member.display_name = "CalCorum"
mock_member.name = "cal"
mock_member.id = 987654321
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = [mock_member]
content = "@CalCorum"
result = convert_mentions(content, mock_guild)
assert result == "<@987654321>"
def test_converts_user_mention_by_username(self):
"""Test that @username is converted to <@user_id>."""
mock_member = MagicMock()
mock_member.display_name = "Cal Corum"
mock_member.name = "calcorum"
mock_member.id = 987654321
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = [mock_member]
content = "@calcorum"
result = convert_mentions(content, mock_guild)
assert result == "<@987654321>"
def test_converts_multiple_mentions(self):
"""Test that multiple mentions in same content are all converted."""
mock_role1 = MagicMock()
mock_role1.name = "Team A"
mock_role1.id = 111
mock_role2 = MagicMock()
mock_role2.name = "Team B"
mock_role2.id = 222
mock_guild = MagicMock()
mock_guild.roles = [mock_role1, mock_role2]
mock_guild.members = []
content = "@Team A vs @Team B"
result = convert_mentions(content, mock_guild)
assert result == "<@&111> vs <@&222>"
def test_preserves_already_formatted_mentions(self):
"""Test that <@123> and <@&456> are not modified."""
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = []
content = "Already formatted: <@123456789> and <@&987654321>"
result = convert_mentions(content, mock_guild)
assert result == content
def test_preserves_unrecognized_mentions(self):
"""Test that @mentions with no match are left unchanged."""
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = []
content = "@UnknownRole says hello"
result = convert_mentions(content, mock_guild)
assert result == "@UnknownRole says hello"
def test_handles_none_guild(self):
"""Test that None guild returns content unchanged."""
content = "@SomeRole"
result = convert_mentions(content, None)
assert result == content
def test_handles_empty_content(self):
"""Test that empty content returns empty."""
mock_guild = MagicMock()
result = convert_mentions("", mock_guild)
assert result == ""
def test_handles_mentions_with_punctuation(self):
"""Test that mentions followed by punctuation are handled."""
mock_role = MagicMock()
mock_role.name = "TestRole"
mock_role.id = 123
mock_guild = MagicMock()
mock_guild.roles = [mock_role]
mock_guild.members = []
content = "Hello @TestRole!"
result = convert_mentions(content, mock_guild)
assert result == "Hello <@&123>!"
def test_role_takes_priority_over_user(self):
"""Test that when a name matches both role and user, role wins."""
mock_role = MagicMock()
mock_role.name = "SameName"
mock_role.id = 111
mock_member = MagicMock()
mock_member.display_name = "SameName"
mock_member.name = "samename"
mock_member.id = 222
mock_guild = MagicMock()
mock_guild.roles = [mock_role]
mock_guild.members = [mock_member]
content = "@SameName"
result = convert_mentions(content, mock_guild)
# Role should be matched first
assert result == "<@&111>"
class TestConvertMentionsAsync:
"""Tests for the async convert_mentions_async function."""
@pytest.mark.asyncio
async def test_async_converts_role_mention(self):
"""Test async version converts mentions."""
mock_role = MagicMock()
mock_role.name = "TestRole"
mock_role.id = 123
mock_guild = MagicMock()
mock_guild.roles = [mock_role]
mock_guild.members = []
mock_guild.chunked = True
content = "@TestRole"
result = await convert_mentions_async(content, mock_guild)
assert result == "<@&123>"
@pytest.mark.asyncio
async def test_async_chunks_guild_when_requested(self):
"""Test that guild.chunk() is called when fetch_members=True and not chunked."""
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = []
mock_guild.chunked = False
mock_guild.chunk = AsyncMock()
await convert_mentions_async("@Test", mock_guild, fetch_members=True)
mock_guild.chunk.assert_called_once()
@pytest.mark.asyncio
async def test_async_skips_chunk_when_already_chunked(self):
"""Test that guild.chunk() is not called when already chunked."""
mock_guild = MagicMock()
mock_guild.roles = []
mock_guild.members = []
mock_guild.chunked = True
mock_guild.chunk = AsyncMock()
await convert_mentions_async("@Test", mock_guild, fetch_members=True)
mock_guild.chunk.assert_not_called()