From 13fba96fd82f4e94dbda8cff5dc635966e06fa67 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 29 Aug 2026 08:14:21 +0800 Subject: [PATCH] refactor(runtime): govern dependency module naming --- .github/workflows/dependency-compat.yml | 4 +- .../workflows/native-dependency-update.yml | 10 ++- app/adapters/external/market.py | 2 +- app/adapters/system/package.py | 2 +- app/adapters/system/plugin/package.py | 10 +-- app/application/plugin/install.py | 2 +- app/runtime/dependencies/__init__.py | 1 + .../native.py} | 0 .../profile.py} | 1 - docker/update.sh | 19 ++++- docs/architecture-optimization-checklist.md | 4 +- docs/architecture-overview.md | 8 +- docs/architecture-refactor-roadmap.md | 12 ++- docs/rules/05-architecture.md | 9 ++- docs/rules/07-naming-conventions.md | 14 +++- docs/v3t-runtime-governance.md | 2 +- scripts/verify_runtime_profile.py | 2 +- .../architecture/dependency-baseline.json | 25 +++--- .../official-plugin-baseline.json | 4 +- .../fixtures/architecture/ruff-baseline.json | 7 -- tests/test_architecture_ci.py | 6 +- tests/test_architecture_dependencies.py | 39 +++++++++ tests/test_docker_bootstrap.py | 79 ++++++++++++++++++- tests/test_native_dependency_activation.py | 2 +- tests/test_plugin_helper.py | 2 +- tests/test_plugin_install_command.py | 2 +- tests/test_plugin_package_manager.py | 2 +- tests/test_runtime_dependencies.py | 2 +- 28 files changed, 216 insertions(+), 56 deletions(-) create mode 100644 app/runtime/dependencies/__init__.py rename app/runtime/{native_dependencies.py => dependencies/native.py} (100%) rename app/runtime/{dependencies.py => dependencies/profile.py} (99%) diff --git a/.github/workflows/dependency-compat.yml b/.github/workflows/dependency-compat.yml index 7fd713a55..a66e1429d 100644 --- a/.github/workflows/dependency-compat.yml +++ b/.github/workflows/dependency-compat.yml @@ -10,7 +10,7 @@ on: - 'app/__init__.py' - 'app/doctor/dependencies.py' - 'app/foundation/environment.py' - - 'app/runtime/dependencies.py' + - 'app/runtime/dependencies/profile.py' - 'scripts/verify_runtime_profile.py' - 'docker/Dockerfile' - 'docker/**' @@ -24,7 +24,7 @@ on: - 'app/__init__.py' - 'app/doctor/dependencies.py' - 'app/foundation/environment.py' - - 'app/runtime/dependencies.py' + - 'app/runtime/dependencies/profile.py' - 'scripts/verify_runtime_profile.py' - 'docker/Dockerfile' - 'docker/**' diff --git a/.github/workflows/native-dependency-update.yml b/.github/workflows/native-dependency-update.yml index 2ada150cb..44aa6123a 100644 --- a/.github/workflows/native-dependency-update.yml +++ b/.github/workflows/native-dependency-update.yml @@ -8,7 +8,10 @@ on: - 'app/adapters/external/market.py' - 'app/adapters/system/host.py' - 'app/adapters/system/package.py' - - 'app/runtime/dependencies.py' + - 'app/adapters/system/plugin/package.py' + - 'app/application/plugin/install.py' + - 'app/runtime/dependencies/native.py' + - 'app/runtime/dependencies/profile.py' - 'scripts/probe_native_dependency_update.py' - '.github/workflows/native-dependency-update.yml' push: @@ -18,7 +21,10 @@ on: - 'app/adapters/external/market.py' - 'app/adapters/system/host.py' - 'app/adapters/system/package.py' - - 'app/runtime/dependencies.py' + - 'app/adapters/system/plugin/package.py' + - 'app/application/plugin/install.py' + - 'app/runtime/dependencies/native.py' + - 'app/runtime/dependencies/profile.py' - 'scripts/probe_native_dependency_update.py' - '.github/workflows/native-dependency-update.yml' workflow_dispatch: diff --git a/app/adapters/external/market.py b/app/adapters/external/market.py index 217e2188c..11d2f126f 100644 --- a/app/adapters/external/market.py +++ b/app/adapters/external/market.py @@ -50,7 +50,7 @@ from app.foundation.singleton import WeakSingleton from app.foundation.url import UrlUtils from app.foundation.version import compare_version from app.runtime.cache import cached, is_fresh -from app.runtime.dependencies import ( +from app.runtime.dependencies.profile import ( iter_runtime_profile_requirement_strings, iter_runtime_requirement_strings, runtime_excluded_dependency_pairs, diff --git a/app/adapters/system/package.py b/app/adapters/system/package.py index dd16c9037..4d7864728 100644 --- a/app/adapters/system/package.py +++ b/app/adapters/system/package.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from pathlib import Path from urllib.parse import urlsplit, urlunsplit -from app.runtime.dependencies import runtime_sync_arguments +from app.runtime.dependencies.profile import runtime_sync_arguments @dataclass(frozen=True) diff --git a/app/adapters/system/plugin/package.py b/app/adapters/system/plugin/package.py index ee94f7189..683578fab 100644 --- a/app/adapters/system/plugin/package.py +++ b/app/adapters/system/plugin/package.py @@ -12,16 +12,16 @@ from typing import Any, Optional, cast from app.adapters.external.market import PluginHelper as _PluginHelper from app.adapters.system.host import SystemUtils -from app.runtime.execution import ( - run_in_threadpool_to_completion as _await_thread_operation, -) -from app.runtime.log import logger -from app.runtime.native_dependencies import ( +from app.runtime.dependencies.native import ( LoadedNativeDependencySnapshot, NativeDependencyChange, capture_loaded_native_dependencies, detect_changed_native_dependencies, ) +from app.runtime.execution import ( + run_in_threadpool_to_completion as _await_thread_operation, +) +from app.runtime.log import logger from app.runtime.settings import get_runtime_setting diff --git a/app/application/plugin/install.py b/app/application/plugin/install.py index 2f8509dd0..4d933cdf2 100644 --- a/app/application/plugin/install.py +++ b/app/application/plugin/install.py @@ -17,9 +17,9 @@ from app.application.plugin.transaction import ( PluginInstallationRecord, PluginPersistenceService, ) +from app.runtime.dependencies.native import NativeDependencyChange from app.runtime.execution import await_task_to_terminal from app.runtime.log import logger -from app.runtime.native_dependencies import NativeDependencyChange from app.schemas.exception import ( PersistenceUnavailableError, PluginMutationRejectedError, diff --git a/app/runtime/dependencies/__init__.py b/app/runtime/dependencies/__init__.py new file mode 100644 index 000000000..d19e00318 --- /dev/null +++ b/app/runtime/dependencies/__init__.py @@ -0,0 +1 @@ +"""运行依赖 profile 与原生载荷激活边界。""" diff --git a/app/runtime/native_dependencies.py b/app/runtime/dependencies/native.py similarity index 100% rename from app/runtime/native_dependencies.py rename to app/runtime/dependencies/native.py diff --git a/app/runtime/dependencies.py b/app/runtime/dependencies/profile.py similarity index 99% rename from app/runtime/dependencies.py rename to app/runtime/dependencies/profile.py index 44180ed69..e37a9201b 100644 --- a/app/runtime/dependencies.py +++ b/app/runtime/dependencies/profile.py @@ -11,7 +11,6 @@ from packaging.utils import canonicalize_name from app.foundation.environment import is_free_threaded_runtime - RUNTIME_STANDARD_GROUP = "runtime-standard" RUNTIME_FREE_THREADED_GROUP = "runtime-free-threaded" diff --git a/docker/update.sh b/docker/update.sh index 42df18fe8..a4314b767 100644 --- a/docker/update.sh +++ b/docker/update.sh @@ -151,6 +151,7 @@ function update_pending_state() { function sync_project_dependencies_for() { local project_dir="$1" + local legacy_runtime_selector local runtime_group local runtime_selector local -a uv_cmd=( @@ -164,9 +165,21 @@ function sync_project_dependencies_for() { ) runtime_group="" if grep -Eq '^runtime-(standard|free-threaded)[[:space:]]*=' "${project_dir}/pyproject.toml"; then - runtime_selector="${project_dir}/app/runtime/dependencies.py" - [ -f "${runtime_selector}" ] || return 1 - runtime_group="$("${VENV_PATH}/bin/python3" "${runtime_selector}")" || return 1 + runtime_selector="${project_dir}/app/runtime/dependencies/profile.py" + legacy_runtime_selector="${project_dir}/app/runtime/dependencies.py" + if [ -f "${runtime_selector}" ]; then + runtime_group="$( + PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ + "${VENV_PATH}/bin/python3" -m app.runtime.dependencies.profile + )" || return 1 + elif [ -f "${legacy_runtime_selector}" ]; then + runtime_group="$( + PYTHONPATH="${project_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ + "${VENV_PATH}/bin/python3" "${legacy_runtime_selector}" + )" || return 1 + else + return 1 + fi [ -n "${runtime_group}" ] || return 1 fi if [ -n "${runtime_group}" ]; then diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md index 30e133bc1..f115086b0 100644 --- a/docs/architecture-optimization-checklist.md +++ b/docs/architecture-optimization-checklist.md @@ -70,7 +70,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 932 / 7,886 | `dependency-baseline.json` 当前快照 | +| 宿主 Python 模块 / 内部依赖边 | 933 / 7,890 | `dependency-baseline.json` 当前快照 | | 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | @@ -79,7 +79,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 | | 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 | | 全量 mypy 历史债务 | 10,982 / 599 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 | -| Ruff 历史诊断 | 671 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | +| Ruff 历史诊断 | 667 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | | 覆盖率低水位 | Application 80.75%,Domain 79.32% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | ### 3.3 热点文件 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 89cae6917..68ddf7555 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -192,7 +192,7 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P |---|---|---| | `app/foundation/` | 无状态、无配置、无 I/O 的底层原语:反射/动态导入、加密、DOM、单例、文本、URL、版本比较 | `reflection.py`、`crypto.py`、`singleton.py` | | `app/domain/` | 纯 MoviePilot 业务语义:媒体上下文、识别解析、站点状态解释、磁力语义、NFO 刮削 | `context.py`、`metainfo.py`、`meta/`、`scraper.py` | -| `app/runtime/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、任务所有权、执行/关联上下文、并发、调度、限流、本地化、GC、重启状态 | `config.py`、`events.py`、`event/`、`tasks.py`、`execution.py`、`correlation.py`、`log.py`、`cache.py` | +| `app/runtime/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、运行依赖 profile 与原生载荷激活检测、任务所有权、执行/关联上下文、并发、调度、限流、本地化、GC、重启状态 | `config.py`、`events.py`、`event/`、`dependencies/`、`tasks.py`、`execution.py`、`correlation.py`、`log.py`、`cache.py` | | `app/runtime/extensions/` | 模块 / 插件 / 配置化服务 / 托管资源的发现、注册与生命周期适配;旧管理器文件保留稳定 ABI 门面,具体实现拆在主题子包 | `module_manager.py`、`plugin_manager.py`、`plugin/` | | `app/runtime/compat/` | 仅标准库的精确旧模块、包与符号导入路由;不是业务实现,也不是通用 re-export 层 | `manifest.py`、`imports.py` | | `app/adapters/network/` | 通用 HTTP、浏览器、DNS、Cloudflare、IP 传输机制 | `http.py`、`browser.py` | @@ -220,6 +220,8 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P | `app/workflow/` | 工作流引擎 | — | | `app/plugins/` | 插件运行时副本/覆盖目录,由插件管理器加载;不是官方插件源码或宿主架构实现,架构审计以插件仓库与宿主边界为准 | — | +`app/runtime/dependencies/` 是一个同名能力包:`profile.py` 只负责解释器 ABI 对应的依赖组,`native.py` 只负责已加载原生发行包的快照与变更检测。包根不重复导出实现,宿主直接导入职责子模块;旧的平级 `dependencies.py` 与 `native_dependencies.py` 已退出规范实现。 + --- ## 四、启动与生命周期 @@ -726,8 +728,8 @@ flowchart LR | 指标 | 当前值 | |---|---:| -| Python 模块 | 932 | -| 内部导入边 | 7,886 | +| Python 模块 | 933 | +| 内部导入边 | 7,890 | | 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) | | Application / Chain 具体 Adapter 直连 | 0 / 0 | | Direct egress | 55(债务已清零,55 条精确 containment) | diff --git a/docs/architecture-refactor-roadmap.md b/docs/architecture-refactor-roadmap.md index e4e59cb15..b92e4a6cb 100644 --- a/docs/architecture-refactor-roadmap.md +++ b/docs/architecture-refactor-roadmap.md @@ -139,7 +139,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。 | S3-L3 Scheduler | `VERIFIED` | S2-L3 | 旧 `app/scheduler.py` 已退役并迁入 `app.scheduler` 同名包;catalog、execution/bridge/progress、`ExecutionRegistry`、reconciler、lifecycle 与 maintenance 已分离,业务 callable 由 startup 经 `SchedulerServices` 注入,包内无参 Chain 构造清零;功能/生命周期 215 项、架构/兼容/文档 189 项通过,官方插件基线语义未变化 | | S3-L4 DownloadChain | `DELIVERED` | S1-L6 | `7941fd921`:旧单体退役;selection、submission、batch、history、post-processing 等职责进入同名包单一 owner,提交后动作遵守新完成语义;锁定全量 `7162 passed, 9 skipped`,远端 `0/0` | | S3-L5 SearchChain | `DELIVERED` | S2-L7 | `1c145d716`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、类型/复杂度门禁和锁定全量 `7222 passed, 9 skipped` 通过;远端 `0/0` | -| S3-L5.1 Runtime dependencies 命名治理 | `ACTIVE` | S3-L5 | `dependencies.py` 与 `native_dependencies.py` 退役为 `runtime/dependencies/` 同名包;单词子模块、无包根重复导出,并把规则写入 `docs/rules/07-naming-conventions.md` 及机器门禁 | +| S3-L5.1 Runtime dependencies 命名治理 | `VERIFIED` | S3-L5 | `dependencies.py` 与 `native_dependencies.py` 退役为 `runtime/dependencies/` 同名包;单词子模块、无包根重复导出,并把规则写入 `docs/rules/07-naming-conventions.md` 及机器门禁 | | S3-L6 MediaChain | `PLANNED` | S2-L2 | recognition、source projection、music alignment、cache 分离,兼容仅经统一层 | | S3-L7 Agent/System/Plugin API | `PLANNED` | S2,S1-L6 | WebAgent SSE/file/audio、nettest/log/update/market 用例进入 Application,endpoint 只做传输适配 | @@ -176,7 +176,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。 ### S3-L5.1 Runtime dependencies 命名治理 -**Status:** `ACTIVE` +**Status:** `VERIFIED` **Outcome** @@ -199,6 +199,14 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。 - 运行依赖分组、插件原生依赖激活、插件安装/包管理专项及架构/质量门禁全部通过。 - 提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。 +**Evidence(2026-08-29)** + +- 平铺实现已删除,`app/runtime/dependencies/` 仅有 `__init__.py`、`profile.py`、`native.py`;包根只有包说明,宿主导入全部直达职责子模块。 +- Docker 更新边界同时覆盖新包载荷与旧版本 `dependencies.py` 快照回滚,未把旧实现恢复到主程序。 +- 官方插件仓最新远端 `aa40b961a2093474a77edd9eed88844ff07425ac` 对旧路径及全部公开符号零命中,因此未新增无消费者的 SDK/Compat 债务。 +- 运行依赖、原生激活、插件安装/包管理及架构专项 `406 passed`;最终架构/CI/Docker 门禁 `182 passed`。 +- 锁定全量 `7224 passed, 9 skipped`;Pylint `10.00/10` 且重复率 `0.000%`;mypy、Ruff(历史诊断降至 `667`)、复杂度和依赖基线门禁全部通过。 + ### S3-L5 SearchChain **Status:** `DELIVERED` diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 78e139331..566975833 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -100,6 +100,7 @@ directory categories. | `app/runtime/execution.py` | Shared sync/async execution and cross-thread submission boundary with correlation propagation | | `app/runtime/correlation.py` | Request/cross-thread correlation context and safe propagation into logs and child work | | `app/runtime/state.py` | Process restart and update state | +| `app/runtime/dependencies/` | Runtime dependency profile selection in `profile.py` and loaded native payload activation detection in `native.py`; the package root contains no implementation or host re-exports | | `app/runtime/extensions/` | Module, plugin, configured-service and managed-resource discovery/registration/lifecycle adapters | | `app/runtime/compat/` | Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics | @@ -114,6 +115,12 @@ API dependencies must narrow that object to a domain runtime (for example, `AgentChatRuntime`) instead of adding a string key to a global service map. Legacy registries may delegate the same object while domains migrate, but they must not construct a second set of service instances. +The retired flat `app/runtime/dependencies.py` and +`app/runtime/native_dependencies.py` modules must not return. Canonical host +callers import `app.runtime.dependencies.profile` or +`app.runtime.dependencies.native` directly; the package root is not an export +facade, and plugin compatibility is added only through an exact SDK/Compat route +when a verified plugin contract requires it. Canonical host consumers of the process-wide module, plugin, scheduler and system-configuration runtimes must call `get_module_manager()`, `get_plugin_manager()`, `get_scheduler()` and `get_configured_system_config()` @@ -883,4 +890,4 @@ imports, entrypoint (`api`/`agent`/`monitor`/`workflow`/`doctor`) imports of modules only through `run_module` dispatch), and downloader SDK (`qbittorrentapi`, `transmission_rpc`) imports inside `app/chain`. -*Last Updated: 2026-08-28* +*Last Updated: 2026-08-29* diff --git a/docs/rules/07-naming-conventions.md b/docs/rules/07-naming-conventions.md index 838e78b52..1226e8a19 100644 --- a/docs/rules/07-naming-conventions.md +++ b/docs/rules/07-naming-conventions.md @@ -16,6 +16,17 @@ All new code must follow these conventions. Consistent naming is how the codebas | Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` | | Skill directories | `/` | `transfer-failed-retry/`, `moviepilot-cli/` | +### Production Module Creation Gate + +新增或拆分生产模块时必须按以下顺序决策,代码评审与架构测试以此为准: + +1. 优先扩展当前职责 owner;只因文件变长或需要一个私有辅助类,不得新建平级模块。 +2. 一个能力需要第二个生产文件时,必须在同一次变更中改为同名目录包,禁止继续增加 `_.py` 平级文件。 +3. 包内子文件使用准确、可独立说明职责的单个小写名词,如 `dependencies/profile.py` 与 `dependencies/native.py`;不得使用 `native_dependencies.py`、`transfer_execution.py` 这类重复能力名。 +4. 包根 `__init__.py` 只允许包说明和确有外部契约证据的公开门面,不得复制实现,也不得为宿主代码重复导出子模块符号;宿主必须直接导入 owner 子模块。 +5. 旧路径兼容只能在确认真实插件消费者后,通过 `app/sdk/` 或 `app/runtime/compat/` 做精确映射;不得为假设消费者保留旧源码、通配映射或双份导出。 +6. 新增文件前必须同步更新本规则对应的机器门禁;若现有门禁不能表达该命名约束,应在同一变更中补充 `tests/test_architecture_dependencies.py`。 + --- ## Classes @@ -120,8 +131,9 @@ All new code must follow these conventions. Consistent naming is how the codebas | `configuration.get("RssUrls")` | `configuration.get(SystemConfigKey.RssUrls)` | | `class subscribe_oper:` | `class SubscribeOper:` | | `transfer.py` + `transfer_execution.py` | `transfer/workflow.py` + `transfer/execution.py` | +| `dependencies.py` + `native_dependencies.py` | `dependencies/profile.py` + `dependencies/native.py` | | Package-root host re-exports for an old path | Exact SDK/Compat mapping; host code imports the owning child module | | `MessageChannel.Telegram`(新代码) | `NotificationChannel.Telegram` | | `Notification(title=...)`(新代码) | `Message(title=...)` | -*Last Updated: 2026-08-27* +*Last Updated: 2026-08-29* diff --git a/docs/v3t-runtime-governance.md b/docs/v3t-runtime-governance.md index d8cfbe27d..2e8c0d7b5 100644 --- a/docs/v3t-runtime-governance.md +++ b/docs/v3t-runtime-governance.md @@ -28,7 +28,7 @@ V3t 的目标不是让所有请求都更快。它主要改善可并行的 Python - `runtime-free-threaded` `tool.uv.conflicts` 保证两个组不能同时解析。Docker 的两个构建 stage 分别固定选择对应组;源码升级、 -启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.runtime_dependency_group()` 根据当前 +启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.profile.runtime_dependency_group()` 根据当前 解释器的 `Py_GIL_DISABLED` 构建标志选择 profile。 依赖选择不得改为镜像标签判断,也不得在业务模块中按包名散落 V3/V3t 分支。Dockerfile 只选择运行 diff --git a/scripts/verify_runtime_profile.py b/scripts/verify_runtime_profile.py index d82da065f..55bb304a7 100644 --- a/scripts/verify_runtime_profile.py +++ b/scripts/verify_runtime_profile.py @@ -17,7 +17,7 @@ from packaging.requirements import InvalidRequirement, Requirement from packaging.utils import canonicalize_name from app.doctor import dependencies as dependency_doctor -from app.runtime.dependencies import runtime_excluded_dependency_pairs +from app.runtime.dependencies.profile import runtime_excluded_dependency_pairs _UV_MISSING_DEPENDENCY_PATTERN = re.compile( r"The package `(?P[^`]+)` requires `(?P[^`]+)`, " diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 0488fba51..ffed0a790 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -1119,8 +1119,8 @@ "runtime_only": true } }, - "edge_count": 7886, - "edge_sha256": "52a65e7b862f6f0ed63c59bd0abeca8d7400ca566a4481f71ab556559664d515", + "edge_count": 7890, + "edge_sha256": "d184c440fcec26e316115b99871803e96a0e4938597d4ef603fac79a92640888", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -1170,6 +1170,7 @@ "app.adapters.external.market -> app.runtime", "app.adapters.external.market -> app.runtime.cache", "app.adapters.external.market -> app.runtime.dependencies", + "app.adapters.external.market -> app.runtime.dependencies.profile", "app.adapters.external.market -> app.runtime.execution", "app.adapters.external.market -> app.runtime.log", "app.adapters.external.market -> app.runtime.observability", @@ -1250,6 +1251,7 @@ "app.adapters.system.host -> app.schemas.dashboard", "app.adapters.system.package -> app.runtime", "app.adapters.system.package -> app.runtime.dependencies", + "app.adapters.system.package -> app.runtime.dependencies.profile", "app.adapters.system.plugin.dependency -> app.adapters", "app.adapters.system.plugin.dependency -> app.adapters.external", "app.adapters.system.plugin.dependency -> app.adapters.external.market", @@ -1267,9 +1269,10 @@ "app.adapters.system.plugin.package -> app.adapters.system", "app.adapters.system.plugin.package -> app.adapters.system.host", "app.adapters.system.plugin.package -> app.runtime", + "app.adapters.system.plugin.package -> app.runtime.dependencies", + "app.adapters.system.plugin.package -> app.runtime.dependencies.native", "app.adapters.system.plugin.package -> app.runtime.execution", "app.adapters.system.plugin.package -> app.runtime.log", - "app.adapters.system.plugin.package -> app.runtime.native_dependencies", "app.adapters.system.plugin.package -> app.runtime.settings", "app.adapters.system.resource -> app.adapters", "app.adapters.system.resource -> app.adapters.network", @@ -3919,9 +3922,10 @@ "app.application.plugin.install -> app.application.plugin.source", "app.application.plugin.install -> app.application.plugin.transaction", "app.application.plugin.install -> app.runtime", + "app.application.plugin.install -> app.runtime.dependencies", + "app.application.plugin.install -> app.runtime.dependencies.native", "app.application.plugin.install -> app.runtime.execution", "app.application.plugin.install -> app.runtime.log", - "app.application.plugin.install -> app.runtime.native_dependencies", "app.application.plugin.install -> app.schemas", "app.application.plugin.install -> app.schemas.exception", "app.application.plugin.install -> app.schemas.plugin", @@ -7768,8 +7772,10 @@ "app.runtime.config -> app.schemas.types", "app.runtime.debounce -> app.runtime", "app.runtime.debounce -> app.runtime.log", - "app.runtime.dependencies -> app.foundation", - "app.runtime.dependencies -> app.foundation.environment", + "app.runtime.dependencies.native -> app.runtime", + "app.runtime.dependencies.native -> app.runtime.log", + "app.runtime.dependencies.profile -> app.foundation", + "app.runtime.dependencies.profile -> app.foundation.environment", "app.runtime.deprecation.policy -> app.runtime", "app.runtime.deprecation.policy -> app.runtime.deprecation", "app.runtime.deprecation.policy -> app.runtime.deprecation.notices", @@ -7970,8 +7976,6 @@ "app.runtime.extensions.service_config -> app.schemas", "app.runtime.extensions.service_config -> app.schemas.system", "app.runtime.extensions.service_config -> app.schemas.types", - "app.runtime.native_dependencies -> app.runtime", - "app.runtime.native_dependencies -> app.runtime.log", "app.runtime.observability -> app.runtime", "app.runtime.observability -> app.runtime.deprecation", "app.runtime.observability -> app.runtime.deprecation.policy", @@ -9009,7 +9013,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 932, + "module_count": 933, "modules": [ "app", "app.adapters", @@ -9759,6 +9763,8 @@ "app.runtime.correlation", "app.runtime.debounce", "app.runtime.dependencies", + "app.runtime.dependencies.native", + "app.runtime.dependencies.profile", "app.runtime.deprecation", "app.runtime.deprecation.notices", "app.runtime.deprecation.policy", @@ -9804,7 +9810,6 @@ "app.runtime.localization", "app.runtime.log", "app.runtime.loop", - "app.runtime.native_dependencies", "app.runtime.observability", "app.runtime.progress", "app.runtime.rate", diff --git a/tests/fixtures/architecture/official-plugin-baseline.json b/tests/fixtures/architecture/official-plugin-baseline.json index c99cee3e9..f9b8eef28 100644 --- a/tests/fixtures/architecture/official-plugin-baseline.json +++ b/tests/fixtures/architecture/official-plugin-baseline.json @@ -5623,9 +5623,9 @@ } }, "provenance": { - "head": "aa69b3a5bd21148bf443cb69eb3095d4f557ea62", + "head": "aa40b961a2093474a77edd9eed88844ff07425ac", "python_file_count": 284, - "source_sha256": "4c87d1625dffc8e84dbd8eb5c3fc75eb7457e64afaf43509af652461afa3e817" + "source_sha256": "c9621ee326c82a47f7c79725249b8adb3cf5176d604e868547a913e6cd77c5cb" }, "schema_version": 3, "scope": { diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json index f228ce909..8f0d2853a 100644 --- a/tests/fixtures/architecture/ruff-baseline.json +++ b/tests/fixtures/architecture/ruff-baseline.json @@ -708,9 +708,6 @@ "app/runtime/debounce.py": { "I001": 1 }, - "app/runtime/dependencies.py": { - "I001": 1 - }, "app/runtime/event/errors.py": { "I001": 1 }, @@ -982,10 +979,6 @@ "tests/test_db_session_lifecycle.py": { "I001": 1 }, - "tests/test_docker_bootstrap.py": { - "F841": 2, - "I001": 1 - }, "tests/test_docker_entrypoint_permissions.py": { "I001": 1 }, diff --git a/tests/test_architecture_ci.py b/tests/test_architecture_ci.py index 9abeebb64..9e51f661c 100644 --- a/tests/test_architecture_ci.py +++ b/tests/test_architecture_ci.py @@ -186,7 +186,10 @@ def test_native_dependency_update_probe_has_narrow_automatic_triggers(): "app/adapters/external/market.py", "app/adapters/system/host.py", "app/adapters/system/package.py", - "app/runtime/dependencies.py", + "app/adapters/system/plugin/package.py", + "app/application/plugin/install.py", + "app/runtime/dependencies/native.py", + "app/runtime/dependencies/profile.py", "scripts/probe_native_dependency_update.py", ".github/workflows/native-dependency-update.yml", ] @@ -218,6 +221,7 @@ def test_dependency_compatibility_covers_windows_free_threaded_profile(): assert "MOVIEPILOT_SAFE_MODE" in str(workflow["jobs"]["install"]["steps"]) expected_paths = { "app/__init__.py", + "app/runtime/dependencies/profile.py", "scripts/verify_runtime_profile.py", } assert expected_paths <= set(workflow["on"]["pull_request"]["paths"]) diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 8f93975fc..4eda5d206 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -79,6 +79,8 @@ RETIRED_CANONICAL_FILES = ( "app/foundation/structures.py", "app/foundation/zhconv.py", "app/runtime/runtime.py", + "app/runtime/dependencies.py", + "app/runtime/native_dependencies.py", "app/adapters/network/rss.py", "app/adapters/network/sites.pyi", "app/application/plugins.py", @@ -298,6 +300,43 @@ def test_retired_canonical_filenames_do_not_return(): assert leftovers == [] +def test_runtime_dependencies_use_same_named_single_word_package() -> None: + """运行依赖能力必须使用同名包,且宿主直接依赖单一职责子模块。""" + package = APP_ROOT / "runtime" / "dependencies" + assert {path.name for path in package.glob("*.py")} == { + "__init__.py", + "native.py", + "profile.py", + } + + init_tree = ast.parse( + (package / "__init__.py").read_text(encoding="utf-8"), + filename=str(package / "__init__.py"), + ) + assert ast.get_docstring(init_tree) + assert len(init_tree.body) == 1 + + violations: list[str] = [] + for path in APP_ROOT.rglob("*.py"): + relative = path.relative_to(APP_ROOT) + if relative.parts[0] == "plugins": + continue + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + for node in ast.walk(tree): + if ( + isinstance(node, ast.ImportFrom) + and node.module == "app.runtime.dependencies" + ): + violations.append(f"{relative}:{node.lineno}:from-package-root") + elif isinstance(node, ast.Import): + violations.extend( + f"{relative}:{node.lineno}:package-root" + for alias in node.names + if alias.name == "app.runtime.dependencies" + ) + assert violations == [] + + def test_workflow_query_contract_returns_only_typed_snapshots(): """工作流正式查询端口不得退化为 Any 或 ORM 返回值。""" path = APP_ROOT / "application" / "workflow.py" diff --git a/tests/test_docker_bootstrap.py b/tests/test_docker_bootstrap.py index d52f2aefd..b59a8bd53 100644 --- a/tests/test_docker_bootstrap.py +++ b/tests/test_docker_bootstrap.py @@ -9,7 +9,6 @@ from pathlib import Path import pytest - ROOT = Path(__file__).resolve().parents[1] LAUNCHER = ROOT / "docker" / "launcher.sh" UPDATER = ROOT / "docker" / "update.sh" @@ -75,7 +74,9 @@ runtime-free-threaded = ["free-threaded"] encoding="utf-8", ) (project / "uv.lock").write_text("version = 1\n", encoding="utf-8") - runtime_selector = project / "app" / "runtime" / "dependencies.py" + runtime_selector = ( + project / "app" / "runtime" / "dependencies" / "profile.py" + ) runtime_selector.parent.mkdir(parents=True) runtime_selector.write_text("print('runtime-free-threaded')\n", encoding="utf-8") venv_bin = tmp_path / "venv" / "bin" @@ -123,6 +124,78 @@ runtime-free-threaded = ["free-threaded"] assert "--no-default-groups --group runtime-free-threaded" in command +def test_update_sync_supports_legacy_runtime_selector_during_rollback( + tmp_path: Path, +) -> None: + """更新回滚必须能读取改名前旧快照中的依赖 profile 选择器。""" + project = tmp_path / "legacy-project" + project.mkdir() + (project / "pyproject.toml").write_text( + """ +[project] +name = "moviepilot" +version = "0" + +[dependency-groups] +runtime-standard = ["standard"] +runtime-free-threaded = ["free-threaded"] +""", + encoding="utf-8", + ) + (project / "uv.lock").write_text("version = 1\n", encoding="utf-8") + legacy_selector = project / "app" / "runtime" / "dependencies.py" + legacy_selector.parent.mkdir(parents=True) + legacy_selector.write_text("print('runtime-standard')\n", encoding="utf-8") + + venv_bin = tmp_path / "venv" / "bin" + venv_bin.mkdir(parents=True) + python_log = tmp_path / "python.log" + python_bin = venv_bin / "python3" + python_bin.write_text( + "#!/bin/bash\nprintf '%s\\n' \"$*\" > " + f"{shlex.quote(str(python_log))}\nprintf '%s\\n' runtime-standard\n", + encoding="utf-8", + ) + python_bin.chmod(0o755) + uv_bin = tmp_path / "uv" + uv_log = tmp_path / "uv.log" + uv_bin.write_text( + f"#!/bin/bash\nprintf '%s\\n' \"$*\" > {shlex.quote(str(uv_log))}\n", + encoding="utf-8", + ) + uv_bin.chmod(0o755) + script = textwrap.dedent( + f"""\ + CONFIG_DIR="$1" + VENV_PATH="$2" + UV_BIN="$3" + source {UPDATER!s} + PACKAGE_ENV=() + UV_OPTIONS=() + sync_project_dependencies_for "$4" + """ + ) + + subprocess.run( + [ + "bash", + "-c", + script, + "legacy-runtime-profile-test", + str(tmp_path / "config"), + str(tmp_path / "venv"), + str(uv_bin), + str(project), + ], + check=True, + ) + + assert python_log.read_text(encoding="utf-8").strip() == str(legacy_selector) + assert "--no-default-groups --group runtime-standard" in uv_log.read_text( + encoding="utf-8" + ) + + def test_build_profiles_use_full_runtime_capability_probe() -> None: """两套依赖 profile 必须经过统一的完整运行能力验证。""" dockerfile = (ROOT / "docker" / "Dockerfile").read_text(encoding="utf-8") @@ -1222,7 +1295,6 @@ def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path) def test_update_transaction_keeps_marker_when_backup_cleanup_fails(tmp_path: Path) -> None: config_dir = tmp_path / "config" - pending_file = config_dir / "temp" / "__update_pending__" log_file = tmp_path / "cleanup.log" script = textwrap.dedent( f"""\ @@ -1368,7 +1440,6 @@ def test_staged_payload_swap_failure_restores_previous_generation(tmp_path: Path (stage_app / "app" / "new.py").write_text("new", encoding="utf-8") (stage_public / "index.html").write_text("new-front", encoding="utf-8") config_dir = tmp_path / "config" - uv_log = tmp_path / "uv.log" script = textwrap.dedent( f"""\ CONFIG_DIR="$1" diff --git a/tests/test_native_dependency_activation.py b/tests/test_native_dependency_activation.py index edcd4d5b3..f61181677 100644 --- a/tests/test_native_dependency_activation.py +++ b/tests/test_native_dependency_activation.py @@ -2,7 +2,7 @@ from pathlib import Path -from app.runtime import native_dependencies +import app.runtime.dependencies.native as native_dependencies class _Distribution: diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index fb26e29fb..b8265dece 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -1394,7 +1394,7 @@ demo = { index = "private" } requirements_file = Path(temp_dir) / "requirements.txt" requirements_file.write_text(f"{requirement}\n", encoding="utf-8") with patch( - "app.runtime.dependencies.runtime_dependency_group", + "app.runtime.dependencies.profile.runtime_dependency_group", return_value=runtime_group, ): success, message = PluginHelper._PluginHelper__validate_runtime_dependency_conflicts( diff --git a/tests/test_plugin_install_command.py b/tests/test_plugin_install_command.py index 7abb42512..7eb0de273 100644 --- a/tests/test_plugin_install_command.py +++ b/tests/test_plugin_install_command.py @@ -33,7 +33,7 @@ from app.application.plugin.transaction import ( PluginInstallationPhase, PluginInstallationRecord, ) -from app.runtime.native_dependencies import NativeDependencyChange +from app.runtime.dependencies.native import NativeDependencyChange from app.schemas.exception import ( DatabaseWorkerClosedError, PersistenceUnavailableError, diff --git a/tests/test_plugin_package_manager.py b/tests/test_plugin_package_manager.py index 78244b187..11966d4f7 100644 --- a/tests/test_plugin_package_manager.py +++ b/tests/test_plugin_package_manager.py @@ -6,7 +6,7 @@ from unittest.mock import Mock import pytest from app.adapters.system.plugin.package import PluginPackageManager -from app.runtime.native_dependencies import LoadedNativeDependencySnapshot +from app.runtime.dependencies.native import LoadedNativeDependencySnapshot def _manager(monkeypatch, tmp_path: Path) -> PluginPackageManager: diff --git a/tests/test_runtime_dependencies.py b/tests/test_runtime_dependencies.py index 68e591394..e765d03bd 100644 --- a/tests/test_runtime_dependencies.py +++ b/tests/test_runtime_dependencies.py @@ -4,9 +4,9 @@ from types import SimpleNamespace import pytest +import app.runtime.dependencies.profile as dependencies from app.doctor import dependencies as dependency_doctor from app.foundation import environment -from app.runtime import dependencies from scripts import verify_runtime_profile