1- import os
1+ import sys
22from asyncio import Queue
3+ from pathlib import Path
34from unittest .mock import MagicMock
45
56import pytest
7+ import pytest_asyncio
68
79from astrbot .core .config .astrbot_config import AstrBotConfig
810from astrbot .core .db .sqlite import SQLiteDatabase
911from astrbot .core .star .context import Context
10- from astrbot .core .star .star import star_registry
12+ from astrbot .core .star .star import star_map , star_registry
1113from astrbot .core .star .star_handler import star_handlers_registry
1214from astrbot .core .star .star_manager import PluginManager
1315
1416
15- @pytest .fixture
16- def plugin_manager_pm (tmp_path ):
17- """Provides a fully isolated PluginManager instance for testing.
18- - Uses a temporary directory for plugins.
19- - Uses a temporary database.
20- - Creates a fresh context for each test.
21- """
22- # Create temporary resources
23- temp_plugins_path = tmp_path / "plugins"
24- temp_plugins_path .mkdir ()
25- temp_db_path = tmp_path / "test_db.db"
17+ def _clear_module_cache () -> None :
18+ """Clear module cache for data module tree to ensure test isolation."""
19+ modules_to_remove = [
20+ key for key in sys .modules if key == "data" or key .startswith ("data." )
21+ ]
22+ for key in modules_to_remove :
23+ del sys .modules [key ]
24+
25+
26+ def _clear_registry (plugin_name : str ) -> None :
27+ """Clear plugin from global registries."""
28+ # Clear star_registry (list)
29+ star_registry [:] = [md for md in star_registry if md .name != plugin_name ]
30+ # Clear star_map (dict)
31+ keys_to_remove = [
32+ key for key , md in star_map .items () if md .name == plugin_name
33+ ]
34+ for key in keys_to_remove :
35+ del star_map [key ]
36+ # Clear star_handlers_registry (StarHandlerRegistry)
37+ for handler in list (star_handlers_registry ):
38+ if plugin_name in (handler .handler_module_path or "" ):
39+ star_handlers_registry .remove (handler )
40+
41+ TEST_PLUGIN_REPO = "https://github.com/Soulter/helloworld"
42+ TEST_PLUGIN_DIR = "helloworld"
43+ TEST_PLUGIN_NAME = "helloworld"
44+
45+
46+ def _write_local_test_plugin (plugin_dir : Path , repo_url : str ) -> None :
47+ plugin_dir .mkdir (parents = True , exist_ok = True )
48+ (plugin_dir / "metadata.yaml" ).write_text (
49+ "\n " .join (
50+ [
51+ f"name: { TEST_PLUGIN_NAME } " ,
52+ "author: AstrBot Team" ,
53+ "desc: Local test plugin" ,
54+ "version: 1.0.0" ,
55+ f"repo: { repo_url } " ,
56+ ],
57+ )
58+ + "\n " ,
59+ encoding = "utf-8" ,
60+ )
61+ (plugin_dir / "main.py" ).write_text (
62+ "\n " .join (
63+ [
64+ "from astrbot.api import star" ,
65+ "" ,
66+ "class Main(star.Star):" ,
67+ " pass" ,
68+ "" ,
69+ ],
70+ ),
71+ encoding = "utf-8" ,
72+ )
73+
74+
75+ @pytest_asyncio .fixture
76+ async def plugin_manager_pm (tmp_path , monkeypatch ):
77+ """Provides a fully isolated PluginManager instance for testing."""
78+ # Clear module cache before setup to ensure isolation
79+ _clear_module_cache ()
80+
81+ test_root = tmp_path / "astrbot_root"
82+ data_dir = test_root / "data"
83+ plugin_dir = data_dir / "plugins"
84+ config_dir = data_dir / "config"
85+ temp_dir = data_dir / "temp"
86+ for path in (plugin_dir , config_dir , temp_dir ):
87+ path .mkdir (parents = True , exist_ok = True )
88+
89+ # Ensure `import data.plugins.<plugin>.main` resolves to this temp root.
90+ (data_dir / "__init__.py" ).write_text ("" , encoding = "utf-8" )
91+ (plugin_dir / "__init__.py" ).write_text ("" , encoding = "utf-8" )
92+
93+ # Use monkeypatch for both env var and sys.path to ensure proper cleanup
94+ monkeypatch .setenv ("ASTRBOT_ROOT" , str (test_root ))
95+ monkeypatch .syspath_prepend (str (test_root ))
2696
2797 # Create fresh, isolated instances for the context
2898 event_queue = Queue ()
2999 config = AstrBotConfig ()
30- db = SQLiteDatabase (str (temp_db_path ))
31-
32- # Set the plugin store path in the config to the temporary directory
33- config .plugin_store_path = str (temp_plugins_path )
100+ db = SQLiteDatabase (str (data_dir / "test_db.db" ))
101+ config .plugin_store_path = str (plugin_dir )
34102
35- # Mock dependencies for the context
36103 provider_manager = MagicMock ()
37104 platform_manager = MagicMock ()
38105 conversation_manager = MagicMock ()
39106 message_history_manager = MagicMock ()
40107 persona_manager = MagicMock ()
108+ persona_manager .personas_v3 = []
41109 astrbot_config_mgr = MagicMock ()
42110 knowledge_base_manager = MagicMock ()
111+ cron_manager = MagicMock ()
43112
44113 star_context = Context (
45- event_queue ,
46- config ,
47- db ,
48- provider_manager ,
49- platform_manager ,
50- conversation_manager ,
51- message_history_manager ,
52- persona_manager ,
53- astrbot_config_mgr ,
114+ event_queue = event_queue ,
115+ config = config ,
116+ db = db ,
117+ provider_manager = provider_manager ,
118+ platform_manager = platform_manager ,
119+ conversation_manager = conversation_manager ,
120+ message_history_manager = message_history_manager ,
121+ persona_manager = persona_manager ,
122+ astrbot_config_mgr = astrbot_config_mgr ,
54123 knowledge_base_manager = knowledge_base_manager ,
124+ cron_manager = cron_manager ,
125+ subagent_orchestrator = None ,
55126 )
56127
57- # Create the PluginManager instance
58128 manager = PluginManager (star_context , config )
59- return manager
129+ try :
130+ yield manager
131+ finally :
132+ # Cleanup global registries and module cache
133+ _clear_registry (TEST_PLUGIN_NAME )
134+ _clear_module_cache ()
135+ await db .engine .dispose ()
136+
137+
138+ @pytest .fixture
139+ def local_updator (plugin_manager_pm : PluginManager , monkeypatch ):
140+ plugin_path = Path (plugin_manager_pm .plugin_store_path ) / TEST_PLUGIN_DIR
60141
142+ async def mock_install (repo_url : str , proxy = "" ): # noqa: ARG001
143+ if repo_url != TEST_PLUGIN_REPO :
144+ raise Exception ("Repo not found" )
145+ _write_local_test_plugin (plugin_path , repo_url )
146+ return str (plugin_path )
61147
62- def test_plugin_manager_initialization (plugin_manager_pm : PluginManager ):
148+ async def mock_update (plugin , proxy = "" ): # noqa: ARG001
149+ if plugin .name != TEST_PLUGIN_NAME :
150+ raise Exception ("Plugin not found" )
151+ if not plugin_path .exists ():
152+ raise Exception ("Plugin path missing" )
153+ (plugin_path / ".updated" ).write_text ("ok" , encoding = "utf-8" )
154+
155+ monkeypatch .setattr (plugin_manager_pm .updator , "install" , mock_install )
156+ monkeypatch .setattr (plugin_manager_pm .updator , "update" , mock_update )
157+ return plugin_path
158+
159+
160+ @pytest .mark .asyncio
161+ async def test_plugin_manager_initialization (plugin_manager_pm : PluginManager ):
63162 assert plugin_manager_pm is not None
64163 assert plugin_manager_pm .context is not None
65164 assert plugin_manager_pm .config is not None
@@ -73,73 +172,59 @@ async def test_plugin_manager_reload(plugin_manager_pm: PluginManager):
73172
74173
75174@pytest .mark .asyncio
76- async def test_install_plugin (plugin_manager_pm : PluginManager ):
77- """Tests successful plugin installation in an isolated environment."""
78- test_repo = "https://github.com/Soulter/astrbot_plugin_essential"
79- plugin_info = await plugin_manager_pm .install_plugin (test_repo )
80- plugin_path = os .path .join (
81- plugin_manager_pm .plugin_store_path ,
82- "astrbot_plugin_essential" ,
83- )
84-
175+ async def test_install_plugin (plugin_manager_pm : PluginManager , local_updator : Path ):
176+ """Tests successful plugin installation without external network."""
177+ plugin_info = await plugin_manager_pm .install_plugin (TEST_PLUGIN_REPO )
85178 assert plugin_info is not None
86- assert os .path .exists (plugin_path )
87- assert any (md .name == "astrbot_plugin_essential" for md in star_registry ), (
88- "Plugin 'astrbot_plugin_essential' was not loaded into star_registry."
89- )
179+ assert plugin_info ["name" ] == TEST_PLUGIN_NAME
180+ assert local_updator .exists ()
181+ assert any (md .name == TEST_PLUGIN_NAME for md in star_registry )
90182
91183
92184@pytest .mark .asyncio
93- async def test_install_nonexistent_plugin (plugin_manager_pm : PluginManager ):
185+ async def test_install_nonexistent_plugin (
186+ plugin_manager_pm : PluginManager , local_updator
187+ ):
94188 """Tests that installing a non-existent plugin raises an exception."""
95189 with pytest .raises (Exception ):
96190 await plugin_manager_pm .install_plugin (
97- "https://github.com/Soulter/non_existent_repo" ,
191+ "https://github.com/Soulter/non_existent_repo"
98192 )
99193
100194
101195@pytest .mark .asyncio
102- async def test_update_plugin (plugin_manager_pm : PluginManager ):
103- """Tests updating an existing plugin in an isolated environment."""
104- # First, install the plugin
105- test_repo = "https://github.com/Soulter/astrbot_plugin_essential"
106- await plugin_manager_pm .install_plugin (test_repo )
107-
108- # Then, update it
109- await plugin_manager_pm .update_plugin ("astrbot_plugin_essential" )
196+ async def test_update_plugin (plugin_manager_pm : PluginManager , local_updator : Path ):
197+ """Tests updating an existing plugin without external network."""
198+ plugin_info = await plugin_manager_pm .install_plugin (TEST_PLUGIN_REPO )
199+ assert plugin_info is not None
200+ plugin_name = plugin_info ["name" ]
201+ await plugin_manager_pm .update_plugin (plugin_name )
202+ assert (local_updator / ".updated" ).exists ()
110203
111204
112205@pytest .mark .asyncio
113- async def test_update_nonexistent_plugin (plugin_manager_pm : PluginManager ):
206+ async def test_update_nonexistent_plugin (
207+ plugin_manager_pm : PluginManager , local_updator
208+ ):
114209 """Tests that updating a non-existent plugin raises an exception."""
115210 with pytest .raises (Exception ):
116211 await plugin_manager_pm .update_plugin ("non_existent_plugin" )
117212
118213
119214@pytest .mark .asyncio
120- async def test_uninstall_plugin (plugin_manager_pm : PluginManager ):
121- """Tests successful plugin uninstallation in an isolated environment."""
122- # First, install the plugin
123- test_repo = "https://github.com/Soulter/astrbot_plugin_essential"
124- await plugin_manager_pm .install_plugin (test_repo )
125- plugin_path = os .path .join (
126- plugin_manager_pm .plugin_store_path ,
127- "astrbot_plugin_essential" ,
128- )
129- assert os .path .exists (plugin_path ) # Pre-condition
215+ async def test_uninstall_plugin (plugin_manager_pm : PluginManager , local_updator : Path ):
216+ """Tests successful plugin uninstallation."""
217+ plugin_info = await plugin_manager_pm .install_plugin (TEST_PLUGIN_REPO )
218+ assert plugin_info is not None
219+ plugin_name = plugin_info ["name" ]
220+ assert local_updator .exists ()
130221
131- # Then, uninstall it
132- await plugin_manager_pm .uninstall_plugin ("astrbot_plugin_essential" )
222+ await plugin_manager_pm .uninstall_plugin (plugin_name )
133223
134- assert not os .path .exists (plugin_path )
135- assert not any (md .name == "astrbot_plugin_essential" for md in star_registry ), (
136- "Plugin 'astrbot_plugin_essential' was not unloaded from star_registry."
137- )
224+ assert not local_updator .exists ()
225+ assert not any (md .name == TEST_PLUGIN_NAME for md in star_registry )
138226 assert not any (
139- "astrbot_plugin_essential" in md .handler_module_path
140- for md in star_handlers_registry
141- ), (
142- "Plugin 'astrbot_plugin_essential' handler was not unloaded from star_handlers_registry."
227+ TEST_PLUGIN_NAME in md .handler_module_path for md in star_handlers_registry
143228 )
144229
145230
0 commit comments