Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,12 @@ async def _collect_and_register_commands(self) -> None:
registered_commands = []

for handler_md in star_handlers_registry:
if not star_map[handler_md.handler_module_path].activated:
# `star_map` is keyed by the plugin's main module, so a handler
# registered from a plugin submodule (e.g. a mixin class) has no
# entry here. Skip such handlers instead of raising KeyError and
# aborting the sync of every other plugin's commands.
plugin = star_map.get(handler_md.handler_module_path)
if not plugin or not plugin.activated:
continue
if not handler_md.enabled:
continue
Expand Down
126 changes: 125 additions & 1 deletion tests/test_discord_command_sync.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from unittest.mock import Mock
from unittest.mock import MagicMock, Mock

import pytest

Expand Down Expand Up @@ -55,3 +55,127 @@ async def test_discord_command_sync_ignores_daily_quota(monkeypatch):
adapter.client.sync_commands.assert_awaited_once()
warning.assert_called_once()
assert "30034" in warning.call_args.args[0]


class _FakeSlashCommand:
"""Minimal stand-in for `discord.SlashCommand` that keeps the command name."""

def __init__(self, **kwargs) -> None:
self.name = kwargs["name"]
self.description = kwargs.get("description", "")


def _build_command_adapter(monkeypatch: pytest.MonkeyPatch):
"""Build an adapter whose `discord` reference is a self-contained stub.

Other Discord test modules import the adapter while the real `discord`
package is loaded, so the module-level `discord` reference of the adapter
may point at the installed package instead of the mock. Detaching it keeps
the command registration path independent of the installed package.
"""
from astrbot.core.platform.sources.discord import discord_platform_adapter

adapter = _build_adapter(monkeypatch)
stub = MagicMock()
stub.Option = MagicMock()
stub.SlashCommandOptionType = MagicMock()
stub.SlashCommandOptionType.string = 3
stub.SlashCommand = _FakeSlashCommand
stub.HTTPException = DiscordSyncError
monkeypatch.setattr(discord_platform_adapter, "discord", stub)
return adapter


def _make_command_handler(module_path: str, command_name: str):
"""Build a `StarHandlerMetadata` exposing a single slash command."""
from astrbot.core.star.filter.command import CommandFilter
from astrbot.core.star.star_handler import EventType, StarHandlerMetadata

async def _handler(self, event):
return None

handler_md = StarHandlerMetadata(
event_type=EventType.AdapterMessageEvent,
handler_full_name=f"{module_path}_{command_name}",
handler_name=command_name,
handler_module_path=module_path,
handler=_handler,
event_filters=[],
)
handler_md.event_filters.append(
CommandFilter(command_name, handler_md=handler_md),
)
return handler_md


@pytest.mark.asyncio
async def test_discord_command_sync_skips_handler_from_plugin_submodule(
monkeypatch,
):
"""A handler defined in a plugin submodule must not abort the whole sync.

`star_map` is keyed by the plugin's main module, so a handler registered
from a submodule (e.g. a mixin class in `plugin/foo.py`) has a
`handler_module_path` that is absent from `star_map`. Looking that key up
directly raised `KeyError`, which aborted the collection loop before
`sync_commands()` was reached, so *no* plugin's slash commands were
registered.
"""
from astrbot.core.platform.sources.discord import discord_platform_adapter
from astrbot.core.star.star import StarMetadata

adapter = _build_command_adapter(monkeypatch)

plugin_meta = StarMetadata(name="example_plugin")
plugin_meta.activated = True
monkeypatch.setattr(
discord_platform_adapter,
"star_map",
{"data.plugins.example_plugin.main": plugin_meta},
)
monkeypatch.setattr(
discord_platform_adapter,
"star_handlers_registry",
[
# Registered from a submodule of the plugin: not a `star_map` key.
_make_command_handler("data.plugins.example_plugin.mixin", "sub_cmd"),
# Registered from the plugin's main module.
_make_command_handler("data.plugins.example_plugin.main", "cmd"),
],
)

await adapter._collect_and_register_commands()

adapter.client.sync_commands.assert_awaited_once()
registered = [
call.args[0].name
for call in adapter.client.add_application_command.call_args_list
]
assert registered == ["cmd"]


@pytest.mark.asyncio
async def test_discord_command_sync_skips_deactivated_plugin(monkeypatch):
"""Commands of a deactivated plugin are not registered."""
from astrbot.core.platform.sources.discord import discord_platform_adapter
from astrbot.core.star.star import StarMetadata

adapter = _build_command_adapter(monkeypatch)

plugin_meta = StarMetadata(name="example_plugin")
plugin_meta.activated = False
monkeypatch.setattr(
discord_platform_adapter,
"star_map",
{"data.plugins.example_plugin.main": plugin_meta},
)
monkeypatch.setattr(
discord_platform_adapter,
"star_handlers_registry",
[_make_command_handler("data.plugins.example_plugin.main", "cmd")],
)

await adapter._collect_and_register_commands()

adapter.client.sync_commands.assert_awaited_once()
adapter.client.add_application_command.assert_not_called()
Loading