Files
MoviePilot/tests/test_runtime_composition.py

236 lines
8.9 KiB
Python

"""宿主运行时组合根的对象身份、发布与冷导入测试。"""
from __future__ import annotations
import json
import subprocess
import sys
from types import SimpleNamespace
from unittest.mock import Mock
from app.runtime.tasks import TaskRegistry
from app.startup.composition import runtime as runtime_composition
def _inputs() -> runtime_composition.RuntimeInputs:
"""构造不连接数据库、不启动线程的最小运行时输入。"""
dependencies = runtime_composition.RuntimeDependencies(
download_history=Mock(name="download_history"),
transfer_history=Mock(name="transfer_history"),
site=Mock(name="site"),
subscription=Mock(name="subscription"),
subscription_history=Mock(name="subscription_history"),
transfer_execution=Mock(name="transfer_execution"),
message_helper=Mock(name="message_helper"),
message_queue=Mock(name="message_queue"),
)
system_config = Mock(name="system_config")
configuration = SimpleNamespace(
system_config=system_config,
system_service=Mock(name="system_service"),
runtime=Mock(name="runtime_configuration"),
settings=Mock(name="runtime_settings"),
)
database = SimpleNamespace(workflow_query=Mock(name="workflow_query"))
agent_data = SimpleNamespace(
sites=dependencies.site,
subscriptions=dependencies.subscription,
subscription_history=dependencies.subscription_history,
transfer_history=dependencies.transfer_history,
transfer_execution=dependencies.transfer_execution,
download_history=dependencies.download_history,
)
agent = SimpleNamespace(
data=agent_data,
chat_repository=Mock(name="chat_repository"),
persistence=Mock(name="chat_persistence"),
)
authentication = SimpleNamespace(
user_repository=Mock(name="user_repository"),
passkey_repository=Mock(name="passkey_repository"),
standalone_user=Mock(name="standalone_user"),
system_config=Mock(name="standalone_system_config"),
passkey=Mock(name="standalone_passkey"),
)
return runtime_composition.RuntimeInputs(
configuration=configuration,
database=database,
agent=agent,
authentication=authentication,
classification=Mock(name="classification_runtime"),
classification_execution=Mock(name="classification_execution"),
dependencies=dependencies,
tasks=TaskRegistry(),
)
def test_runtime_composition_reuses_dependencies_and_projects_api_identity() -> None:
"""HostRuntime、Agent、Chain 依赖与旧 API 投影必须保持同一对象身份。"""
inputs = _inputs()
composition = runtime_composition.compose_runtime(inputs)
runtime = composition.runtime
ports = composition.api_data
assert composition.dependencies is inputs.dependencies
assert runtime.agent is inputs.agent.data
assert runtime.agent_chat.repository is inputs.agent.chat_repository
assert runtime.agent_chat.persistence is inputs.agent.persistence
assert runtime.authentication.user_repository is inputs.authentication.user_repository
assert runtime.authentication.passkey_repository is inputs.authentication.passkey_repository
assert runtime.classification is inputs.classification
assert runtime.classification_execution is inputs.classification_execution
assert runtime.configuration is inputs.configuration.runtime
assert runtime.settings is inputs.configuration.settings
assert runtime.system._system_config is inputs.configuration.system_service
assert runtime.tasks is inputs.tasks
assert runtime.messaging.helper is inputs.dependencies.message_helper
assert runtime.messaging.queue is inputs.dependencies.message_queue
assert runtime.history.transfer_repository is inputs.dependencies.transfer_history
assert runtime.history.transfer_execution_repository is inputs.dependencies.transfer_execution
assert runtime.site.standalone is inputs.dependencies.site
assert inputs.agent.data.transfer_execution is runtime.history.transfer_execution_repository
assert inputs.agent.data.sites is runtime.site.standalone
assert ports.sync_session is runtime.persistence.sync_session
assert ports.async_session is runtime.persistence.async_session
assert ports.repositories == {
"download_history": runtime.history.download_repository,
"media_server": runtime.history.media_server_repository,
"message": runtime.messaging.repository,
"passkey": runtime.authentication.passkey_repository,
"site": runtime.site.repository,
"subscribe": runtime.subscription.repository,
"subscribe_history": runtime.subscription.history_repository,
"user": runtime.authentication.user_repository,
"workflow": runtime.workflow.repository,
}
assert ports.standalone == {
"passkey": runtime.authentication.passkey,
"system_config": runtime.authentication.system_config,
"user": runtime.authentication.standalone_user,
}
assert ports.unit_of_work == {
"async": runtime.persistence.async_transaction,
"sync": runtime.persistence.sync_transaction,
}
assert composition.site_query._repository is inputs.dependencies.site
assert composition.site_health._repository is inputs.dependencies.site
def test_runtime_composition_publishes_and_resets_exact_projection(monkeypatch) -> None:
"""运行时 owner 应发布并按对称顺序撤销其拥有的全部投影。"""
composition = runtime_composition.compose_runtime(_inputs())
published: dict[str, object] = {}
reset_order: list[str] = []
monkeypatch.setattr(
runtime_composition,
"configure_api_data_runtime",
lambda value: published.update(api=value),
)
monkeypatch.setattr(
runtime_composition,
"configure_transfer_history_repository",
lambda value: published.update(transfer=value),
)
monkeypatch.setattr(
runtime_composition,
"configure_site_query_service",
lambda value: published.update(site_query=value),
)
monkeypatch.setattr(
runtime_composition,
"configure_site_health_service",
lambda value: published.update(site_health=value),
)
monkeypatch.setattr(
runtime_composition,
"reset_site_health_service",
lambda: reset_order.append("site_health"),
)
monkeypatch.setattr(
runtime_composition,
"reset_site_query_service",
lambda: reset_order.append("site_query"),
)
monkeypatch.setattr(
runtime_composition,
"reset_transfer_history_repository",
lambda: reset_order.append("transfer_history"),
)
monkeypatch.setattr(
runtime_composition,
"reset_api_data_runtime",
lambda: reset_order.append("api_data"),
)
runtime_composition.publish_runtime(composition)
runtime_composition.reset_runtime()
assert published["api"] is composition.api_data
assert published["transfer"]() is composition.dependencies.transfer_history
assert published["site_query"] is composition.site_query
assert published["site_health"] is composition.site_health
assert reset_order == [
"site_health",
"site_query",
"transfer_history",
"api_data",
]
def test_runtime_composition_import_is_cold() -> None:
"""导入 runtime owner 不得加载 initializer、启动 worker/队列或发布端口。"""
completed = subprocess.run(
[
sys.executable,
"-c",
r"""
import json
import sys
from app.api.data import get_api_data_ports
from app.application.history import get_transfer_history_repository
from app.application.messaging.message import MessageHelper, MessageQueueManager
from app.application.site.health import get_configured_site_health_service
from app.application.site.query import get_configured_site_query_service
import app.startup.composition.runtime
unconfigured = []
for getter in (
get_api_data_ports,
get_transfer_history_repository,
get_configured_site_health_service,
get_configured_site_query_service,
):
try:
getter()
except RuntimeError:
unconfigured.append(True)
print(json.dumps({
"database_worker_loaded": "app.db.worker" in sys.modules,
"initializer_loaded": any(
name == "app.startup.initializers" or name.startswith("app.startup.initializers.")
for name in sys.modules
),
"message_helper_created": MessageHelper.get_existing_instance() is not None,
"message_queue_created": MessageQueueManager.get_existing_instance() is not None,
"unconfigured": len(unconfigured),
}))
""",
],
check=True,
capture_output=True,
text=True,
)
assert json.loads(completed.stdout) == {
"database_worker_loaded": False,
"initializer_loaded": False,
"message_helper_created": False,
"message_queue_created": False,
"unconfigured": 4,
}