refactor: reorganize backend module boundaries

This commit is contained in:
jxxghp
2026-08-14 19:53:33 +08:00
parent fe0b444ceb
commit 369d7d6448
584 changed files with 2423 additions and 2275 deletions
+4 -4
View File
@@ -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:
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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:
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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]")
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+100 -60
View File
@@ -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
+2 -2
View File
@@ -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"
+11 -11
View File
@@ -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)
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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,
+8 -8
View File
@@ -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())
+4 -4
View File
@@ -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,
}
+1 -1
View File
@@ -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 -1
View File
@@ -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:
+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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 -1
View File
@@ -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):
+4 -4
View File
@@ -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 -13
View File
@@ -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()
+10 -10
View File
@@ -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,
+7 -7
View File
@@ -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 -2
View File
@@ -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"
+1 -1
View File
@@ -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 -1
View File
@@ -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():
+3 -3
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
from app.foundation.jieba import cut
from app.foundation.text import cut
def test_cut_accepts_legacy_hmm_argument():
+6 -6
View File
@@ -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)
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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 -2
View File
@@ -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):
+4 -4
View File
@@ -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 = {
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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(
+7 -7
View File
@@ -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,
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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():
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
from unittest import TestCase
from app.foundation.object import ObjectUtils
from app.foundation.reflection import ObjectUtils
class ObjectUtilsTest(TestCase):
+2 -2
View File
@@ -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:直连"]
+2 -2
View File
@@ -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():
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -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)
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
import unittest
from app.platform.config import Settings
from app.runtime.config import Settings
class PostgreSQLSocketConfigTests(unittest.TestCase):
+1 -1
View File
@@ -1,4 +1,4 @@
from app.platform.config import Settings
from app.runtime.config import Settings
PROXY_ENV_NAMES = (
+6 -6
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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