"""Tests for StarHandlerRegistry and StarHandlerMetadata.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from astrbot.core.star.star_handler import ( EventType, StarHandlerMetadata, StarHandlerRegistry, ) @pytest.fixture def registry(): """Create a fresh StarHandlerRegistry.""" return StarHandlerRegistry() @pytest.fixture def mock_handler(): """Create a mock handler for testing.""" def make_handler( event_type: EventType, full_name: str, module_path: str = "test_module", enabled: bool = True, priority: int = 0, extras_configs: dict | None = None, ) -> StarHandlerMetadata: handler = MagicMock(spec=StarHandlerMetadata) handler.event_type = event_type handler.handler_full_name = full_name handler.handler_name = full_name.split("_")[-1] handler.handler_module_path = module_path handler.enabled = enabled configs = extras_configs or {} if priority != 0: configs["priority"] = priority handler.extras_configs = configs return handler return make_handler class TestStarHandlerRegistryAppend: """Tests for StarHandlerRegistry.append().""" def test_append_adds_to_map(self, registry, mock_handler): """Append adds handler to star_handlers_map.""" handler = mock_handler(EventType.AdapterMessageEvent, "test_handler") registry.append(handler) assert registry.star_handlers_map["test_handler"] is handler def test_append_adds_to_list(self, registry, mock_handler): """Append adds handler to _handlers list.""" handler = mock_handler(EventType.AdapterMessageEvent, "test_handler") registry.append(handler) assert handler in registry._handlers def test_append_sets_default_priority(self, registry, mock_handler): """Append sets default priority=0 if not specified.""" handler = mock_handler(EventType.AdapterMessageEvent, "test_handler") registry.append(handler) assert handler.extras_configs["priority"] == 0 def test_append_preserves_existing_priority(self, registry, mock_handler): """Append preserves explicitly set priority.""" handler = mock_handler( EventType.AdapterMessageEvent, "test_handler", priority=5, ) registry.append(handler) assert handler.extras_configs["priority"] == 5 def test_append_sorts_by_priority_descending(self, registry, mock_handler): """Append keeps handlers sorted by priority (highest first).""" h1 = mock_handler(EventType.AdapterMessageEvent, "low_priority", priority=1) h5 = mock_handler(EventType.AdapterMessageEvent, "high_priority", priority=5) h3 = mock_handler(EventType.AdapterMessageEvent, "mid_priority", priority=3) registry.append(h1) registry.append(h5) registry.append(h3) # Should be sorted: high(5), mid(3), low(1) priorities = [h.extras_configs["priority"] for h in registry._handlers] assert priorities == [5, 3, 1] class TestStarHandlerRegistryGetByEventType: """Tests for StarHandlerRegistry.get_handlers_by_event_type().""" def test_returns_handlers_matching_event_type(self, registry, mock_handler): """Returns only handlers matching the specified event type.""" adapter_handler = mock_handler(EventType.AdapterMessageEvent, "adapter_h") llm_handler = mock_handler(EventType.OnLLMRequestEvent, "llm_h") registry.append(adapter_handler) registry.append(llm_handler) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock(activated=True, reserved=False) result = registry.get_handlers_by_event_type(EventType.AdapterMessageEvent) assert adapter_handler in result assert llm_handler not in result def test_excludes_disabled_handlers(self, registry, mock_handler): """Disabled handlers are excluded.""" enabled_h = mock_handler(EventType.AdapterMessageEvent, "enabled", enabled=True) disabled_h = mock_handler( EventType.AdapterMessageEvent, "disabled", enabled=False ) registry.append(enabled_h) registry.append(disabled_h) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock(activated=True, reserved=False) result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, only_activated=True ) assert enabled_h in result assert disabled_h not in result def test_only_activated_false_bypasses_star_map_check(self, registry, mock_handler): """only_activated=False bypasses star_map activation check but still checks handler.enabled.""" enabled_h = mock_handler(EventType.AdapterMessageEvent, "enabled", enabled=True) disabled_h = mock_handler( EventType.AdapterMessageEvent, "disabled", enabled=False ) registry.append(enabled_h) registry.append(disabled_h) result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, only_activated=False ) assert enabled_h in result # handler.enabled is still checked even with only_activated=False assert disabled_h not in result def test_plugin_not_activated_excluded(self, registry, mock_handler): """Handlers from deactivated plugins are excluded.""" handler = mock_handler( EventType.AdapterMessageEvent, "plugin_h", module_path="mod" ) registry.append(handler) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock(activated=False) result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, only_activated=True ) assert handler not in result def test_plugin_not_in_star_map_excluded(self, registry, mock_handler): """Handlers whose plugin is not in star_map are excluded.""" handler = mock_handler( EventType.AdapterMessageEvent, "orphan_h", module_path="orphan" ) registry.append(handler) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = None result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, only_activated=True ) assert handler not in result def test_plugins_name_whitelist(self, registry, mock_handler): """plugins_name filters to specific plugin names.""" handler1 = mock_handler( EventType.AdapterMessageEvent, "h1", module_path="plugin_a" ) handler2 = mock_handler( EventType.AdapterMessageEvent, "h2", module_path="plugin_b" ) registry.append(handler1) registry.append(handler2) def mock_get(path): m = MagicMock(activated=True, reserved=False) m.name = path # set name as actual string attribute return m with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.side_effect = mock_get result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, plugins_name=["plugin_a"], ) assert handler1 in result assert handler2 not in result def test_plugins_name_wildcard_all(self, registry, mock_handler): """plugins_name=['*'] includes all handlers (bypasses whitelist but not activation check).""" h1 = mock_handler(EventType.AdapterMessageEvent, "h1", module_path="p1") h2 = mock_handler(EventType.AdapterMessageEvent, "h2", module_path="p2") registry.append(h1) registry.append(h2) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock(activated=True, reserved=False) result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, plugins_name=["*"] ) assert len(result) == 2 def test_event_type_allowed_even_when_not_in_plugin_list( self, registry, mock_handler ): """Certain event types bypass the plugins_name filter.""" handler = mock_handler( EventType.OnAstrBotLoadedEvent, "loaded_h", module_path="mod" ) registry.append(handler) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock( name="mod", activated=True, reserved=False ) # Should include even though plugin not in plugins_name list result = registry.get_handlers_by_event_type( EventType.OnAstrBotLoadedEvent, plugins_name=["other"] ) assert handler in result def test_reserved_plugin_bypasses_whitelist(self, registry, mock_handler): """Reserved plugins bypass the plugins_name whitelist.""" handler = mock_handler( EventType.AdapterMessageEvent, "reserved_h", module_path="core_mod" ) registry.append(handler) with patch("astrbot.core.star.star_handler.star_map") as mock_map: mock_map.get.return_value = MagicMock( name="core", activated=True, reserved=True ) result = registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, plugins_name=["other"] ) assert handler in result class TestStarHandlerRegistryGetByFullName: """Tests for get_handler_by_full_name().""" def test_returns_handler_by_name(self, registry, mock_handler): """Returns the handler with the given full name.""" h1 = mock_handler(EventType.AdapterMessageEvent, "handler_one") h2 = mock_handler(EventType.AdapterMessageEvent, "handler_two") registry.append(h1) registry.append(h2) result = registry.get_handler_by_full_name("handler_one") assert result is h1 def test_returns_none_for_missing_name(self, registry): """Returns None for a name not in the registry.""" result = registry.get_handler_by_full_name("nonexistent") assert result is None class TestStarHandlerRegistryGetByModuleName: """Tests for get_handlers_by_module_name().""" def test_returns_handlers_for_module(self, registry, mock_handler): """Returns all handlers from a specific module.""" h1 = mock_handler(EventType.AdapterMessageEvent, "m1_h1", module_path="mod_a") h2 = mock_handler(EventType.OnLLMRequestEvent, "m1_h2", module_path="mod_a") h3 = mock_handler(EventType.AdapterMessageEvent, "m2_h1", module_path="mod_b") registry.append(h1) registry.append(h2) registry.append(h3) result = registry.get_handlers_by_module_name("mod_a") assert h1 in result assert h2 in result assert h3 not in result def test_returns_empty_for_unknown_module(self, registry): """Returns empty list for a module with no handlers.""" result = registry.get_handlers_by_module_name("unknown_module") assert result == [] class TestStarHandlerRegistryClear: """Tests for StarHandlerRegistry.clear().""" def test_clear_removes_all_handlers(self, registry, mock_handler): """clear() empties both maps and lists.""" registry.append(mock_handler(EventType.AdapterMessageEvent, "h1")) registry.append(mock_handler(EventType.OnLLMRequestEvent, "h2")) registry.clear() assert len(registry.star_handlers_map) == 0 assert len(registry._handlers) == 0 class TestStarHandlerRegistryRemove: """Tests for StarHandlerRegistry.remove().""" def test_remove_existing_handler(self, registry, mock_handler): """remove() removes the specified handler.""" h1 = mock_handler(EventType.AdapterMessageEvent, "h1") h2 = mock_handler(EventType.AdapterMessageEvent, "h2") registry.append(h1) registry.append(h2) registry.remove(h1) assert "h1" not in registry.star_handlers_map assert h1 not in registry._handlers assert "h2" in registry.star_handlers_map def test_remove_nonexistent_no_error(self, registry, mock_handler): """remove() of non-existent handler does not raise.""" handler = mock_handler(EventType.AdapterMessageEvent, "h1") registry.remove(handler) # Should not raise class TestStarHandlerRegistryIteration: """Tests for __iter__ and __len__.""" def test_iter_yields_handlers(self, registry, mock_handler): """__iter__ yields all handlers in priority order.""" h1 = mock_handler(EventType.AdapterMessageEvent, "h1") h2 = mock_handler(EventType.AdapterMessageEvent, "h2") registry.append(h1) registry.append(h2) result = list(registry) assert h1 in result assert h2 in result def test_len_returns_count(self, registry, mock_handler): """__len__ returns number of handlers.""" assert len(registry) == 0 registry.append(mock_handler(EventType.AdapterMessageEvent, "h1")) assert len(registry) == 1 registry.append(mock_handler(EventType.AdapterMessageEvent, "h2")) assert len(registry) == 2 class TestStarHandlerMetadataPriority: """Tests for StarHandlerMetadata.__lt__().""" def test_lt_lower_priority(self): """Handler with lower priority is 'less than' higher priority.""" h_low = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="low", handler_name="low", handler_module_path="m", handler=MagicMock(), event_filters=[], extras_configs={"priority": 1}, ) h_high = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="high", handler_name="high", handler_module_path="m", handler=MagicMock(), event_filters=[], extras_configs={"priority": 5}, ) assert (h_low < h_high) is True assert (h_high < h_low) is False def test_lt_default_priority(self): """Handler with default priority (0) is less than non-zero.""" h_default = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="default", handler_name="default", handler_module_path="m", handler=MagicMock(), event_filters=[], ) h_nonzero = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="nonzero", handler_name="nonzero", handler_module_path="m", handler=MagicMock(), event_filters=[], extras_configs={"priority": 10}, ) assert (h_default < h_nonzero) is True def test_lt_same_priority(self): """Handlers with same priority return False for both comparisons.""" h1 = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="h1", handler_name="h1", handler_module_path="m", handler=MagicMock(), event_filters=[], extras_configs={"priority": 5}, ) h2 = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, handler_full_name="h2", handler_name="h2", handler_module_path="m", handler=MagicMock(), event_filters=[], extras_configs={"priority": 5}, ) assert (h1 < h2) is False assert (h2 < h1) is False