mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-28 18:31:38 +00:00
b1a8b2e5d5
- Added `normalize_command_content` method to `CommandManager` to streamline command prefix handling, ensuring consistent processing of incoming messages. - Updated `format_keyword_response`, `check_keywords`, and `execute` methods to utilize the new normalization logic, reducing redundancy and improving clarity. - Refactored related command classes to remove legacy prefix stripping logic, enhancing maintainability and ensuring all commands adhere to the new normalization approach. - Enhanced tests to cover various scenarios for command prefix handling, ensuring robust functionality across different configurations.
335 lines
13 KiB
Python
335 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit tests for command prefix functionality
|
|
Tests that all commands properly handle command prefixes when enabled
|
|
"""
|
|
|
|
from configparser import ConfigParser
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from modules.command_manager import CommandManager
|
|
from modules.commands.base_command import BaseCommand
|
|
from modules.commands.hello_command import HelloCommand
|
|
from modules.commands.ping_command import PingCommand
|
|
from modules.models import MeshMessage
|
|
|
|
|
|
class MockTestCommand(BaseCommand):
|
|
"""Mock command for testing prefix functionality"""
|
|
name = "test"
|
|
keywords = ['test', 't']
|
|
description = "Test command"
|
|
category = "test"
|
|
|
|
async def execute(self, message: MeshMessage) -> bool:
|
|
"""Execute the test command (required by abstract base class)"""
|
|
return True
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_bot():
|
|
"""Create a mock bot instance"""
|
|
bot = Mock()
|
|
bot.logger = Mock()
|
|
bot.logger.debug = Mock()
|
|
bot.logger.info = Mock()
|
|
bot.logger.warning = Mock()
|
|
bot.logger.error = Mock()
|
|
bot.config = ConfigParser()
|
|
bot.config.add_section('Bot')
|
|
bot.config.add_section('Channels')
|
|
bot.config.set('Channels', 'monitor_channels', 'general')
|
|
bot.config.set('Channels', 'respond_to_dms', 'true')
|
|
bot.meshcore = None
|
|
bot.translator = None
|
|
bot.rate_limiter = Mock()
|
|
bot.rate_limiter.can_send = Mock(return_value=True)
|
|
bot.bot_tx_rate_limiter = Mock()
|
|
bot.bot_tx_rate_limiter.wait_for_tx = Mock()
|
|
bot.tx_delay_ms = 0
|
|
bot.bot_root = Path("/tmp")
|
|
bot._local_root = None # CommandManager uses bot_root / local / commands
|
|
return bot
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_message():
|
|
"""Create a mock message"""
|
|
return MeshMessage(
|
|
content="test",
|
|
sender_id="TestUser",
|
|
channel="general",
|
|
is_dm=False
|
|
)
|
|
|
|
|
|
def _make_manager(mock_bot, commands=None):
|
|
with patch('modules.command_manager.PluginLoader') as mock_loader_class:
|
|
mock_loader = Mock()
|
|
mock_loader.load_all_plugins = Mock(return_value=commands or {})
|
|
mock_loader_class.return_value = mock_loader
|
|
return CommandManager(mock_bot)
|
|
|
|
|
|
def _msg(content: str) -> MeshMessage:
|
|
return MeshMessage(
|
|
content=content,
|
|
sender_id="TestUser",
|
|
channel="general",
|
|
is_dm=False,
|
|
)
|
|
|
|
|
|
class ExecuteOnlyCommand(BaseCommand):
|
|
"""Command that handles its own response via execute() (no response format)."""
|
|
|
|
name = "path"
|
|
keywords = ['path', 'p']
|
|
description = "Path command for prefix regression tests"
|
|
category = "test"
|
|
|
|
async def execute(self, message: MeshMessage) -> bool:
|
|
return True
|
|
|
|
|
|
class TestCommandPrefix:
|
|
"""Test command prefix functionality"""
|
|
|
|
def test_no_prefix_allows_commands(self, mock_bot, mock_message):
|
|
"""Test that without prefix configured, commands work normally"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
assert manager.normalize_command_content(mock_message) is True
|
|
assert command.matches_keyword(mock_message) is True
|
|
|
|
prefixed = _msg("!test")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
def test_prefix_required_when_configured(self, mock_bot, mock_message):
|
|
"""Test that when prefix is configured, it's required"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
prefixed = _msg("!test")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
unprefixed = _msg("test")
|
|
assert manager.normalize_command_content(unprefixed) is False
|
|
|
|
def test_dot_prefix(self, mock_bot, mock_message):
|
|
"""Test dot prefix (e.g., .ping)"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '.')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
prefixed = _msg(".test")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
unprefixed = _msg("test")
|
|
assert manager.normalize_command_content(unprefixed) is False
|
|
|
|
def test_single_char_prefix(self, mock_bot, mock_message):
|
|
"""Test single character prefix (e.g., bping)"""
|
|
mock_bot.config.set('Bot', 'command_prefix', 'b')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
prefixed = _msg("btest")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
unprefixed = _msg("test")
|
|
assert manager.normalize_command_content(unprefixed) is False
|
|
|
|
def test_multi_char_prefix(self, mock_bot, mock_message):
|
|
"""Test multi-character prefix (e.g., abcping)"""
|
|
mock_bot.config.set('Bot', 'command_prefix', 'abc')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
prefixed = _msg("abctest")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
assert manager.normalize_command_content(_msg("test")) is False
|
|
assert manager.normalize_command_content(_msg("abtest")) is False
|
|
|
|
def test_prefix_with_whitespace(self, mock_bot, mock_message):
|
|
"""Test that prefix works with whitespace after it"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
spaced = _msg("! test")
|
|
assert manager.normalize_command_content(spaced) is True
|
|
assert command.matches_keyword(spaced) is True
|
|
|
|
tight = _msg("!test")
|
|
assert manager.normalize_command_content(tight) is True
|
|
assert command.matches_keyword(tight) is True
|
|
|
|
def test_prefix_with_keyword_variations(self, mock_bot, mock_message):
|
|
"""Test prefix with different keyword variations"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
for content in ("!test", "!t", "!test arg1 arg2"):
|
|
msg = _msg(content)
|
|
assert manager.normalize_command_content(msg) is True
|
|
assert command.matches_keyword(msg) is True
|
|
|
|
def test_hello_command_with_prefix(self, mock_bot, mock_message):
|
|
"""Test hello command specifically with prefix"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
mock_bot.config.set('Bot', 'bot_name', 'TestBot')
|
|
mock_bot.config.add_section('Hello_Command')
|
|
mock_bot.config.set('Hello_Command', 'enabled', 'true')
|
|
manager = _make_manager(mock_bot)
|
|
command = HelloCommand(mock_bot)
|
|
|
|
prefixed = _msg("!hello")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
assert manager.normalize_command_content(_msg("hello")) is False
|
|
|
|
def test_ping_command_with_prefix(self, mock_bot, mock_message):
|
|
"""Test ping command with prefix"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '.')
|
|
mock_bot.config.add_section('Ping_Command')
|
|
mock_bot.config.set('Ping_Command', 'enabled', 'true')
|
|
manager = _make_manager(mock_bot)
|
|
command = PingCommand(mock_bot)
|
|
|
|
prefixed = _msg(".ping")
|
|
assert manager.normalize_command_content(prefixed) is True
|
|
assert command.matches_keyword(prefixed) is True
|
|
|
|
assert manager.normalize_command_content(_msg("ping")) is False
|
|
|
|
def test_command_manager_with_prefix(self, mock_bot, mock_message):
|
|
"""Test CommandManager handles prefix correctly"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
mock_bot.config.add_section('Keywords')
|
|
mock_bot.config.set('Keywords', 'keywords', '')
|
|
mock_bot.config.add_section('Custom_Syntax')
|
|
mock_bot.config.set('Custom_Syntax', 'custom_syntax', '')
|
|
|
|
manager = _make_manager(mock_bot)
|
|
|
|
assert manager.check_keywords(_msg("test")) == []
|
|
assert isinstance(manager.check_keywords(_msg("!test")), list)
|
|
|
|
def test_prefix_with_mentions(self, mock_bot, mock_message):
|
|
"""Test that prefix works correctly with @[username] mentions"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
mock_bot.config.set('Bot', 'bot_name', 'TestBot')
|
|
mock_bot.config.set('Bot', 'respond_to_mentions', 'also')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
mock_bot.meshcore = Mock()
|
|
mock_bot.meshcore.self_info = {'name': 'TestBot'}
|
|
|
|
bot_mention = _msg("! test")
|
|
assert manager.normalize_command_content(bot_mention) is True
|
|
assert command.matches_keyword(bot_mention) is True
|
|
|
|
other_mention = _msg("!@[OtherUser] test")
|
|
assert manager.normalize_command_content(other_mention) is True
|
|
assert command.matches_keyword(other_mention) is False
|
|
|
|
def test_different_prefixes_dont_match(self, mock_bot, mock_message):
|
|
"""Test that wrong prefix doesn't match"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
|
|
for content in (".test", "btest", "abctest"):
|
|
assert manager.normalize_command_content(_msg(content)) is False
|
|
|
|
def test_prefix_case_sensitive(self, mock_bot, mock_message):
|
|
"""Test that prefix matching is case-sensitive"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
for content in ("!test", "!TEST"):
|
|
msg = _msg(content)
|
|
assert manager.normalize_command_content(msg) is True
|
|
assert command.matches_keyword(msg) is True
|
|
|
|
mock_bot.config.set('Bot', 'command_prefix', 'b')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
assert manager.normalize_command_content(_msg("Btest")) is False
|
|
|
|
def test_empty_prefix_string(self, mock_bot, mock_message):
|
|
"""Test that empty string prefix means no prefix required"""
|
|
mock_bot.config.set('Bot', 'command_prefix', '')
|
|
manager = _make_manager(mock_bot)
|
|
command = MockTestCommand(mock_bot)
|
|
|
|
plain = _msg("test")
|
|
assert manager.normalize_command_content(plain) is True
|
|
assert command.matches_keyword(plain) is True
|
|
|
|
legacy = _msg("!test")
|
|
assert manager.normalize_command_content(legacy) is True
|
|
assert command.matches_keyword(legacy) is True
|
|
|
|
def test_normalize_is_idempotent(self, mock_bot, mock_message):
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
manager = _make_manager(mock_bot)
|
|
|
|
mock_message.content = "!path ab"
|
|
assert manager.normalize_command_content(mock_message) is True
|
|
assert mock_message.content == "path ab"
|
|
assert manager.normalize_command_content(mock_message) is True
|
|
assert mock_message.content == "path ab"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_command_runs_after_check_keywords_with_prefix(self, mock_bot, mock_message):
|
|
"""Regression: execute()-based commands must run when command_prefix is set."""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
mock_bot.config.set('Bot', 'respond_to_mentions', 'false')
|
|
manager = _make_manager(mock_bot)
|
|
mock_bot.command_manager = manager
|
|
path_cmd = ExecuteOnlyCommand(mock_bot)
|
|
path_cmd.execute = AsyncMock(return_value=True)
|
|
path_cmd.send_response = AsyncMock(return_value=True)
|
|
manager.commands['path'] = path_cmd
|
|
mock_message.content = "!path ab"
|
|
|
|
matches = manager.check_keywords(mock_message)
|
|
assert ('path', None) in matches
|
|
|
|
await manager.execute_commands(mock_message)
|
|
path_cmd.execute.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_response_format_command_still_works_with_prefix(self, mock_bot, mock_message):
|
|
"""Commands with response formats are handled in check_keywords."""
|
|
mock_bot.config.set('Bot', 'command_prefix', '!')
|
|
mock_bot.config.add_section('Ping_Command')
|
|
mock_bot.config.set('Ping_Command', 'enabled', 'true')
|
|
mock_bot.config.add_section('Keywords')
|
|
mock_bot.config.set('Keywords', 'ping', 'Pong!')
|
|
|
|
ping_cmd = PingCommand(mock_bot)
|
|
manager = _make_manager(mock_bot, commands={'ping': ping_cmd})
|
|
mock_bot.command_manager = manager
|
|
mock_message.content = "!ping"
|
|
|
|
matches = manager.check_keywords(mock_message)
|
|
assert any(name == 'ping' and response == 'Pong!' for name, response in matches)
|