mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: reorganize backend module boundaries
This commit is contained in:
+4
-4
@@ -6,7 +6,7 @@
|
||||
import sys
|
||||
|
||||
# 必须早于首个 import app.db(其在 import 期即按 CONFIG_PATH 连库):prepare_backend 内部
|
||||
# 先隔离 CONFIG_DIR、补 app.infrastructure.sites 垫片,再建表。app/testing 仅依赖标准库、import 不连库,
|
||||
# 先隔离 CONFIG_DIR、补 app.application.site.sites 垫片,再建表。app/testing 仅依赖标准库、import 不连库,
|
||||
# 故此处先 import 再调用是安全的。
|
||||
from app.testing.bootstrap import prepare_backend
|
||||
|
||||
@@ -33,7 +33,7 @@ def pytest_sessionfinish(session, exitstatus):
|
||||
_report_session_cleanup_error(session, "agent blocking executors", err)
|
||||
|
||||
try:
|
||||
from app.platform.thread import ThreadHelper
|
||||
from app.runtime.thread import ThreadHelper
|
||||
|
||||
helper = ThreadHelper.get_existing_instance()
|
||||
if helper:
|
||||
@@ -42,14 +42,14 @@ def pytest_sessionfinish(session, exitstatus):
|
||||
_report_session_cleanup_error(session, "thread helper", err)
|
||||
|
||||
try:
|
||||
from app.messaging.message import stop_message
|
||||
from app.application.messaging.message import stop_message
|
||||
|
||||
stop_message()
|
||||
except Exception as err:
|
||||
_report_session_cleanup_error(session, "message service", err)
|
||||
|
||||
try:
|
||||
from app.platform.log import LoggerManager
|
||||
from app.runtime.log import LoggerManager
|
||||
|
||||
LoggerManager.shutdown()
|
||||
except Exception as err:
|
||||
|
||||
@@ -4,9 +4,9 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from app.platform.config import ConfigModel
|
||||
from app.runtime.config import ConfigModel
|
||||
from app.modules.acoustid import AcoustIdModule
|
||||
from app.foundation.http import AsyncRequestUtils, RequestUtils
|
||||
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
|
||||
|
||||
|
||||
RECORDING_ID = "38035858-f990-4fbb-b3b2-f2f8b958eeba"
|
||||
|
||||
@@ -20,7 +20,7 @@ from app.agent.middleware.subagents import (
|
||||
SUBAGENT_TASK_TOOL_NAME,
|
||||
)
|
||||
from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.foundation.identity import SYSTEM_INTERNAL_USER_ID
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.messaging.agent import matches_channel_admin, resolve_config_principal_ids
|
||||
from app.application.messaging.agent import matches_channel_admin, resolve_config_principal_ids
|
||||
from app.modules.discord import DiscordModule
|
||||
from app.modules.feishu.feishu import Feishu
|
||||
from app.modules.qqbot import QQBotModule
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.agent.tools.catalog import (
|
||||
ToolIdentityAmbiguousError,
|
||||
)
|
||||
from app.agent.tools.impl.mcp import create_external_mcp_tools
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.agent import AgentMcpServerConfig
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from app.agent import MoviePilotAgent
|
||||
from app.agent.llm import AgentCapabilityManager, LLMHelper
|
||||
from app.chain.message import MessageChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.agent.tools.impl.send_local_file import SendLocalFileInput
|
||||
from app.agent import MoviePilotAgent, AgentChain
|
||||
from app.agent.llm import AgentCapabilityManager
|
||||
from app.chain.message import MessageChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.agent.llm import LLMHelper
|
||||
from app.modules.discord import DiscordModule
|
||||
from app.modules.qqbot import QQBotModule
|
||||
|
||||
@@ -10,12 +10,12 @@ from app.agent.tools.impl.ask_user_choice import (
|
||||
UserChoiceOptionInput,
|
||||
)
|
||||
from app.agent.tools.impl.send_message import SendMessageTool
|
||||
from app.messaging.interaction import (
|
||||
from app.application.messaging.interaction import (
|
||||
AgentInteractionOption,
|
||||
agent_interaction_manager,
|
||||
)
|
||||
from app.chain.message import MessageChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.message import ChannelCapability, ChannelCapabilityManager
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
@@ -199,7 +199,7 @@ class AgentCapabilityManagerTest(unittest.TestCase):
|
||||
]
|
||||
|
||||
with patch(
|
||||
"app.extensions.service_registry.ServiceConfigHelper.get_notification_configs",
|
||||
"app.runtime.extensions.service_registry.ServiceConfigHelper.get_notification_configs",
|
||||
return_value=configs,
|
||||
):
|
||||
self.assertTrue(
|
||||
|
||||
@@ -3,7 +3,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.agent import MoviePilotAgent
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas import AgentLLMProviderEventData
|
||||
from app.schemas.types import ChainEventType
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ from app.agent.tools.impl.ask_user_choice import (
|
||||
)
|
||||
from app.agent.tools.impl.send_message import SendMessageTool
|
||||
from app.chain.message import MessageChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.db import SessionFactory
|
||||
from app.db.message_oper import MessageOper
|
||||
from app.db.models.message import Message
|
||||
from app.messaging.interaction import AgentInteractionOption, agent_interaction_manager, media_interaction_manager
|
||||
from app.application.messaging.interaction import AgentInteractionOption, agent_interaction_manager, media_interaction_manager
|
||||
from app.schemas.types import MessageChannel, NotificationType
|
||||
|
||||
|
||||
@@ -165,10 +165,10 @@ def test_ask_user_choice_message_is_not_recorded_to_message_history():
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"app.platform.events.EventManager.async_send_event",
|
||||
"app.runtime.events.EventManager.async_send_event",
|
||||
new_callable=AsyncMock,
|
||||
) as async_send_event, patch(
|
||||
"app.messaging.message.MessageQueueManager.async_send_message",
|
||||
"app.application.messaging.message.MessageQueueManager.async_send_message",
|
||||
new_callable=AsyncMock,
|
||||
) as async_send_message:
|
||||
result = asyncio.run(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.agent.prompt import prompt_manager
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from app.agent.prompt import PromptManager
|
||||
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsInput
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
|
||||
|
||||
def test_moviepilot_info_does_not_expose_api_token_or_database_password(monkeypatch) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import patch
|
||||
from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.agent.tools.impl.recognize_captcha import RecognizeCaptchaTool
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
from app.integrations.ocr import OcrHelper
|
||||
from app.adapters.external.ocr import OcrHelper
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
@@ -67,7 +67,7 @@ def test_ocr_helper_extracts_data_url_base64_without_downloading_image():
|
||||
image_b64 = base64.b64encode(b"captcha-image").decode()
|
||||
image_url = f"data:image/png;base64,{image_b64}"
|
||||
|
||||
with patch("app.integrations.ocr.RequestUtils") as request_utils:
|
||||
with patch("app.adapters.external.ocr.RequestUtils") as request_utils:
|
||||
request_utils.return_value.post_res.return_value = _FakeResponse(
|
||||
payload={"result": "a8k2"}
|
||||
)
|
||||
@@ -86,7 +86,7 @@ def test_ocr_helper_normalizes_data_url_base64_padding():
|
||||
"""data:image 地址缺少 padding 时应补齐后提交给 OCR 服务。"""
|
||||
image_url = "data:image/jpeg;base64,YWJjZA"
|
||||
|
||||
with patch("app.integrations.ocr.RequestUtils") as request_utils:
|
||||
with patch("app.adapters.external.ocr.RequestUtils") as request_utils:
|
||||
request_utils.return_value.post_res.return_value = _FakeResponse(
|
||||
payload={"result": "z9k2"}
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.agent.tools.impl.read_file import ReadFileTool
|
||||
from app.agent.tools.impl.write_file import WriteFileTool
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
from app.agent import MoviePilotAgent
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.modules.feishu import FeishuModule
|
||||
from app.modules.telegram import TelegramModule
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
@@ -36,13 +36,13 @@ from app.agent.tools.impl.update_agent_task import (
|
||||
UpdateAgentTaskTool,
|
||||
)
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.db import SessionFactory
|
||||
from app.db.agenttask_oper import AgentTaskOper
|
||||
from app.db.models.agenttask import AgentTask
|
||||
from app.schemas import ScheduleInfo
|
||||
from app.scheduler import Scheduler
|
||||
from app.platform.scheduling import TimerUtils
|
||||
from app.runtime.scheduling import TimerUtils
|
||||
|
||||
|
||||
class _FakeAgentTaskScheduler:
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.agent.tools.impl.search_web import (
|
||||
DEFAULT_SEARCH_ENGINE,
|
||||
SearchWebTool,
|
||||
)
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
|
||||
|
||||
class TestAgentSearchWebTool(unittest.TestCase):
|
||||
|
||||
@@ -9,7 +9,7 @@ from app.agent.tools.impl._system_setting_utils import list_setting_specs
|
||||
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
|
||||
from app.agent.tools.impl.update_system_settings import UpdateSystemSettingsTool
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
from app.platform.config import Settings, settings
|
||||
from app.runtime.config import Settings, settings
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from langchain_core.messages import AIMessage
|
||||
|
||||
from app.agent import MoviePilotAgent
|
||||
from app.agent.memory import memory_manager
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.agent.tools.catalog import (
|
||||
ToolIdentityAmbiguousError,
|
||||
)
|
||||
from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
|
||||
|
||||
class _Arguments(BaseModel):
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.agent.tools.impl.ask_user_choice import AskUserChoiceInput, AskUserChoiceTool
|
||||
from app.agent.tools.impl.send_local_file import SendLocalFileTool
|
||||
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.agent.middleware.subagents import is_subagent_stream_metadata
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool
|
||||
from app.api.endpoints.openai import _OpenAIStreamingHandler
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.message import MessageResponse
|
||||
from app.schemas.types import MessageChannel, NotificationType
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_update_download_tasks_resolves_downloader_and_updates_all_supported_fie
|
||||
未显式传下载器时,应先按 Hash 解析任务所属下载器,再一次性执行多项修改。
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _download_dirs(),
|
||||
)
|
||||
hash_value = "a" * 40
|
||||
|
||||
@@ -59,10 +59,10 @@ def test_metainfo_path_inherits_bangumi_id_from_parent() -> None:
|
||||
def test_extended_ids_fall_back_when_installed_rust_is_old() -> None:
|
||||
"""当前Rust扩展缺少新字段时应直接使用Python解析器。"""
|
||||
with patch(
|
||||
"app.infrastructure.rust.supports_extended_media_ids",
|
||||
"app.adapters.system.rust.supports_extended_media_ids",
|
||||
return_value=False,
|
||||
), patch(
|
||||
"app.infrastructure.rust.find_metainfo",
|
||||
"app.adapters.system.rust.find_metainfo",
|
||||
side_effect=AssertionError("旧Rust扩展不应处理扩展来源ID"),
|
||||
):
|
||||
_, metainfo = find_metainfo("Frieren [anilist=154587]")
|
||||
@@ -81,7 +81,7 @@ def test_extended_ids_fall_back_when_installed_rust_is_old() -> None:
|
||||
)
|
||||
def test_python_metainfo_rejects_zero_identity_and_removes_tag(title: str) -> None:
|
||||
"""Python 标签解析器应移除零值标签,但不得生成媒体身份。"""
|
||||
with patch("app.infrastructure.rust.find_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.find_metainfo", return_value=None):
|
||||
parsed_title, metainfo = find_metainfo(title)
|
||||
|
||||
assert metainfo["media_source"] is None
|
||||
@@ -100,7 +100,7 @@ def test_metainfo_normalizes_zero_identity_from_old_rust_extension() -> None:
|
||||
},
|
||||
}
|
||||
with patch(
|
||||
"app.infrastructure.rust.find_metainfo",
|
||||
"app.adapters.system.rust.find_metainfo",
|
||||
return_value=rust_result,
|
||||
):
|
||||
parsed_title, metainfo = find_metainfo("Movie [tmdbid=0]")
|
||||
|
||||
@@ -17,7 +17,7 @@ from app.api.endpoints import storage as storage_endpoint
|
||||
from app.api.endpoints import system as system_endpoint
|
||||
from app.api.endpoints import transfer as transfer_endpoint
|
||||
from app.api.endpoints import user as user_endpoint
|
||||
from app.security.access import verify_resource_token
|
||||
from app.application.security.access import verify_resource_token
|
||||
from app.db.user_oper import (
|
||||
get_current_active_manage_user,
|
||||
get_current_active_manage_user_async,
|
||||
|
||||
@@ -17,7 +17,7 @@ from app.factory import (
|
||||
localized_unhandled_exception_handler,
|
||||
localized_validation_exception_handler,
|
||||
)
|
||||
from app.platform.localization import LocaleHelper
|
||||
from app.runtime.localization import LocaleHelper
|
||||
from app.schemas.common import JsonData
|
||||
from app.schemas.response import Response
|
||||
|
||||
|
||||
@@ -8,17 +8,23 @@ LEGACY_ROOTS = ("app.core", "app.helper", "app.utils")
|
||||
LEGACY_MODULES = {"app.log"}
|
||||
IMPLEMENTATION_ROOTS = (
|
||||
"app.agent.skills",
|
||||
"app.adapters",
|
||||
"app.application",
|
||||
"app.domain",
|
||||
"app.extensions",
|
||||
"app.foundation",
|
||||
"app.infrastructure",
|
||||
"app.integrations",
|
||||
"app.messaging",
|
||||
"app.platform",
|
||||
"app.security",
|
||||
"app.services",
|
||||
"app.runtime",
|
||||
)
|
||||
CYCLE_ROOTS = (*IMPLEMENTATION_ROOTS, "app.runtime.compat", "app.sdk")
|
||||
RETIRED_CANONICAL_ROOTS = (
|
||||
"compat",
|
||||
"extensions",
|
||||
"infrastructure",
|
||||
"integrations",
|
||||
"messaging",
|
||||
"platform",
|
||||
"security",
|
||||
"services",
|
||||
)
|
||||
CYCLE_ROOTS = (*IMPLEMENTATION_ROOTS, "app.compat", "app.sdk")
|
||||
RETIRED_CANONICAL_FILES = (
|
||||
"app/infrastructure/package_installer.py",
|
||||
"app/infrastructure/resource_updater.py",
|
||||
@@ -47,35 +53,45 @@ RETIRED_CANONICAL_FILES = (
|
||||
"app/startup/log_initializer.py",
|
||||
"app/messaging/notification.py",
|
||||
"app/messaging/webpush.py",
|
||||
"app/foundation/jieba.py",
|
||||
"app/foundation/module.py",
|
||||
"app/foundation/object.py",
|
||||
"app/foundation/structures.py",
|
||||
"app/foundation/zhconv.py",
|
||||
"app/runtime/runtime.py",
|
||||
"app/adapters/network/rss.py",
|
||||
"app/adapters/network/sites.pyi",
|
||||
)
|
||||
FORBIDDEN_CAPABILITY_IMPORTS = {
|
||||
"foundation": {
|
||||
"domain", "extensions", "infrastructure", "integrations", "messaging",
|
||||
"platform", "security", "services", "sdk", "compat", "log",
|
||||
},
|
||||
"domain": {
|
||||
"extensions", "integrations", "messaging", "security", "services",
|
||||
"sdk", "compat", "db", "infrastructure", "platform", "log",
|
||||
},
|
||||
"platform": {
|
||||
"domain", "extensions", "integrations", "messaging", "security",
|
||||
"services", "sdk", "compat",
|
||||
},
|
||||
"infrastructure": {
|
||||
"domain", "extensions", "integrations", "messaging", "security",
|
||||
"services", "sdk", "compat",
|
||||
},
|
||||
"extensions": {"messaging", "security", "services", "sdk", "compat"},
|
||||
"integrations": {
|
||||
"extensions", "messaging", "security", "services", "sdk", "compat",
|
||||
},
|
||||
"messaging": {"integrations", "security", "services", "sdk", "compat"},
|
||||
"security": {"extensions", "messaging", "services", "sdk", "compat"},
|
||||
"services": {"integrations", "messaging", "sdk", "compat"},
|
||||
"compat": {
|
||||
"domain", "extensions", "foundation", "infrastructure", "integrations",
|
||||
"messaging", "platform", "security", "services", "sdk",
|
||||
},
|
||||
FORBIDDEN_IMPORT_PREFIXES = {
|
||||
"app.foundation": (
|
||||
"app.adapters",
|
||||
"app.application",
|
||||
"app.db",
|
||||
"app.domain",
|
||||
"app.runtime",
|
||||
"app.sdk",
|
||||
),
|
||||
"app.domain": (
|
||||
"app.adapters",
|
||||
"app.application",
|
||||
"app.db",
|
||||
"app.runtime",
|
||||
"app.sdk",
|
||||
),
|
||||
"app.adapters": (
|
||||
"app.application",
|
||||
"app.runtime.compat",
|
||||
"app.runtime.extensions",
|
||||
"app.sdk",
|
||||
),
|
||||
"app.runtime": (
|
||||
"app.application",
|
||||
"app.sdk",
|
||||
),
|
||||
"app.application": (
|
||||
"app.runtime.compat",
|
||||
"app.sdk",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -220,12 +236,25 @@ def test_retired_canonical_filenames_do_not_return():
|
||||
assert leftovers == []
|
||||
|
||||
|
||||
def test_retired_canonical_roots_contain_no_python_sources():
|
||||
"""已收敛的新增目录不得再次以顶级 Python 包形式出现。"""
|
||||
leftovers = sorted(
|
||||
str(path.relative_to(PROJECT_ROOT))
|
||||
for root_name in RETIRED_CANONICAL_ROOTS
|
||||
for path in (APP_ROOT / root_name).rglob("*.py")
|
||||
)
|
||||
assert leftovers == []
|
||||
|
||||
|
||||
def test_host_code_does_not_import_legacy_roots():
|
||||
"""除插件和兼容层外,宿主代码必须使用 canonical 路径。"""
|
||||
violations: dict[str, set[str]] = {}
|
||||
for path in APP_ROOT.rglob("*.py"):
|
||||
relative = path.relative_to(APP_ROOT)
|
||||
if relative.parts[0] in {"compat", "plugins"}:
|
||||
if relative.parts[0] == "plugins" or relative.parts[:2] == (
|
||||
"runtime",
|
||||
"compat",
|
||||
):
|
||||
continue
|
||||
imports = _legacy_imports(path)
|
||||
if imports:
|
||||
@@ -257,11 +286,13 @@ def test_canonical_layers_do_not_depend_on_sdk_or_compat():
|
||||
for module_name, path in modules.items():
|
||||
if not module_name.startswith(IMPLEMENTATION_ROOTS):
|
||||
continue
|
||||
if module_name.startswith("app.runtime.compat"):
|
||||
continue
|
||||
dependencies = _resolve_imports(module_name, path, known_modules)
|
||||
forbidden = {
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if dependency.startswith(("app.sdk", "app.compat"))
|
||||
if dependency.startswith(("app.sdk", "app.runtime.compat"))
|
||||
}
|
||||
if forbidden:
|
||||
violations[module_name] = forbidden
|
||||
@@ -269,36 +300,38 @@ def test_canonical_layers_do_not_depend_on_sdk_or_compat():
|
||||
|
||||
|
||||
def test_capability_packages_do_not_import_forbidden_upper_layers():
|
||||
"""能力包只能依赖其明确允许的下层或同层协作包。"""
|
||||
"""新能力包只能依赖明确允许的下层或同层协作包。"""
|
||||
modules = _discover_modules()
|
||||
known_modules = set(modules)
|
||||
violations: dict[str, set[str]] = {}
|
||||
for module_name, path in modules.items():
|
||||
parts = module_name.split(".")
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
source_package = parts[1]
|
||||
forbidden_packages = FORBIDDEN_CAPABILITY_IMPORTS.get(source_package)
|
||||
if not forbidden_packages:
|
||||
source_root = next(
|
||||
(
|
||||
root
|
||||
for root in FORBIDDEN_IMPORT_PREFIXES
|
||||
if module_name == root or module_name.startswith(f"{root}.")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not source_root:
|
||||
continue
|
||||
dependencies = _resolve_imports(module_name, path, known_modules)
|
||||
forbidden = {
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if len(dependency.split(".")) >= 2
|
||||
and dependency.split(".")[1] in forbidden_packages
|
||||
if dependency.startswith(FORBIDDEN_IMPORT_PREFIXES[source_root])
|
||||
}
|
||||
if forbidden:
|
||||
violations[module_name] = forbidden
|
||||
assert violations == {}
|
||||
|
||||
|
||||
def test_platform_log_is_a_dependency_leaf():
|
||||
"""底层可引用平台日志,但日志模块本身不得反向导入应用模块。"""
|
||||
def test_runtime_log_is_a_dependency_leaf():
|
||||
"""底层可引用运行时日志,但日志模块本身不得反向导入应用模块。"""
|
||||
modules = _discover_modules()
|
||||
dependencies = _resolve_imports(
|
||||
"app.platform.log",
|
||||
modules["app.platform.log"],
|
||||
"app.runtime.log",
|
||||
modules["app.runtime.log"],
|
||||
set(modules),
|
||||
)
|
||||
assert {
|
||||
@@ -309,7 +342,7 @@ def test_platform_log_is_a_dependency_leaf():
|
||||
|
||||
|
||||
def test_foundation_does_not_emit_runtime_logs():
|
||||
"""基础机制不初始化日志系统,运行期诊断由上层调用方负责。"""
|
||||
"""基础机制不打印或初始化日志系统,运行期诊断由上层调用方负责。"""
|
||||
violations: list[str] = []
|
||||
for path in (APP_ROOT / "foundation").rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
@@ -321,25 +354,32 @@ def test_foundation_does_not_emit_runtime_logs():
|
||||
break
|
||||
if isinstance(node, ast.ImportFrom) and node.module in {
|
||||
"logging",
|
||||
"app.platform.log",
|
||||
"app.runtime.log",
|
||||
}:
|
||||
violations.append(str(path.relative_to(PROJECT_ROOT)))
|
||||
break
|
||||
if (
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == "print"
|
||||
):
|
||||
violations.append(str(path.relative_to(PROJECT_ROOT)))
|
||||
break
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_cache_contract_does_not_import_concrete_adapters():
|
||||
"""平台缓存契约和内存机制不得反向导入基础设施适配器。"""
|
||||
"""运行时缓存契约和内存机制不得反向导入具体缓存适配器。"""
|
||||
modules = _discover_modules()
|
||||
dependencies = _resolve_imports(
|
||||
"app.platform.cache",
|
||||
modules["app.platform.cache"],
|
||||
"app.runtime.cache",
|
||||
modules["app.runtime.cache"],
|
||||
set(modules),
|
||||
)
|
||||
assert {
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if dependency.startswith("app.infrastructure")
|
||||
if dependency.startswith("app.adapters.cache")
|
||||
} == set()
|
||||
|
||||
|
||||
@@ -347,8 +387,8 @@ def test_resource_adapter_does_not_restart_process():
|
||||
"""资源下载安装适配器不得反向调用进程重启能力。"""
|
||||
modules = _discover_modules()
|
||||
dependencies = _resolve_imports(
|
||||
"app.infrastructure.resource",
|
||||
modules["app.infrastructure.resource"],
|
||||
"app.adapters.system.resource",
|
||||
modules["app.adapters.system.resource"],
|
||||
set(modules),
|
||||
)
|
||||
assert "app.platform.runtime" not in dependencies
|
||||
assert "app.runtime.state" not in dependencies
|
||||
|
||||
@@ -4,8 +4,8 @@ import time
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.foundation import http as http_module
|
||||
from app.foundation.http import AsyncRequestUtils
|
||||
from app.adapters.network import http as http_module
|
||||
from app.adapters.network.http import AsyncRequestUtils
|
||||
|
||||
PROXY = "http://proxy.example:7890"
|
||||
URL = "https://raw.githubusercontent.com/demo/repo/main/package.json"
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.domain.meta.metamusic import (
|
||||
format_audio_quality,
|
||||
parse_audio_quality,
|
||||
)
|
||||
from app.services.audio import AudioMetadataHelper
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
from app.schemas.types import MUSIC_ENTITY_ALBUM
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_read_audio_metadata_maps_easy_tags(monkeypatch):
|
||||
sample_rate=44100,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read(Path("/music/08 - Get Lucky.flac"))
|
||||
|
||||
@@ -142,7 +142,7 @@ def test_parse_compact_audio_quality_tokens_without_false_sample_bitrate():
|
||||
|
||||
def test_read_audio_metadata_falls_back_to_filename(monkeypatch):
|
||||
"""无法读取标签时应保留可用于手动整理的文件名元数据。"""
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: None)
|
||||
|
||||
meta = AudioMetadataHelper.read(Path("/music/Unknown Track.mp3"))
|
||||
|
||||
@@ -156,7 +156,7 @@ def test_read_audio_tags_does_not_fill_from_filename(monkeypatch):
|
||||
tags={"title": ["Tagged Title"]},
|
||||
info=SimpleNamespace(length=180),
|
||||
)
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read_tags(Path("/music/Daft Punk - Get Lucky 2013.flac"))
|
||||
|
||||
@@ -174,7 +174,7 @@ def test_read_audio_tags_ignores_invalid_musicbrainz_id(monkeypatch):
|
||||
},
|
||||
info=SimpleNamespace(length=180),
|
||||
)
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read_tags(Path("/music/track.flac"))
|
||||
|
||||
@@ -201,7 +201,7 @@ def test_read_audio_metadata_fallback_uses_dynamic_filename_parser(tmp_path, mon
|
||||
"S H E - S H E十七音乐会 2018 WEB-DL 1080P AVC AAC-FHDMv.flac"
|
||||
)
|
||||
audio_path.write_bytes(b"fake-flac")
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: None)
|
||||
|
||||
meta = AudioMetadataHelper.read(audio_path)
|
||||
|
||||
@@ -227,7 +227,7 @@ def test_read_audio_metadata_partial_tags_use_filename_for_missing_fields(
|
||||
sample_rate=44100,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read(audio_path)
|
||||
|
||||
@@ -251,7 +251,7 @@ def test_read_audio_metadata_distinguishes_alac_inside_m4a(monkeypatch):
|
||||
sample_rate=96000,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read(Path("/music/Lossless Track.m4a"))
|
||||
|
||||
@@ -276,7 +276,7 @@ def test_write_audio_metadata_maps_music_info_to_easy_tags(monkeypatch):
|
||||
self.saved = True
|
||||
|
||||
audio = FakeAudio()
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
success = AudioMetadataHelper.write(
|
||||
Path("/music/08 - Get Lucky.flac"),
|
||||
@@ -319,7 +319,7 @@ def test_write_audio_metadata_does_not_write_album_id_as_recording_tag(monkeypat
|
||||
"""模拟 Mutagen 保存。"""
|
||||
|
||||
audio = FakeAudio()
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
success = AudioMetadataHelper.write(
|
||||
Path("/music/Random Access Memories.flac"),
|
||||
@@ -338,7 +338,7 @@ def test_write_audio_metadata_does_not_write_album_id_as_recording_tag(monkeypat
|
||||
def test_write_audio_metadata_can_embed_cover_without_rewriting_tags(monkeypatch):
|
||||
"""音乐封面策略应能在标签策略关闭时独立执行。"""
|
||||
audio = SimpleNamespace(tags={"title": ["Original"]})
|
||||
monkeypatch.setattr("app.services.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
write_cover = Mock()
|
||||
monkeypatch.setattr(AudioMetadataHelper, "_write_cover", write_cover)
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ from app.chain.scraping import ScrapingChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.platform.events import Event
|
||||
from app.runtime.events import Event
|
||||
from app.domain.metainfo import MetaInfoPath
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.platform.log import logger
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
from tests.cases.files import bluray_files
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from app.agent.tools.impl.browse_webpage import BrowserAction, BrowseWebpageTool
|
||||
from app.infrastructure.browser import BrowserSessionHelper, PlaywrightHelper
|
||||
from app.adapters.network.browser import BrowserSessionHelper, PlaywrightHelper
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
@@ -168,7 +168,7 @@ def test_default_emulation_uses_cloakbrowser_context():
|
||||
page = _FakePage()
|
||||
context = _FakeContext([page])
|
||||
|
||||
with patch("app.infrastructure.browser.settings.BROWSER_EMULATION", "cloakbrowser"), patch.object(
|
||||
with patch("app.adapters.network.browser.settings.BROWSER_EMULATION", "cloakbrowser"), patch.object(
|
||||
PlaywrightHelper,
|
||||
"_PlaywrightHelper__launch_cloakbrowser_context",
|
||||
return_value=context,
|
||||
@@ -197,7 +197,7 @@ def test_legacy_playwright_emulation_uses_cloakbrowser_context():
|
||||
page = _FakePage()
|
||||
context = _FakeContext([page])
|
||||
|
||||
with patch("app.infrastructure.browser.settings.BROWSER_EMULATION", "Playwright"), patch.object(
|
||||
with patch("app.adapters.network.browser.settings.BROWSER_EMULATION", "Playwright"), patch.object(
|
||||
PlaywrightHelper,
|
||||
"_PlaywrightHelper__launch_cloakbrowser_context",
|
||||
return_value=context,
|
||||
|
||||
@@ -5,21 +5,21 @@ import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from app.infrastructure.cache import (
|
||||
from app.adapters.cache.backends import (
|
||||
AsyncFileBackend,
|
||||
AsyncRedisBackend,
|
||||
FileBackend,
|
||||
RedisBackend,
|
||||
)
|
||||
from app.platform.cache import (
|
||||
from app.runtime.cache import (
|
||||
AsyncFileCache,
|
||||
AsyncMemoryBackend,
|
||||
FileCache,
|
||||
MemoryBackend,
|
||||
cached,
|
||||
)
|
||||
from app.platform.config import settings
|
||||
from app.infrastructure.redis import AsyncRedisHelper, RedisHelper, serialize
|
||||
from app.runtime.config import settings
|
||||
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper, serialize
|
||||
|
||||
def test_file_backend_items_keep_relative_keys_and_bytes(tmp_path):
|
||||
"""
|
||||
@@ -542,8 +542,8 @@ def test_redis_helper_uses_blocking_pool_settings(monkeypatch):
|
||||
monkeypatch.setattr(settings, "CACHE_BACKEND_URL", "redis://cache:6379/2")
|
||||
monkeypatch.setattr(settings, "CACHE_REDIS_MAX_CONNECTIONS", 7)
|
||||
monkeypatch.setattr(settings, "CACHE_REDIS_POOL_TIMEOUT", 3)
|
||||
monkeypatch.setattr("app.infrastructure.redis.redis.BlockingConnectionPool.from_url", fake_from_url)
|
||||
monkeypatch.setattr("app.infrastructure.redis.redis.Redis", FakeClient)
|
||||
monkeypatch.setattr("app.adapters.cache.redis.redis.BlockingConnectionPool.from_url", fake_from_url)
|
||||
monkeypatch.setattr("app.adapters.cache.redis.redis.Redis", FakeClient)
|
||||
|
||||
helper = RedisHelper()
|
||||
helper.close()
|
||||
@@ -621,8 +621,8 @@ def test_async_redis_helper_uses_blocking_pool_settings(monkeypatch):
|
||||
monkeypatch.setattr(settings, "CACHE_BACKEND_URL", "redis://cache:6379/3")
|
||||
monkeypatch.setattr(settings, "CACHE_REDIS_MAX_CONNECTIONS", 9)
|
||||
monkeypatch.setattr(settings, "CACHE_REDIS_POOL_TIMEOUT", 4)
|
||||
monkeypatch.setattr("app.infrastructure.redis.AsyncBlockingConnectionPool.from_url", fake_from_url)
|
||||
monkeypatch.setattr("app.infrastructure.redis.Redis", FakeAsyncClient)
|
||||
monkeypatch.setattr("app.adapters.cache.redis.AsyncBlockingConnectionPool.from_url", fake_from_url)
|
||||
monkeypatch.setattr("app.adapters.cache.redis.Redis", FakeAsyncClient)
|
||||
|
||||
config_calls = asyncio.run(run_connect())
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ def load_cli_module():
|
||||
app_module = ModuleType("app")
|
||||
core_module = ModuleType("app.core")
|
||||
helper_module = ModuleType("app.helper")
|
||||
config_module = ModuleType("app.platform.config")
|
||||
system_module = ModuleType("app.platform.runtime")
|
||||
config_module = ModuleType("app.runtime.config")
|
||||
system_module = ModuleType("app.runtime.state")
|
||||
version_module = ModuleType("version")
|
||||
psutil_module = ModuleType("psutil")
|
||||
|
||||
@@ -68,8 +68,8 @@ def load_cli_module():
|
||||
"app": app_module,
|
||||
"app.core": core_module,
|
||||
"app.helper": helper_module,
|
||||
"app.platform.config": config_module,
|
||||
"app.platform.runtime": system_module,
|
||||
"app.runtime.config": config_module,
|
||||
"app.runtime.state": system_module,
|
||||
"version": version_module,
|
||||
"psutil": psutil_module,
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import asyncio
|
||||
from typing import List
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
|
||||
from app.platform.coalesce import (
|
||||
from app.runtime.coalesce import (
|
||||
CoalesceDecision,
|
||||
CoalesceSummary,
|
||||
EventCoalescer,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
|
||||
|
||||
def test_update_float_setting_accepts_json_integer(monkeypatch) -> None:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from app.db import SessionFactory
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
from app.infrastructure import system as system_module
|
||||
from app.infrastructure.system import SystemUtils
|
||||
from app.adapters.system import host as system_module
|
||||
from app.adapters.system.host import SystemUtils
|
||||
|
||||
|
||||
def test_dashboard_system_info_returns_runtime_environment(monkeypatch):
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.db.models.downloadhistory import DownloadHistory, DownloadFiles
|
||||
from app.db.models.message import Message
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.scheduler import SchedulerChain
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ from alembic.script import ScriptDirectory
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.db import Engine
|
||||
from app.db.init import init_db, update_db
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def _run_permission_case(tmp_path: Path, body: str, env: dict[str, str] | None =
|
||||
fake_bin = _write_fake_chown(tmp_path)
|
||||
chown_log = tmp_path / "chown.log"
|
||||
app_dir = tmp_path / "app"
|
||||
resource_dir = app_dir / "app" / "infrastructure"
|
||||
resource_dir = app_dir / "app" / "application" / "site"
|
||||
public_dir = tmp_path / "public"
|
||||
home_dir = tmp_path / "home"
|
||||
(app_dir / "app" / "plugins").mkdir(parents=True)
|
||||
@@ -192,7 +192,7 @@ def test_runtime_writable_paths_are_still_corrected(tmp_path: Path) -> None:
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/home/runtime" in lines
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/config /var/lib/nginx /var/log/nginx" in lines
|
||||
assert "moviepilot:moviepilot /etc/hosts /tmp" in lines
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/app/app/infrastructure" in lines
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/app/app/application/site" in lines
|
||||
assert not any(line.startswith("-R ") and ".cloakbrowser" in line for line in lines)
|
||||
assert not any(f"{tmp_path}/app " in line for line in lines)
|
||||
assert not any(f"{tmp_path}/public" in line for line in lines)
|
||||
@@ -205,7 +205,7 @@ def test_site_resource_permissions_are_repaired_even_when_owner_matches(tmp_path
|
||||
)
|
||||
|
||||
lines = log.splitlines()
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/app/app/infrastructure" in lines
|
||||
assert f"-R moviepilot:moviepilot {tmp_path}/app/app/application/site" in lines
|
||||
assert not any(line.startswith("-R ") and f"{tmp_path}/app " in line for line in lines)
|
||||
assert not any(line.startswith("-R ") and f"{tmp_path}/public" in line for line in lines)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.doctor import checks, run_doctor
|
||||
from app.doctor.formatters import format_json_report, format_text_report
|
||||
from app.doctor.models import DoctorFinding, DoctorFindingStatus, DoctorSeverity
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import socket
|
||||
|
||||
from app.infrastructure import doh
|
||||
from app.adapters.network import doh
|
||||
|
||||
|
||||
def test_doh_executor_is_lazy_and_shutdown_restores_socket(monkeypatch):
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
import app.chain.download as download_module
|
||||
from app.chain.download import DownloadChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.schemas import DownloaderTorrent, FileItem, NotExistMediaInfo, TransferDirectoryConf
|
||||
@@ -138,7 +138,7 @@ def test_download_single_submits_download_added_to_background(monkeypatch):
|
||||
"""
|
||||
_FakeThreadHelper.submitted = []
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _download_dirs(),
|
||||
)
|
||||
monkeypatch.setattr(download_module, "ThreadHelper", _FakeThreadHelper)
|
||||
@@ -258,7 +258,7 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch):
|
||||
|
||||
_FakeThreadHelper.submitted = []
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _download_dirs(),
|
||||
)
|
||||
monkeypatch.setattr(download_module, "ThreadHelper", _FakeThreadHelper)
|
||||
@@ -783,7 +783,7 @@ def test_download_single_records_failure_cooldown_when_downloader_rejects(monkey
|
||||
return SimpleNamespace(id=1)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _download_dirs(),
|
||||
)
|
||||
monkeypatch.setattr(download_module, "TorrentHelper", _FakeTorrentHelper)
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.agent.tools.impl.update_download_tasks import UpdateDownloadTasksTool
|
||||
from app.chain.download import DownloadChain
|
||||
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.services.directory import validate_download_save_path
|
||||
from app.application.directory import validate_download_save_path
|
||||
from app.schemas import DownloaderTorrent, TransferDirectoryConf
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
@@ -133,7 +133,7 @@ def _nested_download_dirs():
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_download_dirs(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _download_dirs(),
|
||||
)
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_validate_download_save_path_accepts_legacy_remote_path_without_storage_
|
||||
def test_validate_download_save_path_prefers_configured_local_root(monkeypatch):
|
||||
"""无前缀路径同时命中本地和远程根目录时应保持本地语义。"""
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: [
|
||||
TransferDirectoryConf(
|
||||
name="远程下载",
|
||||
@@ -191,7 +191,7 @@ def test_validate_download_save_path_accepts_windows_configured_root_and_childre
|
||||
expected,
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _windows_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ def test_validate_download_save_path_rejects_windows_paths_outside_configured_ro
|
||||
save_path,
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _windows_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -255,7 +255,7 @@ def _build_tv_media() -> MediaInfo:
|
||||
|
||||
def test_resolve_media_download_dir_applies_configured_root_classification(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -271,7 +271,7 @@ def test_resolve_media_download_dir_applies_configured_root_classification(monke
|
||||
|
||||
def test_resolve_media_download_dir_keeps_configured_child_path_exact(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -287,7 +287,7 @@ def test_resolve_media_download_dir_keeps_configured_child_path_exact(monkeypatc
|
||||
|
||||
def test_resolve_media_download_dir_applies_remote_root_classification(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -304,7 +304,7 @@ def test_resolve_media_download_dir_applies_remote_root_classification(monkeypat
|
||||
def test_resolve_media_download_dir_accepts_legacy_remote_root_without_storage_prefix(monkeypatch):
|
||||
"""订阅中的旧版远程根路径应按对应存储和分类配置解析。"""
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -320,7 +320,7 @@ def test_resolve_media_download_dir_accepts_legacy_remote_root_without_storage_p
|
||||
|
||||
def test_resolve_media_download_dir_uses_matching_media_specific_root(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _media_specific_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -348,7 +348,7 @@ def test_resolve_media_download_dir_uses_exact_nested_root_configuration(
|
||||
expected,
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _nested_download_dirs(),
|
||||
)
|
||||
|
||||
@@ -429,7 +429,7 @@ def test_download_single_rejects_event_overridden_bad_save_path_before_downloade
|
||||
def test_download_single_applies_configured_root_classification(monkeypatch):
|
||||
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
chain = _build_download_chain()
|
||||
@@ -450,7 +450,7 @@ def test_download_single_accepts_legacy_remote_root_without_storage_prefix(monke
|
||||
"""旧订阅的无前缀远程根应以正确 FileURI 提交给下载模块。"""
|
||||
monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
"app.services.directory.DirectoryHelper.get_download_dirs",
|
||||
"app.application.directory.DirectoryHelper.get_download_dirs",
|
||||
lambda _self: _classified_download_dirs(),
|
||||
)
|
||||
chain = _build_download_chain()
|
||||
|
||||
@@ -13,12 +13,12 @@ def _load_downloader_base():
|
||||
app_module.__path__ = []
|
||||
helper_module = types.ModuleType("app.helper")
|
||||
helper_module.__path__ = []
|
||||
service_module = types.ModuleType("app.extensions.service_registry")
|
||||
service_module = types.ModuleType("app.runtime.extensions.service_registry")
|
||||
schemas_module = types.ModuleType("app.schemas")
|
||||
schema_types_module = types.ModuleType("app.schemas.types")
|
||||
utils_module = types.ModuleType("app.utils")
|
||||
utils_module.__path__ = []
|
||||
mixins_module = types.ModuleType("app.platform.reload")
|
||||
mixins_module = types.ModuleType("app.runtime.reload")
|
||||
|
||||
class StorageSchema(Enum):
|
||||
Local = "local"
|
||||
@@ -75,11 +75,11 @@ def _load_downloader_base():
|
||||
stub_modules = {
|
||||
"app": app_module,
|
||||
"app.helper": helper_module,
|
||||
"app.extensions.service_registry": service_module,
|
||||
"app.runtime.extensions.service_registry": service_module,
|
||||
"app.schemas": schemas_module,
|
||||
"app.schemas.types": schema_types_module,
|
||||
"app.utils": utils_module,
|
||||
"app.platform.reload": mixins_module,
|
||||
"app.runtime.reload": mixins_module,
|
||||
}
|
||||
|
||||
module_path = repo_root / "app" / "modules" / "__init__.py"
|
||||
@@ -101,7 +101,7 @@ def _load_transmission_module():
|
||||
app_module.__path__ = []
|
||||
core_module = types.ModuleType("app.core")
|
||||
core_module.__path__ = []
|
||||
cache_module = types.ModuleType("app.platform.cache")
|
||||
cache_module = types.ModuleType("app.runtime.cache")
|
||||
modules_module = types.ModuleType("app.modules")
|
||||
modules_module.__path__ = []
|
||||
transmission_package_module = types.ModuleType("app.modules.transmission")
|
||||
@@ -109,9 +109,9 @@ def _load_transmission_module():
|
||||
transmission_client_module = types.ModuleType("app.modules.transmission.transmission")
|
||||
schemas_module = types.ModuleType("app.schemas")
|
||||
schema_types_module = types.ModuleType("app.schemas.types")
|
||||
config_module = types.ModuleType("app.platform.config")
|
||||
config_module = types.ModuleType("app.runtime.config")
|
||||
metainfo_module = types.ModuleType("app.domain.metainfo")
|
||||
log_module = types.ModuleType("app.platform.log")
|
||||
log_module = types.ModuleType("app.runtime.log")
|
||||
utils_module = types.ModuleType("app.utils")
|
||||
utils_module.__path__ = []
|
||||
string_module = types.ModuleType("app.domain.string")
|
||||
@@ -233,10 +233,10 @@ def _load_transmission_module():
|
||||
stub_modules = {
|
||||
"app": app_module,
|
||||
"app.core": core_module,
|
||||
"app.platform.cache": cache_module,
|
||||
"app.platform.config": config_module,
|
||||
"app.runtime.cache": cache_module,
|
||||
"app.runtime.config": config_module,
|
||||
"app.domain.metainfo": metainfo_module,
|
||||
"app.platform.log": log_module,
|
||||
"app.runtime.log": log_module,
|
||||
"app.modules": modules_module,
|
||||
"app.modules.transmission": transmission_package_module,
|
||||
"app.modules.transmission.transmission": transmission_client_module,
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.services.formatting import EpisodeFormatRuleHelper, FormatParser, _AutoRecommendSample
|
||||
from app.application.formatting import EpisodeFormatRuleHelper, FormatParser, _AutoRecommendSample
|
||||
from app.schemas import EpisodeFormatRule, FileItem
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ def _make_file(name: str, size: int = 150 * 1024 * 1024) -> FileItem:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_media_exts(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.services.formatting.settings.RMT_MEDIAEXT",
|
||||
"app.application.formatting.settings.RMT_MEDIAEXT",
|
||||
[".mkv", ".mp4"],
|
||||
)
|
||||
|
||||
@@ -233,7 +233,7 @@ def test_auto_recommend_returns_false_when_parse_raises(monkeypatch):
|
||||
def _raise_parse(*args, **kwargs):
|
||||
raise ValueError("broken parse")
|
||||
|
||||
monkeypatch.setattr("app.services.formatting._match_template", _raise_parse)
|
||||
monkeypatch.setattr("app.application.formatting._match_template", _raise_parse)
|
||||
|
||||
state, errmsg, data = helper.recommend([], samples)
|
||||
|
||||
@@ -432,7 +432,7 @@ def test_auto_recommend_uses_native_episode_as_fallback(monkeypatch):
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.formatting.anitopy.parse",
|
||||
"app.application.formatting.anitopy.parse",
|
||||
lambda _: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -508,7 +508,7 @@ def test_auto_recommend_prefers_bracket_episode_over_title_sequence_native(monke
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.formatting.anitopy.parse",
|
||||
"app.application.formatting.anitopy.parse",
|
||||
lambda _: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -541,7 +541,7 @@ def test_auto_recommend_corrects_anitopy_title_sequence_bias(monkeypatch):
|
||||
return {"episode_number": "3"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.formatting.anitopy.parse",
|
||||
"app.application.formatting.anitopy.parse",
|
||||
_mock_parse,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -611,7 +611,7 @@ def test_extract_episode_with_native_fallback_keeps_anitopy_range_list(monkeypat
|
||||
item = _make_file("Show - 01-02 [02].mkv")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.formatting.anitopy.parse",
|
||||
"app.application.formatting.anitopy.parse",
|
||||
lambda _: {"episode_number": ["01", "02"]},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from app.platform.events import Event, EventManager
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.events import Event, EventManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.schemas.types import ChainEventType
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parents[1] / "skills" / "feedback-issue" / "scripts"
|
||||
|
||||
@@ -12,7 +12,7 @@ from unittest.mock import patch
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parents[1] / "skills" / "feedback-issue" / "scripts"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.foundation.http import RequestUtils
|
||||
from app.adapters.network.http import RequestUtils
|
||||
|
||||
|
||||
def test_xml_decoding_prefers_xml_declaration_over_http_default():
|
||||
|
||||
@@ -18,13 +18,13 @@ class JellyfinUserResolutionTest(unittest.TestCase):
|
||||
def test_loader_does_not_leave_stub_modules_in_sys_modules(self):
|
||||
self.assertNotIn("_test_jellyfin_module", sys.modules)
|
||||
self.assertFalse(
|
||||
getattr(sys.modules.get("app.platform.log"), "_jellyfin_test_stub", False)
|
||||
getattr(sys.modules.get("app.runtime.log"), "_jellyfin_test_stub", False)
|
||||
)
|
||||
self.assertFalse(
|
||||
getattr(sys.modules.get("app.platform.config"), "_jellyfin_test_stub", False)
|
||||
getattr(sys.modules.get("app.runtime.config"), "_jellyfin_test_stub", False)
|
||||
)
|
||||
self.assertFalse(
|
||||
getattr(sys.modules.get("app.foundation.http"), "_jellyfin_test_stub", False)
|
||||
getattr(sys.modules.get("app.adapters.network.http"), "_jellyfin_test_stub", False)
|
||||
)
|
||||
|
||||
def _build_client(self) -> Jellyfin:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.foundation.jieba import cut
|
||||
from app.foundation.text import cut
|
||||
|
||||
|
||||
def test_cut_accepts_legacy_hmm_argument():
|
||||
|
||||
@@ -6,14 +6,14 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.compat.diagnostics import (
|
||||
from app.runtime.compat.diagnostics import (
|
||||
configure_legacy_import_diagnostics,
|
||||
get_legacy_import_diagnostics,
|
||||
reset_legacy_import_diagnostics,
|
||||
scan_plugin_legacy_imports,
|
||||
)
|
||||
from app.compat.imports import install_legacy_import_hook
|
||||
from app.compat.manifest import (
|
||||
from app.runtime.compat.imports import install_legacy_import_hook
|
||||
from app.runtime.compat.manifest import (
|
||||
MODULE_ALIASES,
|
||||
PACKAGE_ALIASES,
|
||||
PACKAGE_EXPORTS,
|
||||
@@ -164,8 +164,8 @@ def test_manifest_aliases_reuse_real_canonical_modules():
|
||||
"""正式映射表中的旧路径应在隔离进程中复用全部 canonical 模块。"""
|
||||
code = """
|
||||
import importlib
|
||||
from app.compat.manifest import MODULE_ALIASES
|
||||
# CI 无 app.infrastructure.sites 二进制模块,先补垫片再校验全部映射(与 conftest 同源)。
|
||||
from app.runtime.compat.manifest import MODULE_ALIASES
|
||||
# CI 无 app.application.site.sites 二进制模块,先补垫片再校验全部映射(与 conftest 同源)。
|
||||
from app.testing.bootstrap import ensure_sites_stub
|
||||
ensure_sites_stub()
|
||||
|
||||
@@ -173,7 +173,7 @@ for legacy_name, alias in MODULE_ALIASES.items():
|
||||
try:
|
||||
canonical = importlib.import_module(alias.target)
|
||||
except ModuleNotFoundError:
|
||||
if alias.target == "app.infrastructure.sites":
|
||||
if alias.target == "app.application.site.sites":
|
||||
continue
|
||||
raise
|
||||
legacy = importlib.import_module(legacy_name)
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.startup import lifecycle, modules_initializer
|
||||
from app.foundation import http as http_utils
|
||||
from app.adapters.network import http as http_utils
|
||||
|
||||
|
||||
def _assert_completed_once(mock: MagicMock) -> None:
|
||||
@@ -227,7 +227,7 @@ def test_restart_endpoint_failure_preserves_stop_state(
|
||||
def test_command_restart_failure_does_not_publish_stop_request(monkeypatch):
|
||||
"""命令重启失败时进程仍在运行,不能提前发布停止请求"""
|
||||
from app.chain.system import SystemChain
|
||||
from app.platform.config import global_vars
|
||||
from app.runtime.config import global_vars
|
||||
|
||||
stop_event = threading.Event()
|
||||
monkeypatch.setattr(global_vars, "STOP_EVENT", stop_event)
|
||||
|
||||
@@ -121,7 +121,7 @@ def _build_fake_openai_modules(chat_openai_cls=_FakeChatOpenAIForPatch):
|
||||
|
||||
# 以假 settings/log 控制 helper 加载期行为;用唯一模块名加载,并以 stub_modules 上下文
|
||||
# 在 import 期注入、退出后还原真实平台配置与日志模块,避免污染其他测试。
|
||||
_config_stub = ModuleType("app.platform.config")
|
||||
_config_stub = ModuleType("app.runtime.config")
|
||||
_config_stub.settings = SimpleNamespace(
|
||||
LLM_PROVIDER="global-provider",
|
||||
LLM_MODEL="global-model",
|
||||
@@ -136,11 +136,11 @@ _config_stub.settings = SimpleNamespace(
|
||||
LLM_USE_PROXY=True,
|
||||
PROXY_HOST=None,
|
||||
)
|
||||
_log_stub = ModuleType("app.platform.log")
|
||||
_log_stub = ModuleType("app.runtime.log")
|
||||
_log_stub.logger = _DummyLogger()
|
||||
|
||||
module_path = Path(__file__).resolve().parents[1] / "app" / "agent" / "llm" / "helper.py"
|
||||
with stub_modules({"app.platform.config": _config_stub, "app.platform.log": _log_stub}):
|
||||
with stub_modules({"app.runtime.config": _config_stub, "app.runtime.log": _log_stub}):
|
||||
spec = importlib.util.spec_from_file_location("test_llm_module", module_path)
|
||||
llm_module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
|
||||
@@ -7,8 +7,8 @@ from types import SimpleNamespace
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.factory import create_app, localized_http_exception_handler
|
||||
from app.platform.localization import LocaleHelper
|
||||
from app.platform.progress import ProgressHelper
|
||||
from app.runtime.localization import LocaleHelper
|
||||
from app.runtime.progress import ProgressHelper
|
||||
from app.schemas.dashboard import ScheduleInfo, ScheduleProgress
|
||||
from app.schemas.response import Response
|
||||
from version import APP_VERSION
|
||||
|
||||
@@ -2,7 +2,7 @@ import threading
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.platform.log import LogEntry, NonBlockingFileHandler, log_settings
|
||||
from app.runtime.log import LogEntry, NonBlockingFileHandler, log_settings
|
||||
|
||||
|
||||
def test_non_blocking_file_handler_shutdown_wakes_writer_and_closes_handlers(tmp_path):
|
||||
@@ -97,7 +97,7 @@ def test_non_blocking_file_handler_creates_one_handler_for_concurrent_first_writ
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
monkeypatch.setattr("app.platform.log.RotatingFileHandler", ProbeHandler)
|
||||
monkeypatch.setattr("app.runtime.log.RotatingFileHandler", ProbeHandler)
|
||||
file_path = tmp_path / "concurrent.log"
|
||||
|
||||
def get_handler(started=None):
|
||||
|
||||
@@ -9,13 +9,13 @@ MAIN_PATH = PROJECT_ROOT / "app" / "main.py"
|
||||
|
||||
|
||||
def test_main_script_does_not_shadow_stdlib_platform(tmp_path):
|
||||
"""PyCharm 脚本启动路径不得让 app/platform 遮蔽标准库 platform。"""
|
||||
"""PyCharm 脚本启动路径必须解析到标准库 platform。"""
|
||||
probe = "\n".join(
|
||||
(
|
||||
"import runpy",
|
||||
"import sys",
|
||||
# CI 无 app.infrastructure.sites 二进制模块,先补垫片再执行 main.py(与 conftest 同源);
|
||||
# 必须在篡改 sys.path / 摘除 platform 之前调用,避免真实模块依赖链误引被遮蔽的 app/platform。
|
||||
# CI 无 app.application.site.sites 二进制模块,先补垫片再执行 main.py(与 conftest 同源);
|
||||
# 必须在篡改 sys.path / 摘除 platform 之前调用,避免真实依赖链受探针环境影响。
|
||||
"from app.testing.bootstrap import ensure_sites_stub",
|
||||
"ensure_sites_stub()",
|
||||
f"sys.path.insert(0, {str(MAIN_PATH.parent)!r})",
|
||||
@@ -23,7 +23,7 @@ def test_main_script_does_not_shadow_stdlib_platform(tmp_path):
|
||||
f"runpy.run_path({str(MAIN_PATH)!r}, run_name='pycharm_main_probe')",
|
||||
"import platform",
|
||||
"assert hasattr(platform, 'python_implementation')",
|
||||
"assert '/app/platform/' not in str(getattr(platform, '__file__', ''))",
|
||||
f"assert {str(MAIN_PATH.parent)!r} not in str(getattr(platform, '__file__', ''))",
|
||||
)
|
||||
)
|
||||
env = {
|
||||
|
||||
@@ -5,9 +5,9 @@ from app.api.endpoints.transfer import (
|
||||
query_manual_transfer_history,
|
||||
)
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.services.history import (
|
||||
from app.application.history import (
|
||||
clear_transfer_failures,
|
||||
failed_retry_count,
|
||||
max_failed_retries,
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.agent.tools.factory import MoviePilotToolFactory
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
from app.agent.tools.catalog import ToolCatalogSnapshot
|
||||
from app.api.endpoints import mcp
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from app.chain.message import MediaInteractionChain, MessageChain
|
||||
from app.platform.events import EventManager
|
||||
from app.runtime.events import EventManager
|
||||
from app.domain.context import Context, MediaInfo, TorrentInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.messaging.interaction import media_interaction_manager, plugin_input_interaction_manager
|
||||
from app.application.messaging.interaction import media_interaction_manager, plugin_input_interaction_manager
|
||||
from app.schemas import CommingMessage, TransferDirectoryConf
|
||||
from app.schemas.types import EventType, MediaSource, MediaType, MessageChannel
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.integrations.server import MoviePilotServerHelper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
from app.chain import ChainBase
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.integrations.server import MoviePilotServerHelper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.schemas.types import MediaSource, MediaType, SystemConfigKey
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from fastapi import FastAPI
|
||||
from app.api.endpoints import media as media_endpoints
|
||||
from app.api.endpoints.media import search
|
||||
from app.chain import ChainBase
|
||||
from app.security.access import verify_token
|
||||
from app.application.security.access import verify_token
|
||||
from app.modules.douban import DoubanModule
|
||||
from app.modules.themoviedb import TheMovieDbModule
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
@@ -9,14 +9,14 @@ from app.testing import stub_modules
|
||||
_systemconfig_stub = MagicMock()
|
||||
_systemconfig_stub.SystemConfigOper.return_value.get.return_value = None
|
||||
with stub_modules({
|
||||
'app.infrastructure.sites': MagicMock(),
|
||||
'app.application.site.sites': MagicMock(),
|
||||
'app.db.systemconfig_oper': _systemconfig_stub,
|
||||
}):
|
||||
from app import schemas
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.scraping import ScrapingChain, ScrapingConfig, ScrapingOption
|
||||
from app.domain.context import MediaInfo
|
||||
from app.platform.events import Event
|
||||
from app.runtime.events import Event
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.schemas.types import EventType, MediaType, ScrapingTarget, ScrapingMetadata, ScrapingPolicy
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.extensions.service_registry import ServiceConfigHelper
|
||||
from app.runtime.extensions.service_registry import ServiceConfigHelper
|
||||
from app.schemas.system import MediaServerConf
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_mediaserver_conf_tolerates_blank_sync_interval():
|
||||
def test_get_configs_skips_invalid_entries(monkeypatch):
|
||||
"""单条配置校验失败时应跳过该条,不影响其它服务配置的加载。"""
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.service_registry.SystemConfigOper.get",
|
||||
"app.runtime.extensions.service_registry.SystemConfigOper.get",
|
||||
lambda self, key: [
|
||||
{"name": "good", "type": "emby", "enabled": True},
|
||||
"bad-format",
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import Mock
|
||||
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.schemas import MediaServerLibrary, MediaServerPlayItem
|
||||
from app.security.url import SecurityUtils
|
||||
from app.application.security.url import SecurityUtils
|
||||
|
||||
|
||||
class MediaServerImageSigningTest(unittest.TestCase):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from app.domain.context import MediaInfo
|
||||
from app.messaging.message import TemplateContextBuilder
|
||||
from app.application.messaging.message import TemplateContextBuilder
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.db import AsyncSessionFactory, SessionFactory
|
||||
from app.db.message_oper import MessageOper
|
||||
from app.db.models.message import Message
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.messaging.message import MessageHelper
|
||||
from app.application.messaging.message import MessageHelper
|
||||
from app.schemas import Notification, NotificationClearScope
|
||||
from app.schemas.types import MediaType, NotificationType, SystemConfigKey
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
|
||||
from app.messaging.message import MessageQueueManager, TemplateHelper, stop_message
|
||||
from app.application.messaging.message import MessageQueueManager, TemplateHelper, stop_message
|
||||
from app.foundation.singleton import SingletonClass
|
||||
|
||||
|
||||
|
||||
+18
-18
@@ -9,7 +9,7 @@ from app.domain.metainfo import MetaInfo, MetaInfoPath, find_metainfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.meta.metaanime import MetaAnime
|
||||
from app.services.torrent import TorrentHelper
|
||||
from app.application.torrent import TorrentHelper
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
from tests.cases.meta import meta_cases
|
||||
|
||||
@@ -162,7 +162,7 @@ def test_torrent_title_match_ignores_question_mark_variants():
|
||||
|
||||
def test_python_metainfo_fallback_preserves_xxx_movie_title():
|
||||
"""Python 兜底解析不应删除合法 xXx 片名。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo("xXx 2002 1080p AMZN WEB-DL H.264 DDP 5.1-FROGWeb")
|
||||
|
||||
assert meta.en_name == "Xxx"
|
||||
@@ -174,7 +174,7 @@ def test_python_metainfo_fallback_preserves_xxx_movie_title():
|
||||
|
||||
def test_python_metainfo_fallback_recognizes_eac3_audio_codec():
|
||||
"""Python 兜底解析应识别 EAC3 及其声道信息。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo("Test.Movie.2026.1080p.BluRay.x264.EAC3.5.1-GROUP")
|
||||
|
||||
assert meta.resource_pix == "1080p"
|
||||
@@ -216,7 +216,7 @@ def test_metainfo_preserves_all_resource_types(title, expected):
|
||||
@pytest.mark.parametrize(("title", "expected"), RESOURCE_TYPE_CASES)
|
||||
def test_python_metainfo_preserves_all_resource_types(title, expected):
|
||||
"""Python 兜底解析应按顺序保留并去重所有受支持的资源类型。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title)
|
||||
|
||||
assert meta.resource_type == expected
|
||||
@@ -277,7 +277,7 @@ def test_metainfo_music_round_trip_preserves_fields():
|
||||
|
||||
def test_python_subtitle_episode_range_fin_with_chinese_season():
|
||||
"""Python 兜底解析应识别副标题中 [01-26Fin] 格式的集数范围(#6103)。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="JoJos Bizarre Adventure S01 2012 1080i BluRay x264 FLAC 2.0-AnimeF@ADE",
|
||||
subtitle="JOJO的奇妙冒险 第一季 / JoJo's Bizarre Adventure [01-26Fin] [简繁字幕]",
|
||||
@@ -306,7 +306,7 @@ def test_subtitle_episode_range_fin_with_default_parser():
|
||||
|
||||
def test_python_subtitle_episode_range_fin_without_chinese_marker():
|
||||
"""副标题无中文季集标记时也应识别 [01-38Fin] 集数范围。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="Some Show S01 2022 1080p WEB-DL H264-GRP",
|
||||
subtitle="Some Show [01-38Fin]",
|
||||
@@ -319,7 +319,7 @@ def test_python_subtitle_episode_range_fin_without_chinese_marker():
|
||||
|
||||
def test_python_subtitle_episode_range_end_variant():
|
||||
"""END/完结 等完结标记变体同样应识别为集数范围。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta_end = MetaInfo(
|
||||
title="Some Show S01 2022 1080p WEB-DL H264-GRP",
|
||||
subtitle="Some Show 01-24 END",
|
||||
@@ -337,7 +337,7 @@ def test_python_subtitle_episode_range_end_variant():
|
||||
|
||||
def test_python_subtitle_year_range_not_treated_as_episodes():
|
||||
"""年份范围(如 2019-2020)不得误识别为集数。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="Some Collection 2020 1080p WEB-DL H264-GRP",
|
||||
subtitle="A Collection [2019-2020Fin]",
|
||||
@@ -350,7 +350,7 @@ def test_python_subtitle_year_range_not_treated_as_episodes():
|
||||
def test_python_subtitle_episode_range_fin_rejects_numeric_suffix():
|
||||
"""Python 兜底解析不得把带数字后缀的完结范围截断识别为集数。"""
|
||||
for subtitle in ("Some Show [01-26Fin]2", "Some Show 01-26Fin 2"):
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="Some Show S01 2022 1080p WEB-DL H264-GRP",
|
||||
subtitle=subtitle,
|
||||
@@ -390,7 +390,7 @@ def test_custom_words_episode_offset_supports_multiplication_expression():
|
||||
r"Ha.Ha.Ha.Ha.Ha.2026.S06E([0-1][0-9]).Part1 => 哈哈哈哈哈 (2020){[tmdbid=112732;type=tv]} S06E\1.Part1 && S06 <> .Part1 >> 2*EP-1"
|
||||
]
|
||||
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="Ha.Ha.Ha.Ha.Ha.2026.S06E03.Part1",
|
||||
custom_words=custom_words,
|
||||
@@ -408,7 +408,7 @@ def test_custom_words_episode_offset_supports_repeated_ep_expression():
|
||||
"""测试集数偏移表达式支持重复使用 EP 占位符。"""
|
||||
custom_words = ["旧名 => 新名 && 第 <> 集 >> EP+EP-1"]
|
||||
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title="旧名 第03集", custom_words=custom_words)
|
||||
|
||||
assert meta.name == "新名"
|
||||
@@ -420,7 +420,7 @@ def test_custom_words_episode_offset_rejects_implicit_ep_expression():
|
||||
"""测试集数偏移表达式不把 2EP 当作隐式乘法或字符串拼接。"""
|
||||
custom_words = ["旧名 => 新名 && 第 <> 集 >> 2EP"]
|
||||
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title="旧名 第03集", custom_words=custom_words)
|
||||
|
||||
assert meta.name == "新名"
|
||||
@@ -449,7 +449,7 @@ def test_custom_words_support_special_season_zero_parameter():
|
||||
"Test Show => 测试剧 {[tmdbid=12345;type=tv;s=0]}"
|
||||
]
|
||||
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title="Test Show 01", custom_words=custom_words)
|
||||
|
||||
assert meta.media_source == MediaSource.TMDB
|
||||
@@ -469,7 +469,7 @@ def test_find_metainfo_supports_episode_group_parameter():
|
||||
def test_find_metainfo_does_not_support_episode_group_alias():
|
||||
"""测试 e_group 不会被当作剧集组参数识别。"""
|
||||
group_id = "5ad0ec240e0a26303f00d84d"
|
||||
with patch("app.infrastructure.rust.find_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.find_metainfo", return_value=None):
|
||||
_, metainfo = find_metainfo(f"物语系列 {{[tmdbid=46195;type=tv;e_group={group_id};s=1]}}")
|
||||
assert metainfo["episode_group"] is None
|
||||
|
||||
@@ -483,7 +483,7 @@ def test_video_bit_extracted_for_video_title():
|
||||
|
||||
def test_special_season_zero_enables_whole_season_resource_parsing():
|
||||
"""只有 S00、没有集号的整季标题仍应识别后续编码信息。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title="Demo Show S00 X265 AAC")
|
||||
|
||||
assert meta.begin_season == 0
|
||||
@@ -537,7 +537,7 @@ def test_anime_parser_ignores_empty_and_invalid_season_values():
|
||||
|
||||
def test_hdr_vivid_effect_extracted_for_video_title():
|
||||
"""测试合并写法 HDRVivid 可识别为资源效果。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(
|
||||
title="Never-Ending Summer 2026 S01E18-S01E19 2160p WEB-DL 50Fps "
|
||||
"HDRVivid H265 10bit AAC-XXWEB"
|
||||
@@ -560,7 +560,7 @@ def test_video_bit_extracted_for_anime_title():
|
||||
|
||||
def test_streaming_platform_word_kept_in_movie_title():
|
||||
"""测试正式片名中的流媒体平台词不会被预置清理规则移除。"""
|
||||
with patch("app.infrastructure.rust.parse_metainfo", return_value=None):
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
meta = MetaInfo(title="Amazon Forever 2004 1080p WEB-DL")
|
||||
assert meta.name == "Amazon Forever"
|
||||
assert meta.year == "2004"
|
||||
@@ -590,7 +590,7 @@ def test_custom_identifier_uses_source_specific_id_and_returns_unified_identity(
|
||||
def test_generic_media_identity_is_not_custom_identifier_syntax():
|
||||
"""通用身份字段不得因 Rust 扩展版本差异被自定义识别词解析器接收。"""
|
||||
with patch(
|
||||
"app.infrastructure.rust.find_metainfo",
|
||||
"app.adapters.system.rust.find_metainfo",
|
||||
side_effect=AssertionError("通用身份标签必须绕过 Rust 解析器"),
|
||||
):
|
||||
_, metainfo = find_metainfo(
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_music_name_registry_supports_dynamic_pattern_and_parser(monkeypatch):
|
||||
)
|
||||
try:
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("自定义注册表不应调用 Rust")
|
||||
),
|
||||
@@ -100,7 +100,7 @@ def test_music_name_registry_same_name_replacement_falls_back_to_python(monkeypa
|
||||
MusicNameRegistry.register_parser(replacement, replace=True)
|
||||
try:
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("同名替换解析器时不应调用 Rust")
|
||||
),
|
||||
@@ -139,7 +139,7 @@ def test_parse_query_uses_rust_and_maps_music_fields(monkeypatch):
|
||||
return parsed
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
parse_metamusic,
|
||||
)
|
||||
|
||||
@@ -155,7 +155,7 @@ def test_parse_query_uses_rust_and_maps_music_fields(monkeypatch):
|
||||
def test_apply_title_preserves_existing_fields_from_rust(monkeypatch):
|
||||
"""Rust 解析只应补充空字段,不覆盖标签或文件后缀证据。"""
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
lambda *_args, **_kwargs: {
|
||||
"title": "Rust 曲名",
|
||||
"artists": ["Rust 歌手"],
|
||||
@@ -202,7 +202,7 @@ def test_apply_title_preserves_existing_fields_from_rust(monkeypatch):
|
||||
def test_apply_title_falls_back_when_rust_returns_none(monkeypatch):
|
||||
"""Rust wrapper 不可用时应完整执行现有 Python 命名解析。"""
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
@@ -231,7 +231,7 @@ def test_metainfo_audio_suffix_remains_authoritative_with_rust(monkeypatch):
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
parse_metamusic,
|
||||
)
|
||||
filename = "S H E - S H E十七音乐会 2018 WEB-DL 1080P AVC AAC-FHDMv.flac"
|
||||
@@ -263,7 +263,7 @@ def test_metainfo_path_uses_rust_once_and_keeps_python_directory_context(
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.infrastructure.rust.parse_metamusic",
|
||||
"app.adapters.system.rust.parse_metamusic",
|
||||
parse_metamusic,
|
||||
)
|
||||
path = MetaInfoPath(
|
||||
|
||||
@@ -5,8 +5,8 @@ import pytest
|
||||
from webauthn.helpers.exceptions import InvalidRegistrationResponse
|
||||
|
||||
from app.api.endpoints import mfa as mfa_endpoint
|
||||
from app.security import passkey as passkey_helper
|
||||
from app.security.passkey import (
|
||||
from app.application.security import passkey as passkey_helper
|
||||
from app.application.security.passkey import (
|
||||
PassKeyHelper,
|
||||
PassKeyRegistrationOriginMismatchError,
|
||||
PassKeyRegistrationVerificationError,
|
||||
|
||||
@@ -7,7 +7,7 @@ from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.api.endpoints import mfa as mfa_endpoint
|
||||
from app.security.passkey import PasskeyChallengeStore
|
||||
from app.application.security.passkey import PasskeyChallengeStore
|
||||
|
||||
|
||||
def _request() -> Request:
|
||||
|
||||
@@ -3,8 +3,8 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.services.history import (
|
||||
from app.runtime.config import settings
|
||||
from app.application.history import (
|
||||
clear_transfer_failures,
|
||||
failed_retry_count,
|
||||
record_transfer_failure,
|
||||
|
||||
@@ -4,12 +4,12 @@ from unittest.mock import MagicMock
|
||||
|
||||
from watchfiles import Change
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.monitor import LocalDirectoryWatcher, Monitor
|
||||
from app.monitor.dispatcher import TransferDispatcher
|
||||
from app.monitor.recovery import RecoveryExecutor
|
||||
from app.monitor.syslimits import decide_monitor_mode
|
||||
from app.infrastructure.system import SystemUtils
|
||||
from app.adapters.system.host import SystemUtils
|
||||
|
||||
|
||||
def _build_monitor(handle_file: MagicMock = None):
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from watchfiles import Change
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.monitor.watcher import LocalDirectoryWatcher
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.services.audio import AudioMetadataHelper
|
||||
from app.services.directory import DirectoryHelper
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
from app.application.directory import DirectoryHelper
|
||||
from app.modules.filemanager import FileManagerModule
|
||||
from app.modules.filemanager.transhandler import TransHandler
|
||||
from app.schemas import FileItem, TransferDirectoryConf
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
from app import schemas
|
||||
from app.domain.context import MusicInfo
|
||||
from app.services.mediaserver import MusicMediaServerHelper
|
||||
from app.application.mediaserver import MusicMediaServerHelper
|
||||
from app.modules.emby import EmbyModule
|
||||
from app.modules.emby.emby import Emby
|
||||
from app.modules.jellyfin import JellyfinModule
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
from app.chain import ChainBase
|
||||
from app.chain.media import MediaChain
|
||||
from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.platform.events import Event
|
||||
from app.runtime.events import Event
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.schemas.types import ChainEventType, MediaSource, MediaType
|
||||
|
||||
@@ -306,7 +306,7 @@ def test_chain_explicit_music_source_bypasses_generic_module_dispatch(monkeypatc
|
||||
monkeypatch.setattr(chain.eventmanager, "check", Mock(return_value=False))
|
||||
|
||||
with patch(
|
||||
"app.integrations.server.MoviePilotServerHelper.report_recognize_share"
|
||||
"app.adapters.external.server.MoviePilotServerHelper.report_recognize_share"
|
||||
):
|
||||
result = chain.recognize_media(
|
||||
mtype=MediaType.MUSIC,
|
||||
@@ -580,7 +580,7 @@ def test_chain_recognize_media_returns_musicinfo_and_reports_share():
|
||||
with patch.object(
|
||||
chain, "recognize_music_from_source", return_value=expected
|
||||
), patch(
|
||||
"app.integrations.server.MoviePilotServerHelper.report_recognize_share"
|
||||
"app.adapters.external.server.MoviePilotServerHelper.report_recognize_share"
|
||||
) as report_mock:
|
||||
result = chain.recognize_media(meta=MetaMusic(title="晴天"))
|
||||
|
||||
@@ -598,7 +598,7 @@ def test_chain_async_recognize_media_returns_musicinfo_and_reports_share():
|
||||
"async_recognize_music_from_source",
|
||||
AsyncMock(return_value=expected),
|
||||
), patch(
|
||||
"app.integrations.server.MoviePilotServerHelper.async_report_recognize_share",
|
||||
"app.adapters.external.server.MoviePilotServerHelper.async_report_recognize_share",
|
||||
AsyncMock(),
|
||||
) as report_mock:
|
||||
async def runner():
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from app.chain.scraping import ScrapingChain, ScrapingConfig, _MusicScrapeFileResult
|
||||
from app.domain.context import MUSIC_ENTITY_ALBUM, MusicAlbumInfo, MusicInfo, MusicLyrics
|
||||
from app.platform.events import Event
|
||||
from app.runtime.events import Event
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.schemas import FileItem
|
||||
from app.schemas.types import EventType, ScrapingPolicy
|
||||
|
||||
@@ -6,10 +6,10 @@ from jinja2 import Template
|
||||
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.transfer import JobManager, TransferChain
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.context import MusicInfo
|
||||
from app.messaging.message import TemplateHelper
|
||||
from app.application.messaging.message import TemplateHelper
|
||||
from app.schemas.file import FileItem
|
||||
from app.schemas.system import TransferDirectoryConf
|
||||
from app.schemas.transfer import TransferInfo, TransferTask, TransferTorrent
|
||||
@@ -280,7 +280,7 @@ def test_restore_album_context_keeps_album_identity_and_track_specific_tags(tmp_
|
||||
audio_file = tmp_path / "03. 晴天.flac"
|
||||
audio_file.write_bytes(b"fake-flac")
|
||||
|
||||
from app.services.audio import AudioMetadataHelper
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
|
||||
monkeypatch.setattr(
|
||||
AudioMetadataHelper,
|
||||
@@ -325,7 +325,7 @@ def test_restore_music_context_uses_file_title_over_subscription_title(tmp_path,
|
||||
audio_file = tmp_path / "07.幸福.flac"
|
||||
audio_file.write_bytes(b"fake-flac")
|
||||
|
||||
from app.services.audio import AudioMetadataHelper
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
|
||||
monkeypatch.setattr(
|
||||
AudioMetadataHelper,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.platform.config import settings
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.modules.musicbrainz import MusicBrainzModule
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app import schemas
|
||||
from app.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.domain.context import MusicInfo
|
||||
from app.modules.navidrome import NavidromeModule
|
||||
from app.modules.navidrome.navidrome import Navidrome
|
||||
|
||||
@@ -20,7 +20,7 @@ import pytest
|
||||
from app.domain.context import MUSIC_ENTITY_ALBUM, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.messaging.message import MessageTemplateHelper, TemplateContextBuilder, TemplateHelper
|
||||
from app.application.messaging.message import MessageTemplateHelper, TemplateContextBuilder, TemplateHelper
|
||||
from app.schemas.message import Notification
|
||||
from app.schemas.types import ContentType, SystemConfigKey
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from app.foundation.object import ObjectUtils
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
|
||||
|
||||
class ObjectUtilsTest(TestCase):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.infrastructure.package import (
|
||||
from app.adapters.system.package import (
|
||||
PackageInstallRequest,
|
||||
build_package_install_env,
|
||||
build_package_install_strategies,
|
||||
@@ -106,7 +106,7 @@ def test_build_strategies_uses_pip_only_when_uv_missing(tmp_path):
|
||||
config_dir=tmp_path / "config",
|
||||
)
|
||||
|
||||
with patch("app.infrastructure.package._find_uv", return_value=None):
|
||||
with patch("app.adapters.system.package._find_uv", return_value=None):
|
||||
strategies = build_package_install_strategies(request)
|
||||
|
||||
assert [strategy.strategy_name for strategy in strategies] == ["pip:直连"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.platform.cache import TTLCache
|
||||
from app.security.passkey import PasskeyChallengeStore
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.application.security.passkey import PasskeyChallengeStore
|
||||
|
||||
|
||||
def setup_function():
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any, Iterator
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from app.api.endpoints.plugin import plugin_history
|
||||
from app.api.endpoints.plugin import plugin_releases
|
||||
from app.api.endpoints.plugin import reset_plugin
|
||||
from app.api.endpoints.system import sync_plugin_market_from_wiki
|
||||
from app.platform.config import settings
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.schemas.event import PluginDataResetEventData
|
||||
from app.schemas.types import ChainEventType
|
||||
from app.foundation.singleton import Singleton
|
||||
@@ -242,7 +242,7 @@ def test_plugin_releases_force_uses_cached_release_response_and_schedules_refres
|
||||
"""
|
||||
手动刷新时 package 元数据仍强刷,但 Release 明细先读缓存并后台刷新,避免弹窗阻塞。
|
||||
"""
|
||||
from app.platform.cache import is_fresh
|
||||
from app.runtime.cache import is_fresh
|
||||
|
||||
market_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
@@ -380,7 +380,7 @@ def test_sync_plugin_market_from_wiki_merges_and_deduplicates_repos():
|
||||
patch("app.api.endpoints.system.AsyncRequestUtils", return_value=request_utils),
|
||||
patch("app.api.endpoints.system.settings.PLUGIN_MARKET", "https://github.com/local/existing"),
|
||||
patch(
|
||||
"app.platform.config.Settings.update_setting",
|
||||
"app.runtime.config.Settings.update_setting",
|
||||
autospec=True,
|
||||
return_value=(True, ""),
|
||||
) as update_setting,
|
||||
@@ -458,7 +458,7 @@ def test_delete_plugin_config_can_force_delete_after_plugin_is_stopped():
|
||||
Singleton._instances.pop((PluginManager, (), frozenset()), None)
|
||||
manager = PluginManager()
|
||||
|
||||
with patch("app.extensions.plugin_manager.SystemConfigOper") as system_config_oper:
|
||||
with patch("app.runtime.extensions.plugin_manager.SystemConfigOper") as system_config_oper:
|
||||
system_config_oper.return_value.delete.return_value = True
|
||||
assert manager.delete_plugin_config("DemoPlugin", force=True) is True
|
||||
|
||||
@@ -474,7 +474,7 @@ def test_delete_plugin_data_can_force_delete_after_plugin_is_stopped():
|
||||
manager = PluginManager()
|
||||
calls = []
|
||||
|
||||
with patch("app.extensions.plugin_manager.PluginDataOper") as plugin_data_oper:
|
||||
with patch("app.runtime.extensions.plugin_manager.PluginDataOper") as plugin_data_oper:
|
||||
plugin_data_oper.return_value.del_data.side_effect = lambda pid: calls.append(pid)
|
||||
assert manager.delete_plugin_data("DemoPlugin", force=True) is True
|
||||
|
||||
|
||||
+161
-155
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ import pytest
|
||||
from packaging.version import Version
|
||||
from watchfiles import Change
|
||||
|
||||
from app.platform.events import Event, eventmanager
|
||||
from app.extensions.plugin_manager import PluginManager
|
||||
from app.integrations.market import PluginHelper
|
||||
from app.runtime.events import Event, eventmanager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.adapters.external.market import PluginHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import EventType, SystemConfigKey
|
||||
from app.foundation.singleton import Singleton
|
||||
@@ -66,9 +66,9 @@ def _configure_local_watcher(
|
||||
ROOT_PATH=tmp_path,
|
||||
VERSION_FLAG="v2",
|
||||
)
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.integrations.market.settings", settings_stub)
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.watch", lambda *_args, **_kwargs: iter([changes]))
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.adapters.external.market.settings", settings_stub)
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.watch", lambda *_args, **_kwargs: iter([changes]))
|
||||
|
||||
|
||||
def _set_running_render_mode(
|
||||
@@ -119,11 +119,11 @@ def test_dev_local_plugin_candidate_keeps_hot_sync_allowed_when_system_version_l
|
||||
repo_path, source_file = _build_local_plugin_repo(tmp_path)
|
||||
runtime_dir = tmp_path / "app" / "plugins" / "demoplugin"
|
||||
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.settings", SimpleNamespace(DEV=True, ROOT_PATH=tmp_path))
|
||||
monkeypatch.setattr("app.integrations.market.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.settings", SimpleNamespace(DEV=True, ROOT_PATH=tmp_path))
|
||||
monkeypatch.setattr("app.adapters.external.market.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
|
||||
@@ -145,8 +145,8 @@ def test_local_plugin_candidate_keeps_system_version_gate_outside_dev(
|
||||
"""非 DEV 本地候选继续受主系统版本门禁保护,避免自动热加载绕过安装约束。"""
|
||||
repo_path, source_file = _build_local_plugin_repo(tmp_path)
|
||||
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.settings", SimpleNamespace(DEV=False, ROOT_PATH=tmp_path))
|
||||
monkeypatch.setattr("app.integrations.market.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.settings", SimpleNamespace(DEV=False, ROOT_PATH=tmp_path))
|
||||
monkeypatch.setattr("app.adapters.external.market.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||
|
||||
candidate = plugin_manager._get_local_plugin_candidate_from_path(source_file)
|
||||
@@ -171,11 +171,11 @@ def test_local_plugin_sync_without_candidate_respects_system_version_gate(
|
||||
PLUGIN_LOCAL_REPO_PATHS=str(repo_path),
|
||||
)
|
||||
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.integrations.market.settings", settings_stub)
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.adapters.external.market.settings", settings_stub)
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
|
||||
@@ -205,7 +205,7 @@ def test_local_federated_asset_batch_syncs_once_without_python_reload(
|
||||
_set_running_render_mode(plugin_manager, "vue", "dist/assets")
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.11"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
sync_spy = Mock(wraps=plugin_manager._sync_local_plugin_if_installed)
|
||||
@@ -332,10 +332,10 @@ def test_local_federated_asset_reads_running_render_mode_for_each_batch(
|
||||
ROOT_PATH=tmp_path,
|
||||
VERSION_FLAG="v2",
|
||||
)
|
||||
monkeypatch.setattr("app.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.integrations.market.settings", settings_stub)
|
||||
monkeypatch.setattr("app.runtime.extensions.plugin_manager.settings", settings_stub)
|
||||
monkeypatch.setattr("app.adapters.external.market.settings", settings_stub)
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.watch",
|
||||
"app.runtime.extensions.plugin_manager.watch",
|
||||
lambda *_args, **_kwargs: iter([
|
||||
{(Change.modified, str(source_dir / "dist" / "assets" / "remoteEntry.js"))},
|
||||
{(Change.modified, str(next_entry))},
|
||||
@@ -462,7 +462,7 @@ def test_local_python_change_still_syncs_and_reloads_plugin(
|
||||
)
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.11"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
sync_spy = Mock(wraps=plugin_manager._sync_local_plugin_if_installed)
|
||||
@@ -494,7 +494,7 @@ def test_local_python_change_rejects_root_federated_path_and_still_reloads(
|
||||
_set_running_render_mode(plugin_manager, "vue", dist_path)
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.11"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
sync_spy = Mock(wraps=plugin_manager._sync_local_plugin_if_installed)
|
||||
@@ -530,7 +530,7 @@ def test_local_python_and_federated_changes_share_one_batch_sync(
|
||||
_set_running_render_mode(plugin_manager, "vue", "dist/assets")
|
||||
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.11"))
|
||||
monkeypatch.setattr(
|
||||
"app.extensions.plugin_manager.SystemConfigOper.get",
|
||||
"app.runtime.extensions.plugin_manager.SystemConfigOper.get",
|
||||
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||
)
|
||||
sync_spy = Mock(wraps=plugin_manager._sync_local_plugin_if_installed)
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.integrations.market import extract_plugin_market_repos_from_wiki
|
||||
from app.adapters.external.market import extract_plugin_market_repos_from_wiki
|
||||
from scripts.generate_plugin_market_default import (
|
||||
OFFICIAL_PLUGIN_MARKET,
|
||||
_generate_plugin_market_default,
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import HTTPException
|
||||
|
||||
from app import schemas
|
||||
from app.api.endpoints.plugin import plugin_rating, plugin_ratings, rate_plugin
|
||||
from app.integrations.server import MoviePilotServerHelper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
|
||||
|
||||
def test_server_helper_uses_plugin_rating_endpoints() -> None:
|
||||
@@ -14,7 +14,7 @@ def test_server_helper_uses_plugin_rating_endpoints() -> None:
|
||||
|
||||
async def run_scenario() -> None:
|
||||
with (
|
||||
patch("app.integrations.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"),
|
||||
patch("app.adapters.external.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"),
|
||||
patch.object(
|
||||
MoviePilotServerHelper,
|
||||
"_async_get",
|
||||
|
||||
+15
-11
@@ -5,7 +5,7 @@ from app.sdk.config import settings
|
||||
from app.sdk.events import Event, eventmanager
|
||||
from app.sdk.logging import logger
|
||||
from app.sdk.media import MediaInfo, MetaBase, MetaInfo, MetaMusic, NfoReader
|
||||
from app.sdk.network import RequestUtils
|
||||
from app.sdk.network import RequestUtils, RssHelper, SitesHelper
|
||||
from app.sdk.plugins import ModuleManager, PluginManager
|
||||
from app.sdk.services import NotificationHelper
|
||||
from app.sdk.utilities import StringUtils as UtilityStringUtils
|
||||
@@ -21,16 +21,18 @@ def test_sdk_exports_canonical_plugin_interfaces():
|
||||
from app.domain.scraper import NfoReader as CanonicalNfoReader
|
||||
from app.domain.string import StringUtils as CanonicalStringUtils
|
||||
from app.foundation.crypto import CryptoJsUtils
|
||||
from app.extensions.module_manager import ModuleManager as CanonicalModuleManager
|
||||
from app.extensions.plugin_manager import PluginManager as CanonicalPluginManager
|
||||
from app.foundation.http import RequestUtils as CanonicalRequestUtils
|
||||
from app.platform.cache import Cache as CanonicalCache
|
||||
from app.platform.cache import cached as canonical_cached
|
||||
from app.platform.config import settings as canonical_settings
|
||||
from app.platform.events import Event as CanonicalEvent
|
||||
from app.platform.events import eventmanager as canonical_eventmanager
|
||||
from app.platform.log import logger as canonical_logger
|
||||
from app.services.notification import NotificationHelper as CanonicalNotificationHelper
|
||||
from app.runtime.extensions.module_manager import ModuleManager as CanonicalModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager as CanonicalPluginManager
|
||||
from app.adapters.network.http import RequestUtils as CanonicalRequestUtils
|
||||
from app.application.rss import RssHelper as CanonicalRssHelper
|
||||
from app.application.site.sites import SitesHelper as CanonicalSitesHelper
|
||||
from app.runtime.cache import Cache as CanonicalCache
|
||||
from app.runtime.cache import cached as canonical_cached
|
||||
from app.runtime.config import settings as canonical_settings
|
||||
from app.runtime.events import Event as CanonicalEvent
|
||||
from app.runtime.events import eventmanager as canonical_eventmanager
|
||||
from app.runtime.log import logger as canonical_logger
|
||||
from app.application.notification import NotificationHelper as CanonicalNotificationHelper
|
||||
|
||||
assert Cache is CanonicalCache
|
||||
assert cached is canonical_cached
|
||||
@@ -44,6 +46,8 @@ def test_sdk_exports_canonical_plugin_interfaces():
|
||||
assert MetaMusic is CanonicalMetaMusic
|
||||
assert NfoReader is CanonicalNfoReader
|
||||
assert RequestUtils is CanonicalRequestUtils
|
||||
assert RssHelper is CanonicalRssHelper
|
||||
assert SitesHelper is CanonicalSitesHelper
|
||||
assert NotificationHelper is CanonicalNotificationHelper
|
||||
assert UtilityStringUtils is CanonicalStringUtils
|
||||
assert decrypt is CryptoJsUtils.decrypt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from app.platform.config import Settings
|
||||
from app.runtime.config import Settings
|
||||
|
||||
|
||||
class PostgreSQLSocketConfigTests(unittest.TestCase):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.platform.config import Settings
|
||||
from app.runtime.config import Settings
|
||||
|
||||
|
||||
PROXY_ENV_NAMES = (
|
||||
|
||||
@@ -19,9 +19,9 @@ def _load_qbittorrent_modules():
|
||||
modules_module.__path__ = []
|
||||
qbittorrent_package_module = types.ModuleType("app.modules.qbittorrent")
|
||||
qbittorrent_package_module.__path__ = []
|
||||
log_module = types.ModuleType("app.platform.log")
|
||||
cache_module = types.ModuleType("app.platform.cache")
|
||||
config_module = types.ModuleType("app.platform.config")
|
||||
log_module = types.ModuleType("app.runtime.log")
|
||||
cache_module = types.ModuleType("app.runtime.cache")
|
||||
config_module = types.ModuleType("app.runtime.config")
|
||||
metainfo_module = types.ModuleType("app.domain.metainfo")
|
||||
schemas_module = types.ModuleType("app.schemas")
|
||||
schema_types_module = types.ModuleType("app.schemas.types")
|
||||
@@ -160,10 +160,10 @@ def _load_qbittorrent_modules():
|
||||
stub_modules = {
|
||||
"app": app_module,
|
||||
"app.core": core_module,
|
||||
"app.platform.cache": cache_module,
|
||||
"app.platform.config": config_module,
|
||||
"app.runtime.cache": cache_module,
|
||||
"app.runtime.config": config_module,
|
||||
"app.domain.metainfo": metainfo_module,
|
||||
"app.platform.log": log_module,
|
||||
"app.runtime.log": log_module,
|
||||
"app.modules": modules_module,
|
||||
"app.modules.qbittorrent": qbittorrent_package_module,
|
||||
"app.schemas": schemas_module,
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
|
||||
from app.chain.recommend import RecommendChain
|
||||
from app.platform.cache import TTLCache
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.domain.context import MusicInfo
|
||||
from app.schemas.types import MUSIC_ENTITY_ALBUM
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import requests
|
||||
|
||||
from app.foundation import http as http_module
|
||||
from app.foundation.http import AsyncRequestUtils, RequestUtils
|
||||
from app.adapters.network import http as http_module
|
||||
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user