fix(help): preserve multiword aliases in fallback

This commit is contained in:
agessaman committed 2026-09-15 21:01:39 -07:00
1 parent 3b1edea8af
commit d3a435d9ed
4 files changed
+91 -70

No files matched your search

+25 -45
View File
@@ -1470,6 +1470,28 @@ class CommandManager:
return False
return True
def _find_help_command(self, lookup_name: str) -> Any | None:
"""Resolve an exact command name or alias."""
normalized_name = lookup_name.lower()
command = self.commands.get(normalized_name) or self.commands.get(lookup_name)
if command:
return command
if hasattr(self, 'plugin_loader') and hasattr(self.plugin_loader, 'keyword_mappings'):
mapped_name = self.plugin_loader.keyword_mappings.get(normalized_name)
if mapped_name:
command = self.commands.get(mapped_name)
if command:
return command
for cmd_instance in self.commands.values():
if (
hasattr(cmd_instance, 'keywords')
and normalized_name in [keyword.lower() for keyword in cmd_instance.keywords]
):
return cmd_instance
return None
def get_help_for_command(self, command_name: str, message: MeshMessage | None = None) -> str:
"""Get help text for a specific command (LoRa-friendly compact format).
@@ -1486,60 +1508,18 @@ class CommandManager:
return self.get_general_help(message)
requested_name = command_name.strip()
# Help may include subcommand arguments (for example, ``help net
# create``). Resolve the plugin from the first token, then pass the
# unchanged MeshMessage to get_help_text() so the plugin can interpret
# the remaining text in its own command-specific way.
lookup_name = requested_name.split(maxsplit=1)[0] if requested_name else ""
normalized_name = lookup_name.lower()
# First, try to find a command by exact name
command = self.commands.get(normalized_name) or self.commands.get(lookup_name)
command = self._find_help_command(requested_name)
if not command and requested_name:
command = self._find_help_command(requested_name.split(maxsplit=1)[0])
if command:
# Try to pass message context to get_help_text if supported
try:
help_text = command.get_help_text(message)
except TypeError:
# Fallback for commands that don't accept message parameter
help_text = command.get_help_text()
# Use translator if available
if hasattr(self.bot, 'translator'):
return self.bot.translator.translate('commands.help.specific', command=command_name, help_text=help_text)
return f"Help {command_name}: {help_text}"
# Next, consult plugin_loader keyword mappings (if available)
mapped_name: str | None = None
if hasattr(self, 'plugin_loader') and hasattr(self.plugin_loader, 'keyword_mappings'):
mapped_name = self.plugin_loader.keyword_mappings.get(normalized_name)
if mapped_name:
command = self.commands.get(mapped_name)
if command:
try:
help_text = command.get_help_text(message)
except TypeError:
help_text = command.get_help_text()
if hasattr(self.bot, 'translator'):
return self.bot.translator.translate('commands.help.specific', command=command_name, help_text=help_text)
return f"Help {command_name}: {help_text}"
# If still not found, search through all commands and their keywords
for _cmd_name, cmd_instance in self.commands.items():
# Check if the requested command name matches any of this command's keywords
if (
hasattr(cmd_instance, 'keywords')
and normalized_name in [k.lower() for k in cmd_instance.keywords]
):
# Try to pass message context to get_help_text if supported
try:
help_text = cmd_instance.get_help_text(message)
except TypeError:
# Fallback for commands that don't accept message parameter
help_text = cmd_instance.get_help_text()
# Use translator if available
if hasattr(self.bot, 'translator'):
return self.bot.translator.translate('commands.help.specific', command=command_name, help_text=help_text)
return f"Help {command_name}: {help_text}"
# If still not found, return unknown command message with helpful suggestion
# Use the help command's method to get popular commands (only primary names, no aliases)
available_str = ""
+29 -25
View File
@@ -94,31 +94,36 @@ class HelpCommand(BaseCommand):
str: The formatted help text for the specific command.
"""
requested_name = command_name.strip()
lookup_name = requested_name.split(maxsplit=1)[0] if requested_name else ""
normalized_name = lookup_name.lower()
command = None
candidates = [requested_name]
if requested_name:
base_name = requested_name.split(maxsplit=1)[0]
if base_name != requested_name:
candidates.append(base_name)
# Get the command instance by direct name first
command = (
self.bot.command_manager.commands.get(normalized_name)
or self.bot.command_manager.commands.get(lookup_name)
)
# Then through plugin keyword mappings (if available)
if not command and hasattr(self.bot.command_manager, 'plugin_loader'):
mappings = getattr(self.bot.command_manager.plugin_loader, 'keyword_mappings', {})
mapped_name = mappings.get(normalized_name)
if mapped_name:
command = self.bot.command_manager.commands.get(mapped_name)
# Final fallback: resolve through runtime command keywords
if not command:
for cmd_instance in self.bot.command_manager.commands.values():
if (
hasattr(cmd_instance, 'keywords')
and normalized_name in [k.lower() for k in cmd_instance.keywords]
):
command = cmd_instance
break
mappings = getattr(self.bot.command_manager.plugin_loader, 'keyword_mappings', {})
for lookup_name in candidates:
normalized_name = lookup_name.lower()
command = (
self.bot.command_manager.commands.get(normalized_name)
or self.bot.command_manager.commands.get(lookup_name)
)
if not command:
mapped_name = mappings.get(normalized_name)
if mapped_name:
command = self.bot.command_manager.commands.get(mapped_name)
if not command:
command = next(
(
cmd_instance
for cmd_instance in self.bot.command_manager.commands.values()
if hasattr(cmd_instance, 'keywords')
and normalized_name in [keyword.lower() for keyword in cmd_instance.keywords]
),
None,
)
if command:
break
if command:
# Pass message context to get_help_text if the method supports it
@@ -314,4 +319,3 @@ class HelpCommand(BaseCommand):
return ', '.join(result) + suffix
break
return ', '.join(result)
+18
View File
@@ -362,6 +362,24 @@ class TestGetHelpForCommand:
mock_cmd.get_help_text.assert_called_once_with(message)
assert message.content == "help net create"
def test_multiword_alias_is_checked_before_subcommand_fallback(self, cm_bot):
dadjoke_cmd = MagicMock()
dadjoke_cmd.keywords = ["dadjoke", "dad joke"]
dadjoke_cmd.get_help_text = Mock(return_value="Dad joke help")
unrelated_cmd = MagicMock()
unrelated_cmd.keywords = ["dad"]
unrelated_cmd.get_help_text = Mock(return_value="Wrong help")
manager = make_manager(
cm_bot,
commands={"dadjoke": dadjoke_cmd, "dad": unrelated_cmd},
)
result = manager.get_help_for_command("dad joke")
assert "Dad joke help" in result
dadjoke_cmd.get_help_text.assert_called_once()
unrelated_cmd.get_help_text.assert_not_called()
class TestInternetStatusCache:
"""Tests for InternetStatusCache."""
+19
View File
@@ -253,6 +253,25 @@ class TestGetSpecificHelp:
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()