From d17d62e95807aa3f9dc46151d6d8b4d6ced5bbed Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 29 Aug 2026 15:26:31 +0800 Subject: [PATCH] fix(startup): order plugin service composition --- app/startup/initializers/plugins.py | 19 +- app/startup/lifecycle/__init__.py | 16 +- docs/architecture-optimization-checklist.md | 2 +- .../fixtures/architecture/mypy-baseline.json | 4 +- .../startup-performance-baseline.json | 336 +++++++++--------- tests/test_lifecycle_shutdown.py | 16 +- .../test_plugin_identity_startup_migration.py | 7 +- tests/test_plugin_manager_governance.py | 57 +-- tests/test_plugin_monitor_lifecycle.py | 6 - 9 files changed, 226 insertions(+), 237 deletions(-) diff --git a/app/startup/initializers/plugins.py b/app/startup/initializers/plugins.py index bcd53ea96..bb8ef281d 100644 --- a/app/startup/initializers/plugins.py +++ b/app/startup/initializers/plugins.py @@ -3,7 +3,7 @@ import uuid from collections.abc import Callable from datetime import datetime, timezone from pathlib import Path -from typing import Any +from typing import Any, cast from app.adapters.external.market import ( LOCAL_REPO_PREFIX, @@ -66,6 +66,9 @@ from app.application.plugin.release import ( reset_plugin_release_service, ) from app.application.plugin.routes import register_plugin_api +from app.application.plugin.runtime import ( + PluginRuntime as PluginRuntimePort, +) from app.application.plugin.runtime import ( configure_plugin_runtime, get_plugin_manager, @@ -168,17 +171,21 @@ def build_plugin_runtime_graph(host: PluginRuntimeHost) -> PluginRuntime: ) +def configure_plugin_runtime_services() -> None: + """在模块对象图构造前发布插件 Runtime 工厂和应用层提供器。""" + configure_plugin_catalog_factory(_build_plugin_catalog) + configure_plugin_runtime_factory(build_plugin_runtime_graph) + configure_plugin_runtime(lambda: cast(PluginRuntimePort, PluginManager())) + + def configure_plugin_services() -> None: - """把兼容诊断、远程上报和站点认证等级装配到插件管理器。""" + """在模块持久化端口就绪后装配完整插件应用服务。""" market_transport = PluginMarketTransport() market_client = PluginMarketClient(market_transport) package_manager = PluginPackageManager( source=PluginPackageSourceClient(market_transport), plugin_root=Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins", ) - configure_plugin_catalog_factory(_build_plugin_catalog) - configure_plugin_runtime_factory(build_plugin_runtime_graph) - configure_plugin_runtime(lambda: PluginManager()) plugin_manager = get_plugin_manager() inventory_reader = PluginCandidateInventoryReader( market_loader=market_client.get_plugin_index_result, @@ -519,7 +526,6 @@ async def sync_plugins( plugin_manager = None try: loop = main_loop_registry.require() - configure_plugin_services() plugin_manager = PluginManager() with plugin_manager.mutation("启动后同步插件"): await get_plugin_identity_migration().migrate() @@ -714,7 +720,6 @@ def init_plugins(): """ 初始化插件 """ - configure_plugin_services() plugin_manager = PluginManager() if not plugin_manager.reopen_plugins(): raise RuntimeError("上一应用生命周期的插件后台服务仍未收敛") diff --git a/app/startup/lifecycle/__init__.py b/app/startup/lifecycle/__init__.py index 7e6ac1a9e..471d733e1 100644 --- a/app/startup/lifecycle/__init__.py +++ b/app/startup/lifecycle/__init__.py @@ -63,6 +63,7 @@ from app.startup.initializers.network import ( # noqa: E402 reset_chain_network_ports, ) from app.startup.initializers.plugins import ( + configure_plugin_runtime_services, configure_plugin_services, execute_task, finalize_plugins, @@ -469,9 +470,9 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]: start_timeout_seconds=30, ), LifecycleComponent( - name="插件服务装配", + name="插件运行时装配", dependencies=("路由",), - start=configure_plugin_services, + start=configure_plugin_runtime_services, start_order=65, start_timeout_seconds=30, ), @@ -479,7 +480,7 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]: name="模块服务", dependencies=( "路由", - "插件服务装配", + "插件运行时装配", "站点访问端口", "Chain 外部端口", "Chain 网络端口", @@ -491,9 +492,16 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]: start_timeout_seconds=300, stop_timeout_seconds=300, ), + LifecycleComponent( + name="插件服务装配", + dependencies=("模块服务",), + start=configure_plugin_services, + start_order=72, + start_timeout_seconds=30, + ), LifecycleComponent( name="插件备份恢复", - dependencies=("模块服务", "消息队列"), + dependencies=("插件服务装配", "消息队列"), mode=LifecycleMode.NORMAL_ONLY, start=prepare_plugin_restore, start_order=80, diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md index 853dad9a4..beb64a05d 100644 --- a/docs/architecture-optimization-checklist.md +++ b/docs/architecture-optimization-checklist.md @@ -78,7 +78,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement | | Python 源码量 | 约 298,700 行 | 60 个文件超过 1,000 行,10 个超过 2,000 行 | | 长方法 | 299 个超过 80 行 | 70 个超过 150 行,23 个超过 250 行;大量是私有方法 | -| 全量 mypy 历史债务 | 10,415 / 593 文件 | strict frontier 当前覆盖 41 个文件,Plugin、LLM Provider 与 Agent Manager owner 拆分及受益调用方低水位已固化 | +| 全量 mypy 历史债务 | 10,413 / 593 文件 | strict frontier 当前覆盖 41 个文件,Plugin、LLM Provider 与 Agent Manager owner 拆分及受益调用方低水位已固化 | | Ruff 历史诊断 | 651 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | | 覆盖率低水位 | Application 80.92%,Domain 79.89% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index 461746e43..11fc6c280 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -3285,14 +3285,14 @@ "no-untyped-def": 2 }, "app/startup/initializers/modules.py": { - "arg-type": 7, + "arg-type": 6, "assignment": 1, "attr-defined": 2, "misc": 1, "no-any-return": 2, "no-untyped-call": 28, "no-untyped-def": 6, - "return-value": 3 + "return-value": 2 }, "app/startup/initializers/plugins.py": { "arg-type": 5, diff --git a/tests/fixtures/architecture/startup-performance-baseline.json b/tests/fixtures/architecture/startup-performance-baseline.json index c23037c82..0b614b5eb 100644 --- a/tests/fixtures/architecture/startup-performance-baseline.json +++ b/tests/fixtures/architecture/startup-performance-baseline.json @@ -1,41 +1,41 @@ { "schema_version": 2, - "generated_at": "2026-08-29T05:54:31.505070+00:00", + "generated_at": "2026-08-29T07:27:24.582316+00:00", "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O", "python": "3.14.3", "repeat": 3, "targets": { "app.startup.lifecycle": { "loaded_app_module_count": 491, - "max_ms": 1180.874, - "median_ms": 1117.895, - "min_ms": 1107.448, + "max_ms": 1159.462, + "median_ms": 1074.592, + "min_ms": 1072.211, "samples_ms": [ - 1107.448, - 1117.895, - 1180.874 + 1159.462, + 1074.592, + 1072.211 ] }, "app.factory": { "loaded_app_module_count": 503, - "max_ms": 1160.129, - "median_ms": 1133.423, - "min_ms": 1109.307, + "max_ms": 1094.111, + "median_ms": 1092.394, + "min_ms": 1090.171, "samples_ms": [ - 1160.129, - 1133.423, - 1109.307 + 1090.171, + 1092.394, + 1094.111 ] }, "app.main": { "loaded_app_module_count": 505, - "max_ms": 1194.892, - "median_ms": 1191.744, - "min_ms": 1190.037, + "max_ms": 1165.991, + "median_ms": 1159.713, + "min_ms": 1143.531, "samples_ms": [ - 1194.892, - 1191.744, - 1190.037 + 1143.531, + 1159.713, + 1165.991 ] } }, @@ -46,69 +46,33 @@ "samples": [ { "mode": "normal", - "enabled_component_count": 31, - "startup_ms": 0.667, - "full_lifespan_ms": 1.794, - "stage_ms": { - "文件日志": 0.069, - "后台任务登记器": 0.036, - "数据库准备": 0.027, - "HTTP 基础能力": 0.025, - "站点访问端口": 0.02, - "Chain 外部端口": 0.019, - "Chain 网络端口": 0.018, - "领域依赖装配": 0.018, - "数据库引擎预热": 0.018, - "数据库连接预算": 0.018, - "路由": 0.021, - "插件服务装配": 0.018, - "模块服务": 0.018, - "消息队列": 0.018, - "插件备份恢复": 0.018, - "插件": 0.017, - "定时器": 0.017, - "监控器": 0.017, - "待处理整理回放": 0.018, - "命令服务": 0.017, - "工作流": 0.017, - "插件同步与启动收尾": 0.017 - }, - "threads_before": 1, - "threads_started": 1, - "threads_after": 1, - "tasks_before": 1, - "tasks_started": 1, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "normal", - "enabled_component_count": 31, - "startup_ms": 0.656, - "full_lifespan_ms": 1.933, + "enabled_component_count": 32, + "startup_ms": 0.756, + "full_lifespan_ms": 1.874, "stage_ms": { "文件日志": 0.07, - "后台任务登记器": 0.036, - "数据库准备": 0.027, - "HTTP 基础能力": 0.022, - "站点访问端口": 0.022, - "Chain 外部端口": 0.019, - "Chain 网络端口": 0.018, - "领域依赖装配": 0.018, - "数据库引擎预热": 0.018, - "数据库连接预算": 0.017, - "路由": 0.019, - "插件服务装配": 0.018, - "模块服务": 0.017, - "消息队列": 0.018, - "插件备份恢复": 0.017, - "插件": 0.017, - "定时器": 0.018, + "后台任务登记器": 0.043, + "数据库准备": 0.032, + "HTTP 基础能力": 0.028, + "站点访问端口": 0.024, + "Chain 外部端口": 0.023, + "Chain 网络端口": 0.02, + "领域依赖装配": 0.022, + "数据库引擎预热": 0.02, + "数据库连接预算": 0.022, + "路由": 0.023, + "插件运行时装配": 0.022, + "模块服务": 0.02, + "插件服务装配": 0.022, + "消息队列": 0.02, + "插件备份恢复": 0.021, + "插件": 0.021, + "定时器": 0.021, "监控器": 0.017, - "待处理整理回放": 0.017, + "待处理整理回放": 0.019, "命令服务": 0.017, - "工作流": 0.017, - "插件同步与启动收尾": 0.017 + "工作流": 0.02, + "插件同步与启动收尾": 0.021 }, "threads_before": 1, "threads_started": 1, @@ -120,32 +84,71 @@ }, { "mode": "normal", - "enabled_component_count": 31, - "startup_ms": 0.737, - "full_lifespan_ms": 1.891, + "enabled_component_count": 32, + "startup_ms": 0.709, + "full_lifespan_ms": 1.824, "stage_ms": { - "文件日志": 0.074, + "文件日志": 0.064, + "后台任务登记器": 0.034, + "数据库准备": 0.026, + "HTTP 基础能力": 0.025, + "站点访问端口": 0.021, + "Chain 外部端口": 0.021, + "Chain 网络端口": 0.02, + "领域依赖装配": 0.02, + "数据库引擎预热": 0.017, + "数据库连接预算": 0.019, + "路由": 0.02, + "插件运行时装配": 0.019, + "模块服务": 0.02, + "插件服务装配": 0.021, + "消息队列": 0.021, + "插件备份恢复": 0.021, + "插件": 0.02, + "定时器": 0.02, + "监控器": 0.021, + "待处理整理回放": 0.021, + "命令服务": 0.021, + "工作流": 0.02, + "插件同步与启动收尾": 0.021 + }, + "threads_before": 1, + "threads_started": 1, + "threads_after": 1, + "tasks_before": 1, + "tasks_started": 1, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "normal", + "enabled_component_count": 32, + "startup_ms": 0.751, + "full_lifespan_ms": 1.889, + "stage_ms": { + "文件日志": 0.069, "后台任务登记器": 0.037, - "数据库准备": 0.029, - "HTTP 基础能力": 0.03, + "数据库准备": 0.028, + "HTTP 基础能力": 0.029, "站点访问端口": 0.024, "Chain 外部端口": 0.023, "Chain 网络端口": 0.022, - "领域依赖装配": 0.021, + "领域依赖装配": 0.022, "数据库引擎预热": 0.021, "数据库连接预算": 0.021, "路由": 0.022, - "插件服务装配": 0.021, - "模块服务": 0.02, - "消息队列": 0.02, - "插件备份恢复": 0.02, - "插件": 0.019, - "定时器": 0.02, - "监控器": 0.02, - "待处理整理回放": 0.02, - "命令服务": 0.02, - "工作流": 0.02, - "插件同步与启动收尾": 0.017 + "插件运行时装配": 0.021, + "模块服务": 0.021, + "插件服务装配": 0.022, + "消息队列": 0.021, + "插件备份恢复": 0.021, + "插件": 0.021, + "定时器": 0.021, + "监控器": 0.021, + "待处理整理回放": 0.021, + "命令服务": 0.021, + "工作流": 0.021, + "插件同步与启动收尾": 0.021 }, "threads_before": 1, "threads_started": 1, @@ -156,9 +159,9 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.667, - "median_full_lifespan_ms": 1.891, - "enabled_component_count": 31, + "median_startup_ms": 0.751, + "median_full_lifespan_ms": 1.874, + "enabled_component_count": 32, "enabled_components": [ "文件日志", "后台任务登记器", @@ -171,8 +174,9 @@ "数据库引擎预热", "数据库连接预算", "路由", - "插件服务装配", + "插件运行时装配", "模块服务", + "插件服务装配", "插件备份恢复", "消息队列", "插件", @@ -197,24 +201,56 @@ "samples": [ { "mode": "safe", - "enabled_component_count": 19, - "startup_ms": 0.556, - "full_lifespan_ms": 1.16, + "enabled_component_count": 20, + "startup_ms": 0.614, + "full_lifespan_ms": 1.263, "stage_ms": { - "文件日志": 0.072, - "后台任务登记器": 0.04, - "数据库准备": 0.026, - "HTTP 基础能力": 0.023, - "站点访问端口": 0.02, - "Chain 外部端口": 0.019, - "Chain 网络端口": 0.018, - "领域依赖装配": 0.018, - "数据库引擎预热": 0.018, - "数据库连接预算": 0.017, - "路由": 0.018, - "插件服务装配": 0.018, - "模块服务": 0.017, - "消息队列": 0.017, + "文件日志": 0.069, + "后台任务登记器": 0.039, + "数据库准备": 0.03, + "HTTP 基础能力": 0.027, + "站点访问端口": 0.024, + "Chain 外部端口": 0.024, + "Chain 网络端口": 0.022, + "领域依赖装配": 0.02, + "数据库引擎预热": 0.022, + "数据库连接预算": 0.026, + "路由": 0.023, + "插件运行时装配": 0.021, + "模块服务": 0.022, + "插件服务装配": 0.021, + "消息队列": 0.021, + "插件同步与启动收尾": 0.02 + }, + "threads_before": 1, + "threads_started": 1, + "threads_after": 1, + "tasks_before": 1, + "tasks_started": 1, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "safe", + "enabled_component_count": 20, + "startup_ms": 0.609, + "full_lifespan_ms": 1.252, + "stage_ms": { + "文件日志": 0.071, + "后台任务登记器": 0.036, + "数据库准备": 0.028, + "HTTP 基础能力": 0.026, + "站点访问端口": 0.021, + "Chain 外部端口": 0.021, + "Chain 网络端口": 0.021, + "领域依赖装配": 0.019, + "数据库引擎预热": 0.027, + "数据库连接预算": 0.022, + "路由": 0.026, + "插件运行时装配": 0.022, + "模块服务": 0.021, + "插件服务装配": 0.021, + "消息队列": 0.02, "插件同步与启动收尾": 0.017 }, "threads_before": 1, @@ -227,55 +263,26 @@ }, { "mode": "safe", - "enabled_component_count": 19, - "startup_ms": 0.637, - "full_lifespan_ms": 1.26, + "enabled_component_count": 20, + "startup_ms": 0.582, + "full_lifespan_ms": 1.229, "stage_ms": { - "文件日志": 0.081, - "后台任务登记器": 0.038, - "数据库准备": 0.03, - "HTTP 基础能力": 0.027, - "站点访问端口": 0.024, - "Chain 外部端口": 0.022, - "Chain 网络端口": 0.021, - "领域依赖装配": 0.022, - "数据库引擎预热": 0.022, - "数据库连接预算": 0.022, - "路由": 0.021, - "插件服务装配": 0.022, - "模块服务": 0.021, - "消息队列": 0.021, - "插件同步与启动收尾": 0.021 - }, - "threads_before": 1, - "threads_started": 1, - "threads_after": 1, - "tasks_before": 1, - "tasks_started": 1, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "safe", - "enabled_component_count": 19, - "startup_ms": 0.578, - "full_lifespan_ms": 1.189, - "stage_ms": { - "文件日志": 0.071, - "后台任务登记器": 0.036, + "文件日志": 0.072, + "后台任务登记器": 0.042, "数据库准备": 0.028, - "HTTP 基础能力": 0.027, + "HTTP 基础能力": 0.025, "站点访问端口": 0.023, - "Chain 外部端口": 0.022, + "Chain 外部端口": 0.021, "Chain 网络端口": 0.022, - "领域依赖装配": 0.02, - "数据库引擎预热": 0.021, - "数据库连接预算": 0.021, - "路由": 0.022, - "插件服务装配": 0.021, + "领域依赖装配": 0.017, + "数据库引擎预热": 0.019, + "数据库连接预算": 0.019, + "路由": 0.019, + "插件运行时装配": 0.02, "模块服务": 0.02, + "插件服务装配": 0.019, "消息队列": 0.021, - "插件同步与启动收尾": 0.02 + "插件同步与启动收尾": 0.019 }, "threads_before": 1, "threads_started": 1, @@ -286,9 +293,9 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.578, - "median_full_lifespan_ms": 1.189, - "enabled_component_count": 19, + "median_startup_ms": 0.609, + "median_full_lifespan_ms": 1.252, + "enabled_component_count": 20, "enabled_components": [ "文件日志", "后台任务登记器", @@ -301,8 +308,9 @@ "数据库引擎预热", "数据库连接预算", "路由", - "插件服务装配", + "插件运行时装配", "模块服务", + "插件服务装配", "消息队列", "整理后台服务", "AI智能体会话", diff --git a/tests/test_lifecycle_shutdown.py b/tests/test_lifecycle_shutdown.py index 8b7a4e365..ab5412df2 100644 --- a/tests/test_lifecycle_shutdown.py +++ b/tests/test_lifecycle_shutdown.py @@ -45,6 +45,7 @@ def _patch_lifespan(monkeypatch, *, failing_step: str | None = None) -> dict: "init_workflow", ): monkeypatch.setattr(lifecycle, name, MagicMock()) + monkeypatch.setattr(lifecycle, "configure_plugin_runtime_services", MagicMock()) monkeypatch.setattr(lifecycle, "configure_plugin_services", MagicMock()) plugin_recovery = MagicMock() plugin_recovery.replay = AsyncMock() @@ -547,12 +548,13 @@ def test_lifespan_waits_for_uncancellable_plugin_settlement_before_shutdown( assert order[:2] == ["settled", "backup"] -def test_lifespan_configures_plugin_services_before_modules_and_restore(monkeypatch): - """插件 Runtime 必须先于模块 Chain 上下文和恢复阶段完成装配。""" +def test_lifespan_configures_plugin_runtime_and_services_in_dependency_order(monkeypatch): + """插件 Runtime、模块持久化和应用服务必须按依赖顺序装配。""" shutdown_steps = _patch_lifespan(monkeypatch) order = [] - lifecycle.configure_plugin_services.side_effect = lambda: order.append("configure") + lifecycle.configure_plugin_runtime_services.side_effect = lambda: order.append("runtime") lifecycle.init_modules.side_effect = lambda: order.append("modules") + lifecycle.configure_plugin_services.side_effect = lambda: order.append("services") lifecycle.get_plugin_installation_recovery.return_value.replay.side_effect = ( lambda: order.append("replay") ) @@ -566,7 +568,7 @@ def test_lifespan_configures_plugin_services_before_modules_and_restore(monkeypa asyncio.run(run_lifespan()) - assert order == ["configure", "modules", "replay", "restore"] + assert order == ["runtime", "modules", "services", "replay", "restore"] _assert_completed_once(shutdown_steps["close_http"]) @@ -671,8 +673,9 @@ def test_lifecycle_manifest_declares_normal_and_safe_mode_order() -> None: "数据库引擎预热", "数据库连接预算", "路由", - "插件服务装配", + "插件运行时装配", "模块服务", + "插件服务装配", "消息队列", "插件备份恢复", "插件", @@ -719,8 +722,9 @@ def test_lifecycle_manifest_declares_normal_and_safe_mode_order() -> None: "数据库引擎预热", "数据库连接预算", "路由", - "插件服务装配", + "插件运行时装配", "模块服务", + "插件服务装配", "消息队列", "AI智能体会话", "整理后台服务", diff --git a/tests/test_plugin_identity_startup_migration.py b/tests/test_plugin_identity_startup_migration.py index 3894a2e34..feb54baba 100644 --- a/tests/test_plugin_identity_startup_migration.py +++ b/tests/test_plugin_identity_startup_migration.py @@ -505,11 +505,6 @@ async def test_sync_runs_identity_migration_before_automatic_install( "require", lambda: asyncio.get_running_loop(), ) - monkeypatch.setattr( - plugins_initializer, - "configure_plugin_services", - lambda: order.append("configure"), - ) monkeypatch.setattr(plugins_initializer, "PluginManager", lambda: manager) monkeypatch.setattr( plugins_initializer, @@ -529,7 +524,7 @@ async def test_sync_runs_identity_migration_before_automatic_install( monkeypatch.setattr(plugins_initializer, "execute_task", execute) assert await plugins_initializer.sync_plugins() is False - assert order == ["configure", "migrate", "identity", "sync"] + assert order == ["migrate", "identity", "sync"] @pytest.mark.asyncio diff --git a/tests/test_plugin_manager_governance.py b/tests/test_plugin_manager_governance.py index 4abb5e621..5b6fe9d2d 100644 --- a/tests/test_plugin_manager_governance.py +++ b/tests/test_plugin_manager_governance.py @@ -2,9 +2,6 @@ import ast from pathlib import Path -from unittest.mock import Mock - -import pytest from app.startup.initializers import plugins as plugins_initializer @@ -99,9 +96,9 @@ def test_startup_injects_runtime_factory_before_manager_materialization() -> Non for node in _parse(startup_path).body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) } - service_calls = sorted( + runtime_calls = sorted( (node.lineno, ast.unparse(node.func)) - for node in ast.walk(functions["configure_plugin_services"]) + for node in ast.walk(functions["configure_plugin_runtime_services"]) if isinstance(node, ast.Call) ) sync_calls = sorted( @@ -110,36 +107,22 @@ def test_startup_injects_runtime_factory_before_manager_materialization() -> Non if isinstance(node, ast.Call) ) - assert service_calls.index( - next(item for item in service_calls if item[1] == "configure_plugin_runtime_factory") - ) < service_calls.index( - next(item for item in service_calls if item[1] == "configure_plugin_runtime") - ) < service_calls.index( - next(item for item in service_calls if item[1] == "get_plugin_manager") - ) - assert sync_calls.index( - next(item for item in sync_calls if item[1] == "configure_plugin_services") - ) < sync_calls.index( - next(item for item in sync_calls if item[1] == "PluginManager") + assert runtime_calls.index( + next(item for item in runtime_calls if item[1] == "configure_plugin_runtime_factory") + ) < runtime_calls.index( + next(item for item in runtime_calls if item[1] == "configure_plugin_runtime") ) + assert not any(item[1] == "configure_plugin_services" for item in sync_calls) -def test_plugin_services_publish_application_runtime_before_lookup(monkeypatch) -> None: - """插件服务装配必须先发布 Application provider,再读取 Manager。""" +def test_plugin_runtime_services_publish_application_runtime(monkeypatch) -> None: + """模块对象图构造前必须先发布 Runtime 工厂和 Application provider。""" order: list[str] = [] - - class _LookupReached(RuntimeError): - """表示测试已执行到首次插件 Manager 读取。""" - - def factory(*_args, **_kwargs): - """返回不触发真实构造逻辑的测试替身。""" - return Mock() - - monkeypatch.setattr(plugins_initializer, "PluginMarketTransport", factory) - monkeypatch.setattr(plugins_initializer, "PluginMarketClient", factory) - monkeypatch.setattr(plugins_initializer, "PluginPackageSourceClient", factory) - monkeypatch.setattr(plugins_initializer, "PluginPackageManager", factory) - monkeypatch.setattr(plugins_initializer, "configure_plugin_catalog_factory", Mock()) + monkeypatch.setattr( + plugins_initializer, + "configure_plugin_catalog_factory", + lambda _factory: order.append("catalog"), + ) monkeypatch.setattr( plugins_initializer, "configure_plugin_runtime_factory", @@ -151,17 +134,9 @@ def test_plugin_services_publish_application_runtime_before_lookup(monkeypatch) lambda _provider: order.append("provider"), ) - def lookup_manager(): - """记录首次读取并终止无关的后续服务图构造。""" - order.append("lookup") - raise _LookupReached + plugins_initializer.configure_plugin_runtime_services() - monkeypatch.setattr(plugins_initializer, "get_plugin_manager", lookup_manager) - - with pytest.raises(_LookupReached): - plugins_initializer.configure_plugin_services() - - assert order == ["factory", "provider", "lookup"] + assert order == ["catalog", "factory", "provider"] def test_plugin_manager_does_not_reach_raw_system_adapters() -> None: diff --git a/tests/test_plugin_monitor_lifecycle.py b/tests/test_plugin_monitor_lifecycle.py index caaf0667e..fc8684a91 100644 --- a/tests/test_plugin_monitor_lifecycle.py +++ b/tests/test_plugin_monitor_lifecycle.py @@ -89,11 +89,6 @@ def test_init_plugins_starts_monitor_after_runtime_and_routes(monkeypatch) -> No manager.start_monitor.side_effect = lambda **_kwargs: order.append("monitor") manager.get_local_repo_plugins.return_value = [] manager.get_plugin_source_id.side_effect = lambda plugin_id: plugin_id - monkeypatch.setattr( - plugins_initializer, - "configure_plugin_services", - lambda: order.append("services"), - ) monkeypatch.setattr(plugins_initializer, "PluginManager", lambda: manager) monkeypatch.setattr( plugins_initializer, @@ -104,7 +99,6 @@ def test_init_plugins_starts_monitor_after_runtime_and_routes(monkeypatch) -> No plugins_initializer.init_plugins() assert order == [ - "services", "reopen", "plugin:ReadyPlugin", "routes",