mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
refactor(startup): isolate chain composition
This commit is contained in:
141
app/startup/composition/chain.py
Normal file
141
app/startup/composition/chain.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Chain 无参兼容入口与跨层依赖的宿主组合根。"""
|
||||
|
||||
from functools import partial
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.application.chain.context import (
|
||||
ChainRuntimeContext,
|
||||
configure_chain_runtime_context_provider,
|
||||
)
|
||||
from app.application.configuration import ChainRuntimeConfig
|
||||
from app.application.image import configure_wallpaper_providers
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.application.transfer.execution import TransferExecutionRepository
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.db.adapters.chain import TransactionalChainDurableEventWriter
|
||||
from app.db.adapters.download import TransactionalDownloadFailureRepository
|
||||
from app.db.adapters.history.download import TransactionalDownloadHistoryRepository
|
||||
from app.db.adapters.history.transfer import TransactionalTransferHistoryRepository
|
||||
from app.db.adapters.mediaserver import TransactionalMediaServerRepository
|
||||
from app.db.adapters.site import TransactionalSiteRepository
|
||||
from app.db.adapters.subscription import TransactionalSubscriptionRepository
|
||||
from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.session import SessionFactory
|
||||
from app.runtime.cache import AsyncFileCache, FileCache
|
||||
from app.runtime.events import EventManager
|
||||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.stop import runtime_stop_state
|
||||
from app.startup.composition.database import build_transactional_user_repository
|
||||
from app.startup.composition.subscription import (
|
||||
delete_subscribe_scope,
|
||||
rule_group_mutation_scope,
|
||||
site_reference_mutation_scope,
|
||||
subscription_completion_scope,
|
||||
subscription_mutation_scope,
|
||||
sync_delete_subscribe_scope,
|
||||
sync_subscription_mutation_scope,
|
||||
)
|
||||
|
||||
_event_manager_factory: Callable[[], EventManager] = EventManager
|
||||
|
||||
|
||||
def execute_legacy_transfer_command(**kwargs: Any) -> Any:
|
||||
"""把旧 Chain ABI 延迟转入唯一 TransferChain durable command。"""
|
||||
from app.chain.transfer.facade import TransferChain
|
||||
|
||||
return TransferChain().execute_legacy_transfer_command(**kwargs)
|
||||
|
||||
|
||||
def build_chain_runtime_context(
|
||||
*,
|
||||
message_helper: MessageHelper,
|
||||
message_queue: MessageQueueManager,
|
||||
system_config: SystemConfigOper,
|
||||
site: TransactionalSiteRepository,
|
||||
subscription: TransactionalSubscriptionRepository,
|
||||
download_history: TransactionalDownloadHistoryRepository,
|
||||
transfer_history: TransactionalTransferHistoryRepository,
|
||||
transfer_execution: TransferExecutionRepository,
|
||||
configuration: Callable[[], ChainRuntimeConfig],
|
||||
) -> ChainRuntimeContext:
|
||||
"""创建 Chain 无参兼容入口共享的运行时对象与数据端口。"""
|
||||
return ChainRuntimeContext(
|
||||
module_manager=ModuleManager(),
|
||||
plugin_manager=PluginManager(),
|
||||
event_manager=_event_manager_factory(),
|
||||
message_oper=MessageOper(),
|
||||
message_helper=message_helper,
|
||||
file_cache=FileCache(),
|
||||
async_file_cache=AsyncFileCache(),
|
||||
message_queue=message_queue,
|
||||
module_dispatcher_factory=ModuleInvocationDispatcher,
|
||||
site_repository=site,
|
||||
subscription_repository=subscription,
|
||||
subscription_mutation_scope=subscription_mutation_scope,
|
||||
sync_subscription_mutation_scope=sync_subscription_mutation_scope,
|
||||
subscription_delete_scope=delete_subscribe_scope,
|
||||
sync_subscription_delete_scope=sync_delete_subscribe_scope,
|
||||
subscription_completion_scope=subscription_completion_scope,
|
||||
rule_group_mutation_scope=partial(
|
||||
rule_group_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
site_reference_mutation_scope=partial(
|
||||
site_reference_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
download_history_repository=download_history,
|
||||
transfer_history_repository=transfer_history,
|
||||
transfer_admission_repository=TransactionalTransferAdmissionRepository(SessionFactory),
|
||||
transfer_execution_repository=transfer_execution,
|
||||
media_server_repository=TransactionalMediaServerRepository(SessionFactory),
|
||||
download_failure_repository=TransactionalDownloadFailureRepository(SessionFactory),
|
||||
user_repository=build_transactional_user_repository(),
|
||||
legacy_transfer_command=execute_legacy_transfer_command,
|
||||
configuration=configuration(),
|
||||
durable_event_writer=TransactionalChainDurableEventWriter(SessionFactory),
|
||||
stop_state=runtime_stop_state,
|
||||
)
|
||||
|
||||
|
||||
def configure_wallpaper_services() -> None:
|
||||
"""把需要 Chain 编排的壁纸来源注入图片服务。"""
|
||||
configure_wallpaper_providers(
|
||||
tmdb_wallpaper=lambda: TmdbChain().get_random_wallpager(),
|
||||
tmdb_wallpapers=lambda count: TmdbChain().get_trending_wallpapers(count),
|
||||
mediaserver_wallpaper=lambda: MediaServerChain().get_latest_wallpaper(),
|
||||
mediaserver_wallpapers=lambda count: MediaServerChain().get_latest_wallpapers(count=count),
|
||||
)
|
||||
|
||||
|
||||
def configure_chain_runtime_context(
|
||||
*,
|
||||
message_helper: MessageHelper,
|
||||
message_queue: MessageQueueManager,
|
||||
system_config: SystemConfigOper,
|
||||
site: TransactionalSiteRepository,
|
||||
subscription: TransactionalSubscriptionRepository,
|
||||
download_history: TransactionalDownloadHistoryRepository,
|
||||
transfer_history: TransactionalTransferHistoryRepository,
|
||||
transfer_execution: TransferExecutionRepository,
|
||||
configuration: Callable[[], ChainRuntimeConfig],
|
||||
) -> None:
|
||||
"""登记按需构造的 Chain 上下文,保持无参 Chain 的插件兼容合同。"""
|
||||
configure_chain_runtime_context_provider(
|
||||
lambda: build_chain_runtime_context(
|
||||
message_helper=message_helper,
|
||||
message_queue=message_queue,
|
||||
system_config=system_config,
|
||||
site=site,
|
||||
subscription=subscription,
|
||||
download_history=download_history,
|
||||
transfer_history=transfer_history,
|
||||
transfer_execution=transfer_execution,
|
||||
configuration=configuration,
|
||||
)
|
||||
)
|
||||
@@ -3,11 +3,9 @@ import inspect
|
||||
import sys
|
||||
from collections.abc import Awaitable
|
||||
from functools import partial
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Callable, cast
|
||||
|
||||
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
|
||||
# SitesHelper涉及资源包拉取,提前引入并容错提示
|
||||
try:
|
||||
@@ -25,17 +23,12 @@ from app.adapters.system.resource import (
|
||||
ResourceHelper,
|
||||
configure_resource_version_provider,
|
||||
)
|
||||
from app.application.chain.context import (
|
||||
ChainRuntimeContext,
|
||||
configure_chain_runtime_context_provider,
|
||||
)
|
||||
from app.application.configuration import (
|
||||
TransferRetryConfig,
|
||||
configure_transfer_retry_config,
|
||||
get_configured_system_config,
|
||||
)
|
||||
from app.application.history import configure_transfer_history_repository
|
||||
from app.application.image import configure_wallpaper_providers
|
||||
from app.application.messaging.agent import (
|
||||
dispatch_web_agent_message_event,
|
||||
shutdown_web_agent_background_tasks,
|
||||
@@ -57,8 +50,6 @@ from app.application.site.health import SiteHealthService, configure_site_health
|
||||
from app.application.site.query import SiteQueryService, configure_site_query_service
|
||||
from app.application.workflow import configure_workflow_execution
|
||||
from app.command import CommandChain
|
||||
from app.db.adapters.chain import TransactionalChainDurableEventWriter
|
||||
from app.db.adapters.download import TransactionalDownloadFailureRepository
|
||||
from app.db.adapters.history.download import (
|
||||
SessionDownloadHistoryRepository,
|
||||
TransactionalDownloadHistoryRepository,
|
||||
@@ -67,7 +58,6 @@ from app.db.adapters.history.transfer import (
|
||||
SessionTransferHistoryRepository,
|
||||
TransactionalTransferHistoryRepository,
|
||||
)
|
||||
from app.db.adapters.mediaserver import TransactionalMediaServerRepository
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxDispatchStore,
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
@@ -79,7 +69,6 @@ from app.db.adapters.subscription import (
|
||||
TransactionalSubscriptionHistoryRepository,
|
||||
TransactionalSubscriptionRepository,
|
||||
)
|
||||
from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
|
||||
from app.db.adapters.transfer.execution import (
|
||||
TransactionalTransferExecutionRepository,
|
||||
)
|
||||
@@ -88,7 +77,6 @@ from app.db.adapters.workflow import (
|
||||
)
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.session import (
|
||||
SessionFactory,
|
||||
@@ -101,13 +89,10 @@ from app.db.uow import (
|
||||
SqlAlchemyAsyncUnitOfWork,
|
||||
SqlAlchemyUnitOfWork,
|
||||
)
|
||||
from app.runtime.cache import AsyncFileCache, FileCache
|
||||
from app.runtime.config import settings as legacy_settings
|
||||
from app.runtime.events import EventHandlerBinding, EventManager
|
||||
from app.runtime.execution import run_in_threadpool_to_completion
|
||||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.extensions.service_config import (
|
||||
ServiceConfigHelper,
|
||||
configure_service_config_reader,
|
||||
@@ -117,7 +102,6 @@ from app.runtime.settings import (
|
||||
get_runtime_setting,
|
||||
)
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.stop import runtime_stop_state
|
||||
from app.runtime.tasks import get_task_registry
|
||||
from app.runtime.thread import ThreadHelper
|
||||
from app.schemas.message import Message, MessageType
|
||||
@@ -126,8 +110,11 @@ from app.startup.composition.agent import (
|
||||
compose_agent,
|
||||
publish_agent_services,
|
||||
)
|
||||
from app.startup.composition.chain import (
|
||||
configure_chain_runtime_context,
|
||||
configure_wallpaper_services,
|
||||
)
|
||||
from app.startup.composition.configuration import (
|
||||
build_chain_runtime_config,
|
||||
compose_configuration,
|
||||
publish_configuration,
|
||||
reset_configuration,
|
||||
@@ -142,7 +129,6 @@ from app.startup.composition.context import (
|
||||
WorkflowRuntime,
|
||||
)
|
||||
from app.startup.composition.database import (
|
||||
build_transactional_user_repository,
|
||||
compose_database_services,
|
||||
configure_database,
|
||||
database_runtime_active,
|
||||
@@ -161,13 +147,8 @@ from app.startup.composition.server import configure_server_services
|
||||
from app.startup.composition.subscription import (
|
||||
async_rule_group_mutation_scope,
|
||||
build_subscription_batch_writer,
|
||||
delete_subscribe_scope,
|
||||
rule_group_mutation_scope,
|
||||
site_reference_mutation_scope,
|
||||
subscription_completion_scope,
|
||||
subscription_mutation_scope,
|
||||
sync_delete_subscribe_scope,
|
||||
sync_subscription_mutation_scope,
|
||||
)
|
||||
from app.startup.initializers.agent import configure_agent_data_context, init_agent
|
||||
from app.startup.initializers.resources import (
|
||||
@@ -177,63 +158,6 @@ from app.startup.initializers.resources import (
|
||||
from app.startup.initializers.scheduler import configure_scheduler_agent_tasks
|
||||
|
||||
|
||||
def _execute_legacy_transfer_command(**kwargs: Any) -> Any:
|
||||
"""把旧 Chain ABI 延迟转入唯一 TransferChain durable command。"""
|
||||
from app.chain.transfer.facade import TransferChain
|
||||
|
||||
return TransferChain().execute_legacy_transfer_command(**kwargs)
|
||||
|
||||
|
||||
def _build_chain_runtime_context(
|
||||
*,
|
||||
message_helper: MessageHelper,
|
||||
message_queue: MessageQueueManager,
|
||||
system_config: SystemConfigOper,
|
||||
site: TransactionalSiteRepository,
|
||||
subscription: TransactionalSubscriptionRepository,
|
||||
download_history: TransactionalDownloadHistoryRepository,
|
||||
transfer_history: TransactionalTransferHistoryRepository,
|
||||
) -> ChainRuntimeContext:
|
||||
"""在启动组合根创建 Chain 所需的运行时对象和数据端口。"""
|
||||
return ChainRuntimeContext(
|
||||
module_manager=ModuleManager(),
|
||||
plugin_manager=PluginManager(),
|
||||
event_manager=EventManager(),
|
||||
message_oper=MessageOper(),
|
||||
message_helper=message_helper,
|
||||
file_cache=FileCache(),
|
||||
async_file_cache=AsyncFileCache(),
|
||||
message_queue=message_queue,
|
||||
module_dispatcher_factory=ModuleInvocationDispatcher,
|
||||
site_repository=site,
|
||||
subscription_repository=subscription,
|
||||
subscription_mutation_scope=subscription_mutation_scope,
|
||||
sync_subscription_mutation_scope=sync_subscription_mutation_scope,
|
||||
subscription_delete_scope=delete_subscribe_scope,
|
||||
sync_subscription_delete_scope=sync_delete_subscribe_scope,
|
||||
subscription_completion_scope=subscription_completion_scope,
|
||||
rule_group_mutation_scope=partial(
|
||||
rule_group_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
site_reference_mutation_scope=partial(
|
||||
site_reference_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
download_history_repository=download_history,
|
||||
transfer_history_repository=transfer_history,
|
||||
transfer_admission_repository=TransactionalTransferAdmissionRepository(SessionFactory),
|
||||
transfer_execution_repository=TransactionalTransferExecutionRepository(SessionFactory),
|
||||
media_server_repository=TransactionalMediaServerRepository(SessionFactory),
|
||||
download_failure_repository=TransactionalDownloadFailureRepository(SessionFactory),
|
||||
user_repository=build_transactional_user_repository(),
|
||||
legacy_transfer_command=_execute_legacy_transfer_command,
|
||||
configuration=build_chain_runtime_config(legacy_settings),
|
||||
durable_event_writer=TransactionalChainDurableEventWriter(SessionFactory),
|
||||
stop_state=runtime_stop_state,
|
||||
)
|
||||
|
||||
|
||||
def configure_runtime_data_providers() -> None:
|
||||
"""在启动组合层装配模块和配置目录的运行时读取能力。"""
|
||||
configure_service_config_reader(lambda key: get_configured_system_config().get(key))
|
||||
@@ -244,16 +168,6 @@ def configure_runtime_data_providers() -> None:
|
||||
)
|
||||
|
||||
|
||||
def configure_wallpaper_services() -> None:
|
||||
"""把需要 Chain 编排的壁纸来源注入图片服务。"""
|
||||
configure_wallpaper_providers(
|
||||
tmdb_wallpaper=lambda: TmdbChain().get_random_wallpager(),
|
||||
tmdb_wallpapers=lambda count: TmdbChain().get_trending_wallpapers(count),
|
||||
mediaserver_wallpaper=lambda: MediaServerChain().get_latest_wallpaper(),
|
||||
mediaserver_wallpapers=lambda count: MediaServerChain().get_latest_wallpapers(count=count),
|
||||
)
|
||||
|
||||
|
||||
def notify_event_error(title: str, message: str) -> None:
|
||||
"""将事件总线错误转发到系统消息通道。"""
|
||||
MessageHelper().put(
|
||||
@@ -660,16 +574,16 @@ async def _initialize_modules() -> HostRuntime:
|
||||
# 应用服务不反向依赖 Chain,由启动组合层注入壁纸来源。
|
||||
configure_wallpaper_services()
|
||||
# Chain 无参兼容入口由组合根明确提供依赖上下文;测试和新代码可直接注入替代上下文。
|
||||
configure_chain_runtime_context_provider(
|
||||
lambda: _build_chain_runtime_context(
|
||||
message_helper=message_helper,
|
||||
message_queue=message_queue,
|
||||
system_config=system_config,
|
||||
site=site_repository,
|
||||
subscription=subscription_repository,
|
||||
download_history=download_history_repository,
|
||||
transfer_history=transfer_history_repository,
|
||||
)
|
||||
configure_chain_runtime_context(
|
||||
message_helper=message_helper,
|
||||
message_queue=message_queue,
|
||||
system_config=system_config,
|
||||
site=site_repository,
|
||||
subscription=subscription_repository,
|
||||
download_history=download_history_repository,
|
||||
transfer_history=transfer_history_repository,
|
||||
transfer_execution=transfer_execution_repository,
|
||||
configuration=configuration.runtime.chain,
|
||||
)
|
||||
# 认证访问层不反向依赖数据库实现,由启动组合层注入载荷提供器。
|
||||
configure_security_access()
|
||||
|
||||
@@ -62,7 +62,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
| `chain` | 多入口复用的用例编排与 Module 动态分发已稳定,包根 SCC 已清零 | God object、私有长方法、无参构造和资源物化过重 |
|
||||
| `db/oper` | Model/Oper 已不自建 Session、不自行提交 | 无 Session Facade 仍被宿主组合根注入,业务操作可能拆成多个事务 |
|
||||
| `db/adapters` | 已有显式 Session/UoW 的参考切片 | Port 返回类型不够稳定;Outbox 的 stage 与自提交 dispatcher store 混在一个类型 |
|
||||
| `startup` | 已是 HostRuntime 和生命周期组合根 | `initializers/modules.py` 高扇出,仍有导入期 provider 注册和具体对象目录装配 |
|
||||
| `startup` | 已是 HostRuntime 和生命周期组合根;Chain 兼容上下文由 `composition/chain.py` 独立装配 | `initializers/modules.py` 仍有其他领域高扇出和具体对象目录装配 |
|
||||
| API/Command/Scheduler | 多数入口已转向 Chain/Application;Scheduler 单体已拆为同名职责包并由 startup 注入业务 callable | Agent/System/Plugin 等入口仍承担较多业务与 I/O 编排 |
|
||||
| `sdk`/`compat` | 精确映射、稳定插件 ABI 和宿主 canonical 路径已落地 | SDK 仍暴露部分可变全局对象/具体 Manager,只能渐进收窄,不能直接删除 |
|
||||
|
||||
@@ -70,7 +70,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 970 / 8,279 | `dependency-baseline.json` 当前快照 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 971 / 8,306 | `dependency-baseline.json` 当前快照 |
|
||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
@@ -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,335 / 593 文件 | strict frontier 当前覆盖 41 个文件,Domain projection 与既有 owner 拆分的受益调用方低水位已固化 |
|
||||
| 全量 mypy 历史债务 | 10,335 / 593 文件 | 新 Chain composition owner 零类型债,`modules.py` 低水位下降 1;strict frontier 当前覆盖 41 个文件 |
|
||||
| Ruff 历史诊断 | 648 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率低水位 | Application 81.00%,Domain 80.67% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
@@ -672,10 +672,11 @@ Pylint 10/10、Ruff、mypy/复杂度 ratchet、宿主与最新官方插件基线
|
||||
插件持久化迁入既有 `startup/composition/{configuration,database}.py`;initializer 只调用组合 API。
|
||||
数据库 runtime 拒绝重入,Worker 成功后才发布事务入口;成功关闭与启动失败会对称撤销本批 provider、
|
||||
释放数据库引擎,并在模块 owner 收敛后清除 `app.state.host_runtime`。
|
||||
- [x] L4.2 已将 network、security 与 agent 装配迁入单词 owner;Agent composition 在 Worker 启动后
|
||||
读取容量,并让 `HostRuntime`、兼容 provider、工具管理器与 Scheduler 共享同一数据和任务对象。
|
||||
- [ ] 继续将 Chain、server、outbox 与 HostRuntime 构造按领域移到
|
||||
`startup/composition/*`,最终让 initializer 只负责顺序、注册和是否重启的决策。
|
||||
- [x] L4.2 已将 network、security、agent、server、outbox 与 Chain 装配迁入单词型
|
||||
`startup/composition/*` owner;Agent composition 在 Worker 启动后读取容量并共享同一数据和任务对象,
|
||||
Chain 通过 provider 保持插件无参构造兼容,旧 Transfer command 只在真实调用时延迟导入。
|
||||
- [ ] 继续将 HostRuntime 剩余构造移到 `startup/composition/runtime.py`,最终让 initializer
|
||||
只负责顺序、注册和是否重启的决策。
|
||||
- [ ] 保留生命周期 manifest 和顺序快照;高扇出在组合根是允许的,但业务实现不能继续沉积其中。
|
||||
|
||||
### ARCH-204 收敛 sync/async 重复与测试存量
|
||||
|
||||
@@ -285,7 +285,9 @@ sequenceDiagram
|
||||
`HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一
|
||||
复用 `startup/composition/configuration.py` 的映射、快照加载与发布。`startup/composition/database.py`
|
||||
持有数据库 worker、兼容事务 runner、查询/插件持久化和旧 `ApiDataPorts` 的具体装配;initializer
|
||||
只保留顺序调用。`startup/composition/security.py` 统一装配认证、用户查询、PassKey 与 Web 访问
|
||||
只保留顺序调用。`startup/composition/chain.py` 集中构造无参 Chain 兼容上下文及其持久化、事件、
|
||||
Outbox、消息和模块分发依赖,并延迟绑定旧 Transfer command 与壁纸 Chain,避免组合根导入环。
|
||||
`startup/composition/security.py` 统一装配认证、用户查询、PassKey 与 Web 访问
|
||||
provider,并将同一 `AuthenticationRuntime` 交给 HostRuntime;`startup/composition/network.py` 统一
|
||||
装配网络应用端口;`startup/composition/agent.py` 构造并发布唯一的 Agent 数据、会话和自主任务对象,
|
||||
`HostRuntime`、工具管理器与 Scheduler 复用同一对象身份。`ApiDataPorts` 仅保留旧导入 ABI,
|
||||
@@ -747,8 +749,8 @@ flowchart LR
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 970 |
|
||||
| 内部导入边 | 8,279 |
|
||||
| Python 模块 | 971 |
|
||||
| 内部导入边 | 8,306 |
|
||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||
| Direct egress | 53(债务已清零,53 条精确 containment) |
|
||||
|
||||
@@ -141,6 +141,10 @@ runtime/settings construction and publication. `startup/composition/database.py`
|
||||
owns the process database worker, compatibility transaction runner, query services,
|
||||
plugin persistence and legacy API data-port wiring. Initializers call these owners in
|
||||
startup order and must not recreate their concrete construction.
|
||||
`startup/composition/chain.py` owns the no-argument Chain compatibility context,
|
||||
typed persistence/dispatch object graph, lazy legacy Transfer command binding and
|
||||
wallpaper-provider publication. `initializers/modules.py` only passes the already
|
||||
constructed repositories in startup order and must not recreate those dependencies.
|
||||
`startup/composition/security.py` owns authentication, user lookup, PassKey and Web
|
||||
access provider assembly; `initializers/modules.py` only invokes that owner and places
|
||||
its returned `AuthenticationRuntime` into the host runtime.
|
||||
@@ -151,7 +155,7 @@ startup order and must not recreate their concrete construction.
|
||||
`startup/composition/outbox.py`
|
||||
owns durable handler validation, lazy event/notification replay binding and the
|
||||
short-transaction dispatcher factory. Initializers call this owner in
|
||||
startup order and must not recreate their concrete construction. Database runtime
|
||||
startup order and must not recreate its concrete construction.
|
||||
`startup/composition/server.py` owns MoviePilot Server report/share service
|
||||
construction and registration, including lazy local-data readers and external
|
||||
transport callbacks; `initializers/modules.py` retains only its startup-order call
|
||||
@@ -160,8 +164,8 @@ and the later explicit network warmup. Database runtime
|
||||
chat persistence and autonomous-task repository/execution service set. It may accept
|
||||
the Agent initializer's data-context registrar as a callback, but must not import
|
||||
`startup.initializers`; tool-manager, scheduler and Agent activation order remains in
|
||||
the initializer. Database runtime
|
||||
ownership rejects overlapping lifespans; after a successful worker shutdown, the
|
||||
the initializer. Database runtime ownership rejects overlapping lifespans; after a
|
||||
successful worker shutdown, the
|
||||
configuration/database composition owners must revoke every provider they published
|
||||
before releasing database connections. Other domain providers remain owned by their
|
||||
matching composition batch and must satisfy the same symmetric lifecycle contract
|
||||
|
||||
@@ -47,6 +47,7 @@ RUNTIME_FACADE_POLICIES = (
|
||||
exact_consumers=frozenset(
|
||||
{
|
||||
"app.sdk.plugins",
|
||||
"app.startup.composition.chain",
|
||||
"app.startup.initializers.modules",
|
||||
}
|
||||
),
|
||||
@@ -57,7 +58,7 @@ RUNTIME_FACADE_POLICIES = (
|
||||
exact_consumers=frozenset(
|
||||
{
|
||||
"app.sdk.plugins",
|
||||
"app.startup.initializers.modules",
|
||||
"app.startup.composition.chain",
|
||||
"app.startup.initializers.plugins",
|
||||
}
|
||||
),
|
||||
|
||||
@@ -1089,8 +1089,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 8279,
|
||||
"edge_sha256": "fe7bb7d7e064c8774e3c50b2db083386ff0d5bcb9476de2de3346de39478dd57",
|
||||
"edge_count": 8306,
|
||||
"edge_sha256": "2224fccf31c611f1ba2cf9e8afbdd33b9277ac2846101c85d57e0801b2a7b542",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -8651,6 +8651,49 @@
|
||||
"app.startup.composition.agent -> app.startup.composition.context",
|
||||
"app.startup.composition.agent -> app.startup.composition.database",
|
||||
"app.startup.composition.agent -> app.startup.composition.subscription",
|
||||
"app.startup.composition.chain -> app.application",
|
||||
"app.startup.composition.chain -> app.application.chain",
|
||||
"app.startup.composition.chain -> app.application.chain.context",
|
||||
"app.startup.composition.chain -> app.application.configuration",
|
||||
"app.startup.composition.chain -> app.application.image",
|
||||
"app.startup.composition.chain -> app.application.messaging",
|
||||
"app.startup.composition.chain -> app.application.messaging.message",
|
||||
"app.startup.composition.chain -> app.application.transfer",
|
||||
"app.startup.composition.chain -> app.application.transfer.execution",
|
||||
"app.startup.composition.chain -> app.chain",
|
||||
"app.startup.composition.chain -> app.chain.mediaserver",
|
||||
"app.startup.composition.chain -> app.chain.tmdb",
|
||||
"app.startup.composition.chain -> app.chain.transfer",
|
||||
"app.startup.composition.chain -> app.chain.transfer.facade",
|
||||
"app.startup.composition.chain -> app.db",
|
||||
"app.startup.composition.chain -> app.db.adapters",
|
||||
"app.startup.composition.chain -> app.db.adapters.chain",
|
||||
"app.startup.composition.chain -> app.db.adapters.download",
|
||||
"app.startup.composition.chain -> app.db.adapters.history",
|
||||
"app.startup.composition.chain -> app.db.adapters.history.download",
|
||||
"app.startup.composition.chain -> app.db.adapters.history.transfer",
|
||||
"app.startup.composition.chain -> app.db.adapters.mediaserver",
|
||||
"app.startup.composition.chain -> app.db.adapters.site",
|
||||
"app.startup.composition.chain -> app.db.adapters.subscription",
|
||||
"app.startup.composition.chain -> app.db.adapters.transfer",
|
||||
"app.startup.composition.chain -> app.db.adapters.transfer.admission",
|
||||
"app.startup.composition.chain -> app.db.oper",
|
||||
"app.startup.composition.chain -> app.db.oper.message",
|
||||
"app.startup.composition.chain -> app.db.oper.systemconfig",
|
||||
"app.startup.composition.chain -> app.db.session",
|
||||
"app.startup.composition.chain -> app.runtime",
|
||||
"app.startup.composition.chain -> app.runtime.cache",
|
||||
"app.startup.composition.chain -> app.runtime.events",
|
||||
"app.startup.composition.chain -> app.runtime.extensions",
|
||||
"app.startup.composition.chain -> app.runtime.extensions.module",
|
||||
"app.startup.composition.chain -> app.runtime.extensions.module.dispatcher",
|
||||
"app.startup.composition.chain -> app.runtime.extensions.module_manager",
|
||||
"app.startup.composition.chain -> app.runtime.extensions.plugin_manager",
|
||||
"app.startup.composition.chain -> app.runtime.stop",
|
||||
"app.startup.composition.chain -> app.startup",
|
||||
"app.startup.composition.chain -> app.startup.composition",
|
||||
"app.startup.composition.chain -> app.startup.composition.database",
|
||||
"app.startup.composition.chain -> app.startup.composition.subscription",
|
||||
"app.startup.composition.configuration -> app.application",
|
||||
"app.startup.composition.configuration -> app.application.configuration",
|
||||
"app.startup.composition.configuration -> app.application.database",
|
||||
@@ -8921,11 +8964,8 @@
|
||||
"app.startup.initializers.modules -> app.agent.tools",
|
||||
"app.startup.initializers.modules -> app.agent.tools.manager",
|
||||
"app.startup.initializers.modules -> app.application",
|
||||
"app.startup.initializers.modules -> app.application.chain",
|
||||
"app.startup.initializers.modules -> app.application.chain.context",
|
||||
"app.startup.initializers.modules -> app.application.configuration",
|
||||
"app.startup.initializers.modules -> app.application.history",
|
||||
"app.startup.initializers.modules -> app.application.image",
|
||||
"app.startup.initializers.modules -> app.application.messaging",
|
||||
"app.startup.initializers.modules -> app.application.messaging.agent",
|
||||
"app.startup.initializers.modules -> app.application.messaging.chat",
|
||||
@@ -8941,54 +8981,40 @@
|
||||
"app.startup.initializers.modules -> app.application.workflow",
|
||||
"app.startup.initializers.modules -> app.chain",
|
||||
"app.startup.initializers.modules -> app.chain.download",
|
||||
"app.startup.initializers.modules -> app.chain.mediaserver",
|
||||
"app.startup.initializers.modules -> app.chain.scraping",
|
||||
"app.startup.initializers.modules -> app.chain.search",
|
||||
"app.startup.initializers.modules -> app.chain.site",
|
||||
"app.startup.initializers.modules -> app.chain.subscribe",
|
||||
"app.startup.initializers.modules -> app.chain.subscribe.facade",
|
||||
"app.startup.initializers.modules -> app.chain.tmdb",
|
||||
"app.startup.initializers.modules -> app.chain.transfer",
|
||||
"app.startup.initializers.modules -> app.chain.transfer.facade",
|
||||
"app.startup.initializers.modules -> app.chain.workflow",
|
||||
"app.startup.initializers.modules -> app.command",
|
||||
"app.startup.initializers.modules -> app.db",
|
||||
"app.startup.initializers.modules -> app.db.adapters",
|
||||
"app.startup.initializers.modules -> app.db.adapters.chain",
|
||||
"app.startup.initializers.modules -> app.db.adapters.download",
|
||||
"app.startup.initializers.modules -> app.db.adapters.history",
|
||||
"app.startup.initializers.modules -> app.db.adapters.history.download",
|
||||
"app.startup.initializers.modules -> app.db.adapters.history.transfer",
|
||||
"app.startup.initializers.modules -> app.db.adapters.mediaserver",
|
||||
"app.startup.initializers.modules -> app.db.adapters.outbox",
|
||||
"app.startup.initializers.modules -> app.db.adapters.site",
|
||||
"app.startup.initializers.modules -> app.db.adapters.subscription",
|
||||
"app.startup.initializers.modules -> app.db.adapters.transfer",
|
||||
"app.startup.initializers.modules -> app.db.adapters.transfer.admission",
|
||||
"app.startup.initializers.modules -> app.db.adapters.transfer.execution",
|
||||
"app.startup.initializers.modules -> app.db.adapters.workflow",
|
||||
"app.startup.initializers.modules -> app.db.oper",
|
||||
"app.startup.initializers.modules -> app.db.oper.mediaserver",
|
||||
"app.startup.initializers.modules -> app.db.oper.message",
|
||||
"app.startup.initializers.modules -> app.db.oper.systemconfig",
|
||||
"app.startup.initializers.modules -> app.db.oper.workflow",
|
||||
"app.startup.initializers.modules -> app.db.session",
|
||||
"app.startup.initializers.modules -> app.db.uow",
|
||||
"app.startup.initializers.modules -> app.runtime",
|
||||
"app.startup.initializers.modules -> app.runtime.cache",
|
||||
"app.startup.initializers.modules -> app.runtime.config",
|
||||
"app.startup.initializers.modules -> app.runtime.events",
|
||||
"app.startup.initializers.modules -> app.runtime.execution",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions.module",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions.module.dispatcher",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions.module_manager",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions.plugin_manager",
|
||||
"app.startup.initializers.modules -> app.runtime.extensions.service_config",
|
||||
"app.startup.initializers.modules -> app.runtime.log",
|
||||
"app.startup.initializers.modules -> app.runtime.settings",
|
||||
"app.startup.initializers.modules -> app.runtime.state",
|
||||
"app.startup.initializers.modules -> app.runtime.stop",
|
||||
"app.startup.initializers.modules -> app.runtime.tasks",
|
||||
"app.startup.initializers.modules -> app.runtime.thread",
|
||||
"app.startup.initializers.modules -> app.scheduler",
|
||||
@@ -8999,6 +9025,7 @@
|
||||
"app.startup.initializers.modules -> app.startup",
|
||||
"app.startup.initializers.modules -> app.startup.composition",
|
||||
"app.startup.initializers.modules -> app.startup.composition.agent",
|
||||
"app.startup.initializers.modules -> app.startup.composition.chain",
|
||||
"app.startup.initializers.modules -> app.startup.composition.configuration",
|
||||
"app.startup.initializers.modules -> app.startup.composition.context",
|
||||
"app.startup.initializers.modules -> app.startup.composition.database",
|
||||
@@ -9372,7 +9399,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 970,
|
||||
"module_count": 971,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -10296,6 +10323,7 @@
|
||||
"app.startup",
|
||||
"app.startup.composition",
|
||||
"app.startup.composition.agent",
|
||||
"app.startup.composition.chain",
|
||||
"app.startup.composition.configuration",
|
||||
"app.startup.composition.context",
|
||||
"app.startup.composition.database",
|
||||
|
||||
@@ -3286,7 +3286,7 @@
|
||||
"attr-defined": 2,
|
||||
"misc": 1,
|
||||
"no-any-return": 2,
|
||||
"no-untyped-call": 16,
|
||||
"no-untyped-call": 15,
|
||||
"no-untyped-def": 6,
|
||||
"return-value": 2
|
||||
},
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"app/adapters/external/server.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/adapters/external/wechat_crypt.py": {
|
||||
"I001": 1
|
||||
},
|
||||
@@ -708,9 +705,6 @@
|
||||
"app/runtime/extensions/plugin/storage.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/extensions/service_config.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"app/runtime/gc.py": {
|
||||
"I001": 1
|
||||
},
|
||||
|
||||
@@ -566,7 +566,7 @@ def test_chain_runtime_context_owns_typed_repository_instances():
|
||||
)
|
||||
mediaserver_source = (APP_ROOT / "chain" / "mediaserver.py").read_text(encoding="utf-8")
|
||||
application_source = (APP_ROOT / "application" / "mediaserver.py").read_text(encoding="utf-8")
|
||||
startup_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
startup_source = (APP_ROOT / "startup" / "composition" / "chain.py").read_text(encoding="utf-8")
|
||||
assert "DownloadFailure = Any" not in download_source
|
||||
assert "dboper" not in mediaserver_source
|
||||
assert "async def async_get_item_id(" in application_source
|
||||
@@ -578,7 +578,7 @@ def test_startup_composes_typed_chain_and_agent_data_contexts():
|
||||
context_specs = {
|
||||
"ChainRuntimeContext": (
|
||||
APP_ROOT / "application" / "chain" / "context.py",
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py",
|
||||
APP_ROOT / "startup" / "composition" / "chain.py",
|
||||
{
|
||||
"site_repository",
|
||||
"subscription_repository",
|
||||
@@ -919,6 +919,74 @@ def test_modules_initializer_delegates_agent_composition():
|
||||
assert "startup.initializers" not in agent_source
|
||||
|
||||
|
||||
def test_modules_initializer_delegates_chain_composition():
|
||||
"""Chain 上下文、壁纸与旧整理命令只由单词型 composition owner 装配。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
composition_path = APP_ROOT / "startup" / "composition" / "chain.py"
|
||||
composition_source = composition_path.read_text(encoding="utf-8")
|
||||
composition_tree = ast.parse(composition_source, filename=str(composition_path))
|
||||
package_tree = ast.parse(
|
||||
(APP_ROOT / "startup" / "composition" / "__init__.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
architecture_source = (
|
||||
PROJECT_ROOT / "docs" / "rules" / "05-architecture.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert composition_path.is_file()
|
||||
assert not list(composition_path.parent.glob("chain_*.py"))
|
||||
assert "from app.startup.composition.chain import (" in initializer_source
|
||||
assert initializer_source.index("configure_wallpaper_services()") < (
|
||||
initializer_source.index("configure_chain_runtime_context(")
|
||||
)
|
||||
for retired_name in (
|
||||
"_execute_legacy_transfer_command",
|
||||
"_build_chain_runtime_context",
|
||||
):
|
||||
assert retired_name not in initializer_source
|
||||
for detail in (
|
||||
"ChainRuntimeContext(",
|
||||
"TransactionalTransferAdmissionRepository(SessionFactory)",
|
||||
"TransactionalChainDurableEventWriter(SessionFactory)",
|
||||
"def execute_legacy_transfer_command(",
|
||||
"def build_chain_runtime_context(",
|
||||
"def configure_wallpaper_services(",
|
||||
"def configure_chain_runtime_context(",
|
||||
):
|
||||
assert detail not in initializer_source
|
||||
assert detail in composition_source
|
||||
assert initializer_source.count(
|
||||
"TransactionalTransferExecutionRepository("
|
||||
) == 1
|
||||
assert "transfer_execution=transfer_execution_repository" in initializer_source
|
||||
assert "transfer_execution_repository=transfer_execution" in composition_source
|
||||
assert "TransactionalTransferExecutionRepository" not in composition_source
|
||||
transfer_function = next(
|
||||
node
|
||||
for node in composition_tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "execute_legacy_transfer_command"
|
||||
)
|
||||
assert any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and node.module == "app.chain.transfer.facade"
|
||||
for node in ast.walk(transfer_function)
|
||||
)
|
||||
assert not any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and node.module == "app.chain.transfer.facade"
|
||||
for node in composition_tree.body
|
||||
)
|
||||
assert "`startup/composition/chain.py` owns" in architecture_source
|
||||
assert not any(
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef))
|
||||
for node in package_tree.body
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_workflow_oper_has_no_legacy_writer_or_duplicate_exports():
|
||||
"""工作流旧写入口只能存在于 SDK Legacy facade。"""
|
||||
oper_path = APP_ROOT / "db" / "oper" / "workflow.py"
|
||||
@@ -1119,11 +1187,15 @@ def test_startup_injects_user_adapter_instead_of_raw_oper():
|
||||
database_source = (
|
||||
APP_ROOT / "startup" / "composition" / "database.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
source = initializer_source + database_source
|
||||
chain_source = (
|
||||
APP_ROOT / "startup" / "composition" / "chain.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
source = initializer_source + database_source + chain_source
|
||||
|
||||
assert "TransactionalUserRepository" in source
|
||||
assert "SqlAlchemyUserRepository" in source
|
||||
assert "build_transactional_user_repository" in initializer_source
|
||||
assert "build_transactional_user_repository" not in initializer_source
|
||||
assert "build_transactional_user_repository" in chain_source
|
||||
assert "from app.db.oper.user import UserOper" not in source
|
||||
assert "user=lambda: UserOper()" not in source
|
||||
|
||||
@@ -1160,7 +1232,7 @@ def test_transfer_pending_oper_import_is_confined_to_database_boundary():
|
||||
|
||||
def test_startup_injects_transactional_transfer_admission_repository():
|
||||
"""启动组合根必须向 Chain 注入事务型整理准入仓储。"""
|
||||
path = APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
path = APP_ROOT / "startup" / "composition" / "chain.py"
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
imports_repository = any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Chain 运行上下文注入和无参兼容 provider 测试。"""
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
import pytest
|
||||
@@ -114,3 +116,86 @@ def test_chain_keeps_explicit_typed_repositories() -> None:
|
||||
assert chain.subscription_completion_scope is context.subscription_completion_scope
|
||||
assert chain.transfer_execution_repository is context.transfer_execution_repository
|
||||
assert chain.user_repository is context.user_repository
|
||||
|
||||
|
||||
def test_chain_composition_registers_lazy_compatibility_provider(monkeypatch) -> None:
|
||||
"""组合 API 只登记 provider,首次无参 Chain 请求时才构造完整上下文。"""
|
||||
from app.startup.composition import chain as chain_composition
|
||||
|
||||
context = _context()
|
||||
builder = Mock(return_value=context)
|
||||
register = Mock()
|
||||
dependencies = {
|
||||
"message_helper": Mock(),
|
||||
"message_queue": Mock(),
|
||||
"system_config": Mock(),
|
||||
"site": Mock(),
|
||||
"subscription": Mock(),
|
||||
"download_history": Mock(),
|
||||
"transfer_history": Mock(),
|
||||
"transfer_execution": Mock(),
|
||||
"configuration": Mock(),
|
||||
}
|
||||
monkeypatch.setattr(chain_composition, "build_chain_runtime_context", builder)
|
||||
monkeypatch.setattr(
|
||||
chain_composition,
|
||||
"configure_chain_runtime_context_provider",
|
||||
register,
|
||||
)
|
||||
|
||||
chain_composition.configure_chain_runtime_context(**dependencies)
|
||||
|
||||
builder.assert_not_called()
|
||||
provider = register.call_args.args[0]
|
||||
assert provider() is context
|
||||
builder.assert_called_once_with(**dependencies)
|
||||
|
||||
|
||||
def test_chain_composition_keeps_legacy_transfer_import_lazy(monkeypatch) -> None:
|
||||
"""旧整理命令仅在真实调用时加载 TransferChain,并原样转交参数。"""
|
||||
from app.startup.composition import chain as chain_composition
|
||||
|
||||
execute = Mock(return_value="transferred")
|
||||
transfer_chain = Mock(return_value=Mock(execute_legacy_transfer_command=execute))
|
||||
facade = ModuleType("app.chain.transfer.facade")
|
||||
facade.TransferChain = transfer_chain
|
||||
monkeypatch.setitem(sys.modules, "app.chain.transfer.facade", facade)
|
||||
|
||||
result = chain_composition.execute_legacy_transfer_command(
|
||||
source="downloads",
|
||||
target="library",
|
||||
)
|
||||
|
||||
assert result == "transferred"
|
||||
transfer_chain.assert_called_once_with()
|
||||
execute.assert_called_once_with(source="downloads", target="library")
|
||||
|
||||
|
||||
def test_chain_composition_registers_lazy_wallpaper_providers(monkeypatch) -> None:
|
||||
"""壁纸装配只登记 callable,不在启动阶段提前物化业务 Chain。"""
|
||||
from app.startup.composition import chain as chain_composition
|
||||
|
||||
register = Mock()
|
||||
tmdb_instance = Mock()
|
||||
tmdb_instance.get_random_wallpager.return_value = "tmdb-one"
|
||||
tmdb_instance.get_trending_wallpapers.return_value = ["tmdb-many"]
|
||||
media_instance = Mock()
|
||||
media_instance.get_latest_wallpaper.return_value = "media-one"
|
||||
media_instance.get_latest_wallpapers.return_value = ["media-many"]
|
||||
tmdb_chain = Mock(return_value=tmdb_instance)
|
||||
media_chain = Mock(return_value=media_instance)
|
||||
monkeypatch.setattr(chain_composition, "configure_wallpaper_providers", register)
|
||||
monkeypatch.setattr(chain_composition, "TmdbChain", tmdb_chain)
|
||||
monkeypatch.setattr(chain_composition, "MediaServerChain", media_chain)
|
||||
|
||||
chain_composition.configure_wallpaper_services()
|
||||
|
||||
tmdb_chain.assert_not_called()
|
||||
media_chain.assert_not_called()
|
||||
providers = register.call_args.kwargs
|
||||
assert providers["tmdb_wallpaper"]() == "tmdb-one"
|
||||
assert providers["tmdb_wallpapers"](3) == ["tmdb-many"]
|
||||
assert providers["mediaserver_wallpaper"]() == "media-one"
|
||||
assert providers["mediaserver_wallpapers"](4) == ["media-many"]
|
||||
tmdb_instance.get_trending_wallpapers.assert_called_once_with(3)
|
||||
media_instance.get_latest_wallpapers.assert_called_once_with(count=4)
|
||||
|
||||
@@ -29,9 +29,12 @@ def test_service_locator_gate_detects_each_concrete_runtime_family() -> None:
|
||||
"app.startup.initializers.modules": {
|
||||
"app.command",
|
||||
"app.runtime.extensions.module_manager",
|
||||
"app.runtime.extensions.plugin_manager",
|
||||
"app.scheduler.facade",
|
||||
},
|
||||
"app.startup.composition.chain": {
|
||||
"app.runtime.extensions.module_manager",
|
||||
"app.runtime.extensions.plugin_manager",
|
||||
},
|
||||
"app.startup.composition.outbox": {"app.command"},
|
||||
"app.workflow.manager": {"app.workflow"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user