refactor: use explicit runtime facade getters

This commit is contained in:
jxxghp
2026-08-24 03:56:54 +08:00
parent 9f76fc9dec
commit 4dd2d7fed2
25 changed files with 154 additions and 98 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
# pylint: disable=no-name-in-module
import asyncio
import json
import threading
@@ -801,7 +803,7 @@ async def test_dashboard_schedule_keeps_agent_tasks(monkeypatch) -> None:
)
]
monkeypatch.setattr(
"app.api.endpoints.dashboard.Scheduler",
"app.api.endpoints.dashboard.get_scheduler",
lambda: SimpleNamespace(list=lambda: scheduler_items),
)
+2 -2
View File
@@ -229,7 +229,7 @@ def test_plugin_static_file_requires_resource_token_by_default(monkeypatch):
"""返回插件认证入口列表。"""
return []
monkeypatch.setattr(plugin_endpoint, "PluginManager", FakePluginManager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", FakePluginManager)
monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token))
plugin_endpoint._verify_plugin_static_file_access(
@@ -259,7 +259,7 @@ def test_plugin_auth_remote_files_allow_anonymous_bootstrap(monkeypatch):
}
]
monkeypatch.setattr(plugin_endpoint, "PluginManager", FakePluginManager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", FakePluginManager)
monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token))
plugin_endpoint._verify_plugin_static_file_access(
+1 -1
View File
@@ -134,7 +134,7 @@ def test_cookiecloud_sync_uses_task_registry(monkeypatch) -> None:
"""CookieCloud 手工同步应登记 Scheduler E1 任务而非 Starlette 后台回调。"""
registry = _TaskRegistry()
scheduler = SimpleNamespace(start=lambda **_kwargs: None)
monkeypatch.setattr(site, "Scheduler", lambda: scheduler)
monkeypatch.setattr(site, "get_scheduler", lambda: scheduler)
response = asyncio.run(site.cookie_cloud_sync(registry, SimpleNamespace()))
+34
View File
@@ -352,6 +352,40 @@ def test_host_code_does_not_import_legacy_roots():
assert violations == {}
def test_host_code_uses_explicit_runtime_facade_getters():
"""宿主消费者必须显式调用 getter,不得把兼容 Facade 当作新代码入口。"""
forbidden_imports = {
"app.application.module": {"ModuleManager"},
"app.application.scheduling": {"Scheduler"},
}
violations: list[str] = []
for path in APP_ROOT.rglob("*.py"):
relative = path.relative_to(APP_ROOT)
if relative.parts[0] == "plugins" or relative.parts[:2] == (
"runtime",
"compat",
):
continue
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom) or not node.module:
continue
forbidden_names = forbidden_imports.get(node.module, set())
for alias in node.names:
class_shaped_plugin_getter = (
node.module == "app.application.plugin.runtime"
and alias.name == "get_plugin_manager"
and alias.asname is not None
)
if alias.name in forbidden_names or class_shaped_plugin_getter:
imported_name = alias.asname or alias.name
violations.append(
f"{relative.as_posix()}:{node.lineno}:{imported_name}"
)
assert violations == []
def test_plugin_components_do_not_reexport_legacy_abi_names():
"""新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。"""
violations: list[str] = []
+17 -17
View File
@@ -47,7 +47,7 @@ def test_plugin_history_merges_remote_metadata():
plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[market_plugin])
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager):
result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.repo_url == "https://github.com/demo/plugins"
@@ -68,7 +68,7 @@ def test_runtime_status_reports_pending_and_terminal_counts():
plugin_manager.is_plugin_settling.return_value = True
plugin_manager.get_plugin_runtime_generation.return_value = 7
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager):
result = asyncio.run(runtime_status(None))
assert result.ready is False
@@ -82,7 +82,7 @@ def test_reload_endpoint_reports_load_failure(monkeypatch):
plugin_manager = MagicMock()
plugin_manager.reload_plugin.return_value = PluginRuntimeStatus.LOAD_FAILED
register = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "register_plugin", register)
result = reload_plugin("DemoPlugin", None)
@@ -107,7 +107,7 @@ def test_plugin_history_returns_installed_plugin_when_remote_missing():
plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[])
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager):
result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.id == "DemoPlugin"
@@ -136,7 +136,7 @@ def test_plugin_history_uses_installed_repo_without_refreshing_all_markets():
plugin_manager.async_get_plugins_from_market = AsyncMock(return_value=[market_plugin])
plugin_manager.async_get_online_plugins = AsyncMock(return_value=[])
with patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager):
with patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager):
result = asyncio.run(plugin_history("DemoPlugin", None, True))
assert result.history == {"v1.1.0": "- 新增更新说明"}
@@ -167,7 +167,7 @@ def test_plugin_releases_returns_supported_versions_with_latest_and_current(monk
])
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False))
@@ -206,7 +206,7 @@ def test_plugin_releases_does_not_mutate_cached_release_items(monkeypatch):
plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=release_items)
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False))
@@ -236,7 +236,7 @@ def test_plugin_releases_falls_back_to_compatible_base_package(monkeypatch):
plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[])
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
):
result = asyncio.run(
@@ -268,7 +268,7 @@ def test_plugin_releases_uses_force_refresh_for_market_metadata(monkeypatch):
plugin_helper.async_get_plugin_release_versions = AsyncMock(return_value=[])
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", True))
@@ -319,7 +319,7 @@ def test_plugin_releases_force_uses_cached_release_response_and_schedules_refres
scheduled.append((plugin_id, repo_url, task_registry))
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule),
):
@@ -369,7 +369,7 @@ def test_plugin_releases_force_skips_background_refresh_without_release_cache(mo
scheduled.append((plugin_id, repo_url))
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
patch.object(plugin_endpoint, "_schedule_plugin_release_refresh", fake_schedule),
):
@@ -401,7 +401,7 @@ def test_plugin_releases_hides_items_when_market_plugin_does_not_enable_release(
])
with (
patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.PluginHelper", return_value=plugin_helper),
):
result = asyncio.run(plugin_releases("DemoPlugin", None, "https://github.com/demo/plugins", False))
@@ -540,7 +540,7 @@ def test_virtual_instance_static_file_reads_from_source_directory(tmp_path, monk
source_file.write_text("export default 'shared'", encoding="utf-8")
plugin_manager = MagicMock()
plugin_manager.get_plugin_source_id.return_value = "DemoPlugin"
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(
plugin_endpoint,
"get_api_runtime_config_snapshot",
@@ -570,7 +570,7 @@ def test_uninstall_virtual_instance_never_removes_source_package(monkeypatch):
plugin_manager.get_plugin_source_instances.return_value = []
config = MagicMock()
config.get.return_value = ["DemoPlugin"]
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_configured_system_config", lambda: config)
monkeypatch.setattr(plugin_endpoint, "remove_plugin_api", MagicMock())
monkeypatch.setattr(plugin_endpoint, "remove_plugin_job", MagicMock())
@@ -604,7 +604,7 @@ def test_sealed_http_uninstall_rejects_before_first_side_effect(monkeypatch):
config_provider = MagicMock()
remove_api = MagicMock()
remove_job = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(
plugin_endpoint,
"get_configured_system_config",
@@ -631,7 +631,7 @@ def test_sealed_http_clone_rejects_before_runtime_and_registration(monkeypatch):
plugin_manager.mutation.side_effect = admission.hold
register = MagicMock()
add_to_folder = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "register_plugin", register)
monkeypatch.setattr(plugin_endpoint, "_add_clone_to_plugin_folder", add_to_folder)
@@ -655,7 +655,7 @@ def test_sealed_http_folder_update_rejects_before_config_access(monkeypatch):
plugin_manager = MagicMock()
plugin_manager.mutation.side_effect = admission.hold
config_provider = MagicMock()
monkeypatch.setattr(plugin_endpoint, "PluginManager", lambda: plugin_manager)
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: plugin_manager)
monkeypatch.setattr(
plugin_endpoint,
"get_configured_system_config",
+1 -1
View File
@@ -727,7 +727,7 @@ def test_plugin_reload_refreshes_scheduler_services_idempotently(monkeypatch):
}
]
plugin_manager.get_plugin_attr.return_value = "测试插件"
monkeypatch.setattr("app.scheduler.PluginManager", lambda: plugin_manager)
monkeypatch.setattr("app.scheduler.get_plugin_manager", lambda: plugin_manager)
backend = _FakeSchedulerBackend(["DemoPlugin_old"])
scheduler = _build_scheduler_for_plugin_reload(
jobs={
+1 -1
View File
@@ -86,7 +86,7 @@ def test_clear_cache_is_manual_only(monkeypatch):
"TransferChain",
"WallpaperHelper",
"WorkflowChain",
"PluginManager",
"get_plugin_manager",
]:
monkeypatch.setattr(scheduler_module, name, lambda: generic_chain)
monkeypatch.setattr(
+1 -1
View File
@@ -30,7 +30,7 @@ async def test_reset_submits_cookiecloud_after_site_transaction(monkeypatch):
system_config = Mock()
system_config.async_set = AsyncMock()
monkeypatch.setattr(site_endpoint, "Scheduler", Mock(return_value=scheduler))
monkeypatch.setattr(site_endpoint, "get_scheduler", Mock(return_value=scheduler))
monkeypatch.setattr(
site_endpoint,
"get_configured_system_config",
+2 -2
View File
@@ -825,7 +825,7 @@ class SubscribeEndpointTest(TestCase):
for endpoint in [refresh_subscribes, check_subscribes]:
with self.subTest(endpoint=endpoint.__name__), patch(
"app.api.endpoints.subscribe.Scheduler"
"app.api.endpoints.subscribe.get_scheduler"
) as scheduler:
response = endpoint(current_user=regular_user)
@@ -838,7 +838,7 @@ class SubscribeEndpointTest(TestCase):
(check_subscribes, "subscribe_tmdb"),
]:
with self.subTest(endpoint=endpoint.__name__), patch(
"app.api.endpoints.subscribe.Scheduler"
"app.api.endpoints.subscribe.get_scheduler"
) as scheduler:
response = endpoint(current_user=superuser)
+2 -2
View File
@@ -22,7 +22,7 @@ class _FakeModuleManager:
def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name():
"""模块列表接口应保留旧中文字段,并提供前端可用的多语言字段。"""
token = LocaleHelper.set_current_locale("en-US")
with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()):
with patch.object(system_endpoint, "get_module_manager", return_value=_FakeModuleManager()):
try:
response = system_endpoint.modulelist(_="token")
finally:
@@ -38,7 +38,7 @@ def test_system_modulelist_keeps_chinese_name_and_adds_i18n_name():
def test_system_moduletest_localizes_message():
"""模块测试接口应按当前请求语言直接返回翻译后的 message。"""
token = LocaleHelper.set_current_locale("en-US")
with patch.object(system_endpoint, "ModuleManager", return_value=_FakeModuleManager()):
with patch.object(system_endpoint, "get_module_manager", return_value=_FakeModuleManager()):
try:
response = system_endpoint.moduletest("DoubanModule", _="token")
finally:
+1 -1
View File
@@ -17,7 +17,7 @@ PROJECT_ROOT = Path(__file__).parents[1]
def test_create_app_does_not_start_plugin_manager_or_threads(monkeypatch):
"""ASGI factory 只构建应用结构,不得在创建阶段物化插件运行时。"""
plugin_manager = MagicMock(side_effect=AssertionError("plugin runtime started"))
monkeypatch.setattr(factory, "PluginManager", plugin_manager)
monkeypatch.setattr(factory, "get_plugin_manager", plugin_manager)
threads_before = threading.active_count()
created = factory.create_app()