Skip to content

Commit 587de55

Browse files
whatevertogoclaude
andcommitted
test: fix plugin_manager test isolation issues
- Use local mock plugin instead of real network requests - Clear sys.modules cache for entire data module tree - Clear star_map and star_registry in teardown - Use pytest_asyncio.fixture for async fixture support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9294b44 commit 587de55

1 file changed

Lines changed: 159 additions & 74 deletions

File tree

tests/test_plugin_manager.py

Lines changed: 159 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,164 @@
1-
import os
1+
import sys
22
from asyncio import Queue
3+
from pathlib import Path
34
from unittest.mock import MagicMock
45

56
import pytest
7+
import pytest_asyncio
68

79
from astrbot.core.config.astrbot_config import AstrBotConfig
810
from astrbot.core.db.sqlite import SQLiteDatabase
911
from 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
1113
from astrbot.core.star.star_handler import star_handlers_registry
1214
from 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

Comments
 (0)