mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-02 02:52:22 +00:00
Codex on ba293bb2: functools.wraps copies __module__, so a local subclass that wraps PingCommand.get_help_text with a no-message override looked bundled and lost the retry it needs (it raised instead of returning its help), and callables with odd __module__ values could fail or be misclassified. help_text_for now treats a method as bundled only when it is a plain bound method of this command whose function is defined, per the command class's MRO, by a class whose own __module__ is under modules. Everything else (local classes and overrides, partials, Mocks, other callables) gets dev's call-then-retry. New test: a wrapped local override of PingCommand still gets its help.
Accepted residual, besides joke and rain: an instance-level wrapper (functools.partial, Mock(wraps=...)) around a bundled get_help_text whose body raises TypeError now runs the body twice, since the bundled method accepts the message.
628 lines
23 KiB
Python
628 lines
23 KiB
Python
"""Tests for modules.commands.help_command — pure logic and integration paths."""
|
|
|
|
import configparser
|
|
import sqlite3
|
|
from contextlib import contextmanager
|
|
from unittest.mock import MagicMock, Mock
|
|
|
|
import pytest
|
|
|
|
from modules.commands.help_command import HelpCommand
|
|
from tests.conftest import mock_message
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bot factory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_TRACKED_CONNECTIONS = []
|
|
|
|
|
|
def _create_tracked_connection():
|
|
conn = sqlite3.connect(":memory:")
|
|
_TRACKED_CONNECTIONS.append(conn)
|
|
return conn
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _close_tracked_connections():
|
|
"""Ensure every test-created sqlite connection is closed."""
|
|
yield
|
|
while _TRACKED_CONNECTIONS:
|
|
conn = _TRACKED_CONNECTIONS.pop()
|
|
try:
|
|
conn.close()
|
|
except sqlite3.Error:
|
|
pass
|
|
|
|
|
|
def _make_bot(enabled=True, commands=None):
|
|
"""Create a minimal mock bot for HelpCommand tests."""
|
|
bot = MagicMock()
|
|
bot.logger = Mock()
|
|
|
|
config = configparser.ConfigParser()
|
|
config.add_section("Bot")
|
|
config.set("Bot", "bot_name", "TestBot")
|
|
config.add_section("Channels")
|
|
config.set("Channels", "monitor_channels", "general")
|
|
config.set("Channels", "respond_to_dms", "true")
|
|
config.add_section("Keywords")
|
|
if enabled:
|
|
config.add_section("Help_Command")
|
|
config.set("Help_Command", "enabled", "true")
|
|
|
|
bot.config = config
|
|
bot.translator = MagicMock()
|
|
bot.translator.translate = Mock(side_effect=lambda key, **kw: key)
|
|
bot.translator.get_value = Mock(return_value=None)
|
|
bot.command_manager = MagicMock()
|
|
bot.command_manager.monitor_channels = ["general"]
|
|
bot.command_manager.send_response = MagicMock()
|
|
|
|
# Default empty commands dict
|
|
if commands is None:
|
|
bot.command_manager.commands = {}
|
|
else:
|
|
bot.command_manager.commands = commands
|
|
|
|
# Plugin loader with keyword_mappings
|
|
bot.command_manager.plugin_loader = MagicMock()
|
|
bot.command_manager.plugin_loader.keyword_mappings = {}
|
|
|
|
# DB manager with in-memory SQLite
|
|
conn = _create_tracked_connection()
|
|
db = MagicMock()
|
|
|
|
@contextmanager
|
|
def _conn_ctx():
|
|
yield conn
|
|
|
|
db.connection = _conn_ctx
|
|
db.db_path = ":memory:"
|
|
bot.db_manager = db
|
|
|
|
return bot
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _format_commands_list_to_length (pure logic)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFormatCommandsListToLength:
|
|
def setup_method(self):
|
|
self.cmd = HelpCommand(_make_bot())
|
|
|
|
def test_no_max_length_returns_all(self):
|
|
result = self.cmd._format_commands_list_to_length(["a", "b", "c"])
|
|
assert result == "a, b, c"
|
|
|
|
def test_max_length_zero_returns_all(self):
|
|
result = self.cmd._format_commands_list_to_length(["a", "b", "c"], max_length=0)
|
|
assert result == "a, b, c"
|
|
|
|
def test_empty_list_returns_empty(self):
|
|
result = self.cmd._format_commands_list_to_length([])
|
|
assert result == ""
|
|
|
|
def test_truncates_at_max_length(self):
|
|
# "a, b, c" = 7 chars; limit to 4 → only "a" + " (2 more)"
|
|
result = self.cmd._format_commands_list_to_length(["a", "b", "c"], max_length=10)
|
|
# Just verify it doesn't exceed max_length
|
|
assert len(result) <= 10
|
|
|
|
def test_all_fit_within_max_length(self):
|
|
result = self.cmd._format_commands_list_to_length(["ping", "wx"], max_length=100)
|
|
assert result == "ping, wx"
|
|
|
|
def test_suffix_appended_when_truncated(self):
|
|
names = ["alpha", "beta", "gamma", "delta", "epsilon"]
|
|
result = self.cmd._format_commands_list_to_length(names, max_length=20)
|
|
# Should contain "(N more)" suffix or be truncated
|
|
assert len(result) <= 20 or "more" in result
|
|
|
|
def test_single_item(self):
|
|
result = self.cmd._format_commands_list_to_length(["ping"])
|
|
assert result == "ping"
|
|
|
|
def test_single_item_exceeds_max_length(self):
|
|
result = self.cmd._format_commands_list_to_length(["verylongcommandname"], max_length=5)
|
|
# Can't fit, returns empty or truncated
|
|
assert isinstance(result, str)
|
|
|
|
def test_negative_max_length_returns_all(self):
|
|
result = self.cmd._format_commands_list_to_length(["a", "b"], max_length=-1)
|
|
assert result == "a, b"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_command_valid_for_channel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsCommandValidForChannel:
|
|
def setup_method(self):
|
|
self.bot = _make_bot()
|
|
self.cmd = HelpCommand(self.bot)
|
|
|
|
def test_no_message_always_true(self):
|
|
mock_cmd = MagicMock()
|
|
assert self.cmd._is_command_valid_for_channel("ping", mock_cmd, None) is True
|
|
|
|
def test_channel_allowed_returns_true(self):
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.is_channel_allowed = Mock(return_value=True)
|
|
msg = mock_message(content="help", channel="general")
|
|
assert self.cmd._is_command_valid_for_channel("ping", mock_cmd, msg) is True
|
|
|
|
def test_channel_not_allowed_returns_false(self):
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.is_channel_allowed = Mock(return_value=False)
|
|
msg = mock_message(content="help", channel="general")
|
|
assert self.cmd._is_command_valid_for_channel("ping", mock_cmd, msg) is False
|
|
|
|
def test_no_is_channel_allowed_attribute(self):
|
|
mock_cmd = MagicMock(spec=[]) # No attributes
|
|
msg = mock_message(content="help", channel="general")
|
|
# Should not crash, should check _is_channel_trigger_allowed
|
|
result = self.cmd._is_command_valid_for_channel("ping", mock_cmd, msg)
|
|
assert isinstance(result, bool)
|
|
|
|
def test_channel_trigger_not_allowed(self):
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.is_channel_allowed = Mock(return_value=True)
|
|
self.bot.command_manager._is_channel_trigger_allowed = Mock(return_value=False)
|
|
msg = mock_message(content="help", channel="restricted")
|
|
assert self.cmd._is_command_valid_for_channel("restricted_cmd", mock_cmd, msg) is False
|
|
|
|
def test_channel_trigger_allowed(self):
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.is_channel_allowed = Mock(return_value=True)
|
|
self.bot.command_manager._is_channel_trigger_allowed = Mock(return_value=True)
|
|
msg = mock_message(content="help", channel="general")
|
|
assert self.cmd._is_command_valid_for_channel("ping", mock_cmd, msg) is True
|
|
|
|
def test_no_channel_trigger_check_attribute(self):
|
|
"""When command_manager lacks _is_channel_trigger_allowed, still works."""
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.is_channel_allowed = Mock(return_value=True)
|
|
del self.bot.command_manager._is_channel_trigger_allowed
|
|
msg = mock_message(content="help", channel="general")
|
|
result = self.cmd._is_command_valid_for_channel("ping", mock_cmd, msg)
|
|
assert result is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_specific_help
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetSpecificHelp:
|
|
def test_known_command_with_help_text(self):
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.get_help_text = Mock(return_value="Ping the bot")
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("ping")
|
|
assert "commands.help.specific" in result or result != ""
|
|
|
|
def test_known_command_help_text_no_message_param(self):
|
|
"""A local command whose get_help_text takes no message still gets help."""
|
|
bot = _make_bot()
|
|
|
|
class LocalCommand:
|
|
def get_help_text(self):
|
|
return "Simple help"
|
|
|
|
bot.command_manager.commands = {"foo": LocalCommand()}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("foo")
|
|
assert isinstance(result, str)
|
|
|
|
def test_error_inside_bundled_help_body_is_not_retried(self):
|
|
"""A TypeError raised by a bundled command's help body propagates after one call."""
|
|
from modules.commands.ping_command import PingCommand
|
|
|
|
bot = _make_bot()
|
|
ping = object.__new__(PingCommand)
|
|
calls = []
|
|
|
|
def broken_translate(key, **kwargs):
|
|
calls.append(key)
|
|
raise TypeError("bug in help body")
|
|
|
|
ping.translate = broken_translate
|
|
bot.command_manager.commands = {"ping": ping}
|
|
cmd = HelpCommand(bot)
|
|
with pytest.raises(TypeError):
|
|
cmd.get_specific_help("ping", None)
|
|
assert len(calls) == 1
|
|
|
|
def test_wrapped_local_override_of_a_bundled_command_keeps_the_retry(self):
|
|
"""functools.wraps copies the bundled __module__; the override is still local."""
|
|
import functools
|
|
|
|
from modules.commands.ping_command import PingCommand
|
|
|
|
class LocalPing(PingCommand):
|
|
@functools.wraps(PingCommand.get_help_text)
|
|
def get_help_text(self):
|
|
return "local help"
|
|
|
|
bot = _make_bot()
|
|
bot.command_manager.commands = {"ping": object.__new__(LocalPing)}
|
|
cmd = HelpCommand(bot)
|
|
cmd.get_specific_help("ping", Mock()) # must not raise
|
|
|
|
def test_local_plugin_help_keeps_the_typeerror_retry(self):
|
|
"""A local get_help_text(message=None) that raises TypeError is retried without it."""
|
|
bot = _make_bot()
|
|
calls = []
|
|
|
|
class LocalCommand:
|
|
def get_help_text(self, message=None):
|
|
calls.append(message)
|
|
if message is not None:
|
|
raise TypeError("message-specific help failed")
|
|
return "Generic help"
|
|
|
|
bot.command_manager.commands = {"foo": LocalCommand()}
|
|
cmd = HelpCommand(bot)
|
|
message = Mock()
|
|
cmd.get_specific_help("foo", message)
|
|
assert calls == [message, None]
|
|
|
|
def test_unknown_command_returns_unknown_key(self):
|
|
bot = _make_bot()
|
|
bot.command_manager.commands = {}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("unknowncmd")
|
|
assert "commands.help.unknown" in result
|
|
|
|
def test_alias_mapping_applied(self):
|
|
"""Alias 'ping' maps to itself."""
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.get_help_text = Mock(return_value="Pong!")
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("ping")
|
|
assert isinstance(result, str)
|
|
|
|
def test_alias_from_keyword_mappings_resolves(self):
|
|
bot = _make_bot()
|
|
mock_schedule = MagicMock()
|
|
mock_schedule.get_help_text = Mock(return_value="Schedule help")
|
|
mock_schedule.keywords = ["schedule"]
|
|
bot.command_manager.commands = {"schedule": mock_schedule}
|
|
bot.command_manager.plugin_loader.keyword_mappings = {"sched": "schedule"}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("sched")
|
|
assert isinstance(result, str)
|
|
|
|
def test_alias_from_runtime_keywords_resolves_without_mapping(self):
|
|
bot = _make_bot()
|
|
mock_schedule = MagicMock()
|
|
mock_schedule.get_help_text = Mock(return_value="Schedule help")
|
|
mock_schedule.keywords = ["schedule", "sched"]
|
|
bot.command_manager.commands = {"schedule": mock_schedule}
|
|
bot.command_manager.plugin_loader.keyword_mappings = {}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("sched")
|
|
assert isinstance(result, str)
|
|
|
|
def test_multiword_alias_is_checked_before_subcommand_fallback(self):
|
|
bot = _make_bot()
|
|
dadjoke_cmd = MagicMock()
|
|
dadjoke_cmd.get_help_text = Mock(return_value="Dad joke help")
|
|
dadjoke_cmd.keywords = ["dadjoke", "dad joke"]
|
|
unrelated_cmd = MagicMock()
|
|
unrelated_cmd.get_help_text = Mock(return_value="Wrong help")
|
|
unrelated_cmd.keywords = ["dad"]
|
|
bot.command_manager.commands = {
|
|
"dadjoke": dadjoke_cmd,
|
|
"dad": unrelated_cmd,
|
|
}
|
|
cmd = HelpCommand(bot)
|
|
|
|
cmd.get_specific_help("dad joke")
|
|
|
|
dadjoke_cmd.get_help_text.assert_called_once()
|
|
unrelated_cmd.get_help_text.assert_not_called()
|
|
|
|
def test_no_get_help_text_attribute(self):
|
|
"""Command without get_help_text returns no_help key."""
|
|
bot = _make_bot()
|
|
mock_cmd = MagicMock(spec=[])
|
|
bot.command_manager.commands = {"bare": mock_cmd}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_specific_help("bare")
|
|
assert isinstance(result, str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# can_execute
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCanExecute:
|
|
def test_enabled_true(self):
|
|
bot = _make_bot(enabled=True)
|
|
cmd = HelpCommand(bot)
|
|
msg = mock_message(content="help", channel="general")
|
|
assert cmd.can_execute(msg) is True
|
|
|
|
def test_enabled_false(self):
|
|
bot = _make_bot()
|
|
# Set the flag directly — the config section already exists from _make_bot
|
|
bot.config.set("Help_Command", "enabled", "false")
|
|
cmd = HelpCommand(bot)
|
|
cmd.help_enabled = False
|
|
msg = mock_message(content="help", channel="general")
|
|
assert cmd.can_execute(msg) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_help_text
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetHelpText:
|
|
def test_returns_string(self):
|
|
cmd = HelpCommand(_make_bot())
|
|
result = cmd.get_help_text()
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestGetGeneralHelp:
|
|
"""Tests for get_general_help() (lines 134-138)."""
|
|
|
|
def test_returns_string(self):
|
|
bot = _make_bot()
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_general_help()
|
|
assert isinstance(result, str)
|
|
|
|
def test_includes_commands_help_key(self):
|
|
bot = _make_bot()
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_general_help()
|
|
# Our mock translator returns keys — so should contain 'commands.help.general'
|
|
assert "commands.help" in result
|
|
|
|
|
|
class TestGetAvailableCommandsListFiltered:
|
|
"""Tests for channel-filtered command listing (line 185)."""
|
|
|
|
def test_channel_filter_excludes_invalid_commands(self):
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
mock_ping.is_channel_allowed = Mock(return_value=False) # Excluded
|
|
mock_wx = MagicMock()
|
|
mock_wx.name = "wx"
|
|
mock_wx.is_channel_allowed = Mock(return_value=True) # Included
|
|
bot.command_manager.commands = {"ping": mock_ping, "wx": mock_wx}
|
|
bot.command_manager._is_channel_trigger_allowed = Mock(return_value=True)
|
|
cmd = HelpCommand(bot)
|
|
msg = mock_message(content="help", channel="general")
|
|
result = cmd.get_available_commands_list(message=msg)
|
|
# ping is excluded; wx should be present
|
|
assert "wx" in result or "commands.help" in result # or just no crash
|
|
|
|
def test_command_in_stats_not_in_keyword_mappings(self):
|
|
"""Commands returned from DB stats but not in keyword_mappings (lines 218-235)."""
|
|
from contextlib import contextmanager
|
|
|
|
bot = _make_bot()
|
|
conn = _create_tracked_connection()
|
|
conn.execute("""
|
|
CREATE TABLE command_stats (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
sender_id TEXT,
|
|
command_name TEXT,
|
|
channel TEXT,
|
|
is_dm BOOLEAN,
|
|
response_sent BOOLEAN
|
|
)
|
|
""")
|
|
# Add a command that's NOT in keyword_mappings but IS a primary command name
|
|
conn.execute("INSERT INTO command_stats VALUES (1, 1, 'u', 'unknown_cmd', 'g', 0, 1)")
|
|
conn.commit()
|
|
|
|
db = MagicMock()
|
|
|
|
@contextmanager
|
|
def _conn_ctx():
|
|
yield conn
|
|
|
|
db.connection = _conn_ctx
|
|
bot.db_manager = db
|
|
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.name = "known"
|
|
bot.command_manager.commands = {"known": mock_cmd}
|
|
bot.command_manager.plugin_loader.keyword_mappings = {}
|
|
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestFormatCommandsListSuffix:
|
|
"""Tests for _format_commands_list_to_length with suffix that fits (line 294)."""
|
|
|
|
def test_suffix_fits_within_max(self):
|
|
cmd = HelpCommand(_make_bot())
|
|
# "ping" = 4 chars, " (1 more)" = 9 chars, "wx" doesn't fit; total "ping (1 more)" = 13
|
|
result = cmd._format_commands_list_to_length(["ping", "wx"], max_length=13)
|
|
assert "(1 more)" in result or "ping" in result
|
|
|
|
def test_suffix_appended_when_some_fit(self):
|
|
cmd = HelpCommand(_make_bot())
|
|
names = ["ab", "cd", "ef", "gh"]
|
|
result = cmd._format_commands_list_to_length(names, max_length=8)
|
|
assert isinstance(result, str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# execute
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestExecute:
|
|
def test_execute_returns_true(self):
|
|
import asyncio
|
|
bot = _make_bot()
|
|
cmd = HelpCommand(bot)
|
|
msg = mock_message(content="help", channel="general")
|
|
result = asyncio.run(cmd.execute(msg))
|
|
assert result is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_available_commands_list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetAvailableCommandsList:
|
|
def test_empty_commands_returns_empty(self):
|
|
bot = _make_bot()
|
|
bot.command_manager.commands = {}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
assert isinstance(result, str)
|
|
|
|
def test_with_commands_returns_names(self):
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
assert "ping" in result
|
|
|
|
def test_with_max_length(self):
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list(max_length=3)
|
|
assert len(result) <= 3 or "ping" in result
|
|
|
|
def test_with_message_filter(self):
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
mock_ping.is_channel_allowed = Mock(return_value=True)
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
cmd = HelpCommand(bot)
|
|
msg = mock_message(content="help", channel="general")
|
|
result = cmd.get_available_commands_list(message=msg)
|
|
assert isinstance(result, str)
|
|
|
|
def test_with_stats_table_present(self):
|
|
"""When command_stats table exists, commands are sorted by usage count."""
|
|
from contextlib import contextmanager
|
|
|
|
bot = _make_bot()
|
|
conn = _create_tracked_connection()
|
|
conn.execute("""
|
|
CREATE TABLE command_stats (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
sender_id TEXT,
|
|
command_name TEXT,
|
|
channel TEXT,
|
|
is_dm BOOLEAN,
|
|
response_sent BOOLEAN
|
|
)
|
|
""")
|
|
conn.execute("INSERT INTO command_stats (timestamp, sender_id, command_name, channel, is_dm, response_sent) VALUES (1, 'u1', 'ping', 'general', 0, 1)")
|
|
conn.execute("INSERT INTO command_stats (timestamp, sender_id, command_name, channel, is_dm, response_sent) VALUES (2, 'u1', 'ping', 'general', 0, 1)")
|
|
conn.commit()
|
|
|
|
db = MagicMock()
|
|
|
|
@contextmanager
|
|
def _conn_ctx():
|
|
yield conn
|
|
|
|
db.connection = _conn_ctx
|
|
bot.db_manager = db
|
|
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
bot.command_manager.plugin_loader.keyword_mappings = {"ping": "ping"}
|
|
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
assert "ping" in result
|
|
|
|
def test_unused_commands_listed_when_stats_present(self):
|
|
"""Regression for issue #137: commands with no usage stats must still appear.
|
|
|
|
Previously, once command_stats had any rows, only commands present in the
|
|
table were listed, so never-used commands were missing from help.
|
|
"""
|
|
from contextlib import contextmanager
|
|
|
|
bot = _make_bot()
|
|
conn = _create_tracked_connection()
|
|
conn.execute("""
|
|
CREATE TABLE command_stats (
|
|
id INTEGER PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
sender_id TEXT,
|
|
command_name TEXT,
|
|
channel TEXT,
|
|
is_dm BOOLEAN,
|
|
response_sent BOOLEAN
|
|
)
|
|
""")
|
|
# Only 'ping' has been used; 'version' has never been used.
|
|
conn.execute("INSERT INTO command_stats (timestamp, sender_id, command_name, channel, is_dm, response_sent) VALUES (1, 'u1', 'ping', 'general', 0, 1)")
|
|
conn.commit()
|
|
|
|
db = MagicMock()
|
|
|
|
@contextmanager
|
|
def _conn_ctx():
|
|
yield conn
|
|
|
|
db.connection = _conn_ctx
|
|
bot.db_manager = db
|
|
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
mock_version = MagicMock()
|
|
mock_version.name = "version"
|
|
bot.command_manager.commands = {"ping": mock_ping, "version": mock_version}
|
|
bot.command_manager.plugin_loader.keyword_mappings = {
|
|
"ping": "ping",
|
|
"version": "version",
|
|
}
|
|
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
# The used command appears first, but the unused one must also be listed.
|
|
assert "ping" in result
|
|
assert "version" in result
|
|
|
|
def test_db_exception_falls_back_gracefully(self):
|
|
"""If DB raises, falls back to sorted command names."""
|
|
bot = _make_bot()
|
|
mock_ping = MagicMock()
|
|
mock_ping.name = "ping"
|
|
bot.command_manager.commands = {"ping": mock_ping}
|
|
bad_db = MagicMock()
|
|
|
|
@contextmanager
|
|
def _bad_conn():
|
|
raise Exception("DB down")
|
|
yield
|
|
|
|
bad_db.connection = _bad_conn
|
|
bot.db_manager = bad_db
|
|
|
|
cmd = HelpCommand(bot)
|
|
result = cmd.get_available_commands_list()
|
|
assert isinstance(result, str)
|