From 9f3be0ea4b4a27031800abec2c115830d761d33f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 23 Aug 2026 16:25:08 +0800 Subject: [PATCH] refactor: restore architecture governance gates --- .github/workflows/test.yml | 5 + app/sdk/database.py | 8 + .../backend-architecture-governance.md | 22 +- .../backend-architecture-next-stage.md | 18 +- scripts/architecture/baseline.py | 186 ++++- scripts/startup/performance.py | 74 +- .../architecture/async-blocking-baseline.json | 9 - .../official-plugin-baseline.json | 639 +++++++++++++++--- .../runtime-contract-baseline.json | 285 +------- .../startup-performance-baseline.json | 294 ++++---- tests/test_architecture_baseline_cli.py | 162 ++++- tests/test_architecture_ci.py | 1 + tests/test_architecture_contract_baseline.py | 8 +- 13 files changed, 1146 insertions(+), 565 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31b2091c9..37bf55724 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,11 @@ jobs: - name: Check async blocking ratchet run: uv run --locked --no-sync python scripts/architecture/async_blocking.py + - name: Check startup performance contract + run: >- + uv run --locked --no-sync python + scripts/startup/performance.py --check --repeat 3 + pytest: runs-on: ubuntu-latest name: Unit Tests (${{ matrix.shard }}) diff --git a/app/sdk/database.py b/app/sdk/database.py index 2e9aa9146..a74da2f78 100644 --- a/app/sdk/database.py +++ b/app/sdk/database.py @@ -3,6 +3,14 @@ from app.application.backup import BackupArtifact, BackupVerification from app.application.database import get_database_governance as _get_database_governance +__all__ = [ + "BackupArtifact", + "BackupVerification", + "create_backup", + "list_backups", + "verify_backup", +] + def create_backup() -> BackupArtifact: """在宿主管理目录中创建当前数据库的一致备份。""" diff --git a/docs/refactor/backend-architecture-governance.md b/docs/refactor/backend-architecture-governance.md index d03610f56..557603e69 100644 --- a/docs/refactor/backend-architecture-governance.md +++ b/docs/refactor/backend-architecture-governance.md @@ -25,7 +25,7 @@ 3. `PluginManager` 的加载、生命周期、注册表、投影、存储、目录、路径、同步、依赖、克隆和文件监控分别由 `app/runtime/extensions/plugin/` 下的单职责组件承担;旧管理器只保留 V3 ABI 门面和兼容调用顺序。 4. 动态插件 API 使用专用 raw 路由;主程序统一响应信封不进入插件 `get_api()`。前端 `pluginApi` 对非 `Response` envelope 的 payload 原样交付调用方。 5. 旧插件导入仅由 `app/runtime/compat/manifest.py` 精确映射;canonical 模块不复制旧 Manager/Helper/Oper 导出。`app/plugins/` 仍是运行时副本,继续排除在宿主架构扫描之外。 -6. 当前机器基线为 746 个宿主 Python 模块、6,024 条内部导入边;数据库边界、Adapter→DB、Runtime→DB、Application→DB 及新增 API/Agent/Chain 目标边均为 0。架构门禁、插件兼容快照和基线脚本均已重新生成。 +6. 2026-08-23 当前机器基线为 800 个宿主 Python 模块、6,479 条内部导入边;数据库边界、Adapter→DB、Runtime→DB、Application→DB 及新增 API/Agent/Chain 目标边均为 0。架构门禁、插件兼容快照和基线脚本均已重新生成。 7. 订阅写入统一归入 `app/application/subscription/write.py`;插件动态路由和文件夹操作统一归入 `app/application/plugin/routes.py`、`folders.py`。重构期间新增且未形成插件 ABI 的 `app/application/subscribe.py`、`app/application/plugins.py` 已直接删除,不进入 compat manifest。 ## 2. 范围与明确排除项 @@ -45,7 +45,7 @@ ### 2.2 排除项 - **不审计、不迁移 `app/plugins/` 中的代码。**该目录是已安装插件副本,不是后端架构源代码,也不能作为插件兼容性的唯一事实来源。 -- 插件兼容基线应读取同工作区独立仓库 `../MoviePilot-Plugins` 的 `plugins.v2/`、`plugins.v3/`,再配合宿主的 SDK、兼容清单和插件管理器契约判断。 +- 插件兼容基线应读取同工作区独立仓库 `../MoviePilot-Plugins` 的 `plugins.v3/`、`plugins.v2/` 和 V3 实际会从默认索引回退加载的 `plugins/` 实现,再配合宿主的 SDK、兼容清单和插件管理器契约判断。 - 不把 `app/modules/themoviedb/` 内部第三方或移植代码的局部循环,直接等同于 MoviePilot 自有架构失败。它需要被隔离,但不应优先重写上游库。 - 本轮不主张数据库表结构变更。纯架构批次不得夹带 Alembic 迁移、字段重命名或数据回填。 - 本轮不主张删除 V3 兼容映射。任何删除都应作为显式破坏性变更另行决策。 @@ -99,7 +99,7 @@ MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helpe ### 4.3 模块规模 -排除 `app/plugins/` 后,当前静态扫描得到 746 个 Python 模块、6,024 条内部导入边。主要一级目录规模如下(代码行数包含注释和空行,用于趋势比较而非质量评分): +排除 `app/plugins/` 后,2026-08-23 当前静态扫描得到 800 个 Python 模块、6,479 条内部导入边。下表保留 2026-08-18 收口时的一级目录规模快照(代码行数包含注释和空行,用于趋势比较而非质量评分): | 一级目录 | 约代码行数 | Python 文件数 | 判断 | | --- | ---: | ---: | --- | @@ -147,8 +147,8 @@ MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helpe | 指标 | 初始审计 | 当前基线 | 说明 | | --- | ---: | ---: | --- | -| Python 模块数 | 约 654 | 746 | 增量来自单一职责的 Application、Runtime、Adapter、插件组件和维护用例模块 | -| 内部导入边 | 约 5,623 | 6,024 | 显式端口增加模块数但移除了反向边;边数不作为单独质量目标 | +| Python 模块数 | 约 654 | 800 | 增量来自单一职责的 Application、Runtime、Adapter、插件组件和维护用例模块 | +| 内部导入边 | 约 5,623 | 6,479 | 显式端口增加模块数但移除了反向边;边数不作为单独质量目标 | | SCC 数 | 14 | 1 | 自有代码 SCC 已归零,仅保留 TMDB 移植包内部隔离例外 | | `adapters -> db` | 存在 | 0 | `PluginHelper`、`MoviePilotServerHelper` 的本地数据读取已移到组合根/Application | | `runtime -> db` | 存在 | 0 | 插件存储、服务配置均改为启动注入 | @@ -159,7 +159,7 @@ Doctor/Monitor 改为惰性公开门面;QQBot、Telegram、TriMedia、UGreen - `tests/fixtures/architecture/dependency-baseline.json`:模块、边、SCC 和目标边。 - `tests/fixtures/architecture/runtime-contract-baseline.json`:SDK、兼容清单、事件和 `run_module` 合同。 -- `tests/fixtures/architecture/official-plugin-baseline.json`:独立官方插件仓 V2/V3 导入及钩子快照。 +- `tests/fixtures/architecture/official-plugin-baseline.json`:独立官方插件仓中 V3 实际可加载的 V3/V2/default 实现导入及钩子快照。 - `app/schemas/exports.py`:Schema 根入口的生成式兼容导出清单。 ## 5. 目标架构与依赖方向 @@ -788,9 +788,9 @@ app/agent/execution/ # 执行、流事件、用量、恢复 `app/runtime/compat/manifest.py` 当前约包含: -- 112 个模块别名。 +- 113 个模块别名。 - 1 个包别名。 -- 8 个模块、约 41 个符号别名。 +- 10 个模块、55 个符号别名。 - 3 个虚拟包。 独立插件仓中仍高频使用: @@ -828,8 +828,8 @@ app/agent/execution/ # 执行、流事件、用量、恢复 - SDK 公开面有机器可读清单和变更审查。 - 每次迁移明确列出旧路径、新路径、身份要求和保留期限。 -- 独立插件仓 v2/v3 静态导入扫描通过。 -- V3 治理批次不删除现有 112/41 兼容项。 +- 独立插件仓中 V3 实际可加载实现的静态导入扫描通过。 +- V3 治理批次不删除现有 113 个模块别名、1 个包别名和 55 个符号别名。 ### 6.15 配置、缓存和错误策略分散 @@ -1071,7 +1071,7 @@ startup 注入具体依赖 | 阶段 | 已落地入口 | 已锁定的关键语义 | | --- | --- | --- | -| 0 | `scripts/architecture/baseline.py`、`scripts/schema/exports.py`、三份 architecture fixture | 模块/边/SCC、SDK/compat、事件、`run_module`、官方插件 V2/V3 导入和钩子快照 | +| 0 | `scripts/architecture/baseline.py`、`scripts/schema/exports.py`、architecture fixtures | 模块/边/SCC、显式 `__all__` SDK/compat、事件、`run_module`、官方插件 V3/V2/default 有效实现的导入和钩子快照 | | 0 | `app/adapters/web/plugin/routes.py`、`app/application/plugin/routes.py` | 主程序继续统一 envelope;动态插件 API 默认 raw,自定义状态码、原生 Response、文件/流响应不被改写 | | 0 | `MoviePilot-Frontend/src/api/client.ts` | 联邦插件公共客户端遇到非 `Response` payload 时原样返回;合法 envelope 仍保留统一错误反馈 | | 1 | `app/schemas/exports.py`、`app/schemas/__init__.py` | Schema 根入口惰性兼容导出,宿主内部使用精确子模块,公开符号由生成清单锁定 | diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index cd3fe46ed..6ed4bf64e 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -6,20 +6,30 @@ > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` -> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配、Outbox 外围扩展和 Model 查询兼容面仍按风险切片推进。 +> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配、Outbox 外围扩展和 Model 查询兼容面仍按风险切片推进。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线。 ## 当前复核结论(2026-08-23) 本节是本轮全面复核后的当前事实源。本文后续的阶段实施记录保留历史审计证据, 其中的数量和判断以当时审计提交为准,不能直接当作当前未完成项。 +### 长期整改阶段 0:治理门禁恢复(2026-08-23) + +- 宿主依赖基线已审查 TaskRegistry 接入后的语义差异:当前为 `800` 个模块、`6479` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。 +- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `16`/`8`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。 +- 官方插件快照覆盖 `plugins.v3`、`plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。 +- SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing`、`__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。 +- async 阻塞实际债务已由 fixture 中的 10 项下降到 1 项并固化低水位;剩余项是 Scheduler Agent task 查询,后续阶段迁入异步查询边界后归零。 + +本阶段只修复治理信号和事实源,不把基线刷新当作业务重构完成。后台任务所有权、Module Contract V2、typed runtime、durable 副作用和质量规模化仍按下列 P1/P2 顺序推进。 + ### 总体判断 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 -- 依赖图当前约 `796` 个 Python 模块、`6432` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 +- 依赖图当前为 `800` 个 Python 模块、`6479` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 @@ -33,7 +43,7 @@ ### P2:中长期可演进性债务 -- **大型职责域仍偏重。** 代表性热点包括 `app/chain/subscribe.py`(约 `4141` 行)、`app/chain/transfer.py`(约 `2944` 行)、`app/agent/orchestrator.py`(约 `3535` 行)、`app/agent/llm/provider.py`(约 `3529` 行)、`app/adapters/external/market.py`(约 `2805` 行)和 `app/api/endpoints/agent.py`(约 `2326` 行)。复杂度 ratchet 只保证不超过当前基线,不代表这些文件已经易维护。只有在行为快照、调用命中和事务边界明确后,才值得按用例拆分。 +- **大型职责域仍偏重。** 代表性热点包括 `app/chain/subscribe.py`(约 `4141` 行)、`app/chain/transfer.py`(约 `2944` 行)、`app/agent/orchestrator.py`(约 `3540` 行)、`app/agent/llm/provider.py`(约 `3529` 行)、`app/adapters/external/market.py`(约 `3139` 行)和 `app/api/endpoints/agent.py`(约 `2489` 行)。复杂度 ratchet 只保证不超过当前基线,不代表这些文件已经易维护。只有在行为快照、调用命中和事务边界明确后,才值得按用例拆分。 - **类型门禁覆盖面不足。** `mypy.ini` strict 文件清单目前约 `37` 个文件,Agent、Chain、Module、Adapter 大量代码仍依赖动态类型。应从模块契约、生命周期、Repository/Port 和关键 Chain 返回值开始扩展,而不是直接开启全仓 strict。 - **Pylint 仍是增量硬门禁。** `.github/workflows/pylint.yml` 对改动 Python 文件执行硬检查,但全仓报告使用 `|| true` 仅作 advisory。该策略适合存量迁移,却没有形成全仓质量趋势约束;应增加按目录和新增问题数的 ratchet。 - **测试风格存在历史混用。** 当前约 `499` 个测试文件,仍有约 `70` 个 `unittest.TestCase` 文件。它不是生产架构缺陷,但会增加 fixture、状态隔离和异步测试迁移成本,应在触碰相关模块时渐进迁移。 @@ -45,7 +55,7 @@ - 全功能多 worker 的误导性配置已由 `app/runtime/topology.py` 和 `app/main.py` 拒绝;V3 默认单 worker 的部署事实已经明确。 - 写事务已由组合根/UoW/Outbox 方向收口,`db_update/async_db_update` 为零;不要重新引入 Model 自动提交。 - 已具备 correlation ID、`/health/live`、`/health/ready`、模块/事件/调度观测端口和兼容 Facade 命中指标;历史文档中“完全缺少观测能力”的描述已过时。 -- 旧导入路径、SDK 导出、插件 manifest 和 V1/V2/V3 索引均有白名单或版本约束;兼容层应继续保持“薄、可观测、只增不删”,不应为了清理目录直接删除。 +- 旧导入路径、显式 `__all__` SDK 合同、插件 manifest 和 V3 实际可加载的三层索引实现均有白名单或版本约束;兼容层应继续保持“薄、可观测、只增不删”,不应为了清理目录直接删除。 - TMDB 移植包内部 SCC 属于第三方隔离代码,按现状豁免是合理的技术决策。 ### 刻意保留的兼容成本 diff --git a/scripts/architecture/baseline.py b/scripts/architecture/baseline.py index 493778ac1..fb816de5f 100644 --- a/scripts/architecture/baseline.py +++ b/scripts/architecture/baseline.py @@ -753,34 +753,121 @@ def collect_event_diagnostics() -> dict[str, Any]: } +def _sdk_all_names(tree: ast.Module, path: Path) -> tuple[str, ...]: + """读取 SDK 模块显式声明的 ``__all__``,未声明时不推断公开合同。""" + value_node: Optional[ast.expr] = None + for node in tree.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "__all__" + for target in node.targets + ): + value_node = node.value + elif ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == "__all__" + ): + value_node = node.value + if value_node is None: + return () + try: + names = ast.literal_eval(value_node) + except (ValueError, TypeError) as err: + raise ValueError(f"SDK 模块 {path} 的 __all__ 必须是字符串列表") from err + if not isinstance(names, (list, tuple)) or not all( + isinstance(name, str) for name in names + ): + raise ValueError(f"SDK 模块 {path} 的 __all__ 必须是字符串列表") + if len(names) != len(set(names)): + raise ValueError(f"SDK 模块 {path} 的 __all__ 存在重复名称") + return tuple(names) + + +def _sdk_alias_target( + value: ast.expr, + imported_targets: dict[str, str], +) -> str: + """把顶层别名赋值解析为稳定目标,保留其真实 canonical 来源。""" + if isinstance(value, ast.Name): + return imported_targets.get(value.id, value.id) + if isinstance(value, ast.Attribute): + return f"{_sdk_alias_target(value.value, imported_targets)}.{value.attr}" + return ast.unparse(value) + + +def _collect_sdk_module_exports(path: Path) -> list[dict[str, str]]: + """按单个 SDK 模块的显式 ``__all__`` 生成可比较的符号合同。""" + tree = parse_source(path) + export_names = _sdk_all_names(tree, path) + imported_targets: dict[str, str] = {} + bindings: dict[str, dict[str, str]] = {} + for node in tree.body: + if isinstance(node, ast.ImportFrom) and node.module: + module_name = f"{'.' * node.level}{node.module}" + for alias in node.names: + if alias.name == "*": + continue + public_name = alias.asname or alias.name + target = f"{module_name}.{alias.name}" + imported_targets[public_name] = target + bindings[public_name] = { + "name": public_name, + "kind": "import", + "target": target, + } + elif isinstance(node, ast.Import): + for alias in node.names: + public_name = alias.asname or alias.name.split(".", maxsplit=1)[0] + imported_targets[public_name] = alias.name + bindings[public_name] = { + "name": public_name, + "kind": "import", + "target": alias.name, + } + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + bindings[node.name] = { + "name": node.name, + "kind": type(node).__name__, + "target": "", + } + elif isinstance(node, ast.Assign): + for target_node in node.targets: + if not isinstance(target_node, ast.Name) or target_node.id == "__all__": + continue + bindings[target_node.id] = { + "name": target_node.id, + "kind": "alias", + "target": _sdk_alias_target(node.value, imported_targets), + } + elif ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id != "__all__" + and node.value is not None + ): + bindings[node.target.id] = { + "name": node.target.id, + "kind": "alias", + "target": _sdk_alias_target(node.value, imported_targets), + } + + unresolved = sorted(set(export_names) - set(bindings)) + if unresolved: + raise ValueError( + f"SDK 模块 {path} 的 __all__ 含无法解析的顶层名称:{', '.join(unresolved)}" + ) + return sorted( + (bindings[name] for name in export_names), + key=lambda item: (item["name"], item["kind"], item["target"]), + ) + + def collect_sdk_exports() -> dict[str, list[dict[str, str]]]: - """通过 AST 收集顶层 SDK 公开符号,避免导入时物化运行资源。""" + """按显式 ``__all__`` 收集 SDK 合同,避免导入时物化运行资源。""" result: dict[str, list[dict[str, str]]] = {} for path in sorted((APP_ROOT / "sdk").glob("*.py")): module_name = f"app.sdk.{path.stem}" if path.stem != "__init__" else "app.sdk" - exports: list[dict[str, str]] = [] - for node in parse_source(path).body: - if isinstance(node, ast.ImportFrom) and node.module: - for alias in node.names: - public_name = alias.asname or alias.name - if public_name.startswith("_") or alias.name == "*": - continue - exports.append( - { - "name": public_name, - "kind": "import", - "target": f"{node.module}.{alias.name}", - } - ) - elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): - if not node.name.startswith("_"): - exports.append( - {"name": node.name, "kind": type(node).__name__, "target": ""} - ) - result[module_name] = sorted( - exports, - key=lambda item: (item["name"], item["kind"], item["target"]), - ) + result[module_name] = _collect_sdk_module_exports(path) return result @@ -982,14 +1069,50 @@ def collect_plugin_api_contracts(path: Path) -> list[dict[str, Any]]: return sorted(routes, key=lambda item: (item["path"], item["endpoint"])) +def _read_plugin_index(path: Path) -> dict[str, Any]: + """读取插件索引;缺失索引按该代没有候选处理。""" + if not path.is_file(): + return {} + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"插件索引必须是对象:{path}") + return value + + +def _v3_default_plugin_roots(plugin_repo: Path) -> dict[str, Path]: + """返回 V3/V2 专用索引均未接管时可回退的默认插件源码目录。""" + default_index = _read_plugin_index(plugin_repo / "package.json") + v2_index = _read_plugin_index(plugin_repo / "package.v2.json") + v3_index = _read_plugin_index(plugin_repo / "package.v3.json") + roots: dict[str, Path] = {} + for plugin_id, plugin_info in default_index.items(): + if not isinstance(plugin_info, dict): + continue + if plugin_info.get("v2") is not True or plugin_info.get("v3") is False: + continue + if plugin_id in v3_index: + continue + v2_info = v2_index.get(plugin_id) + if isinstance(v2_info, dict) and v2_info.get("v3") is not False: + continue + plugin_root = plugin_repo / "plugins" / plugin_id.lower() + if not plugin_root.is_dir(): + raise FileNotFoundError(f"V3 默认兼容插件缺少源码目录:{plugin_root}") + roots[plugin_id] = plugin_root + return roots + + def collect_official_plugin_baseline(plugin_repo: Path) -> dict[str, Any]: - """扫描独立官方插件仓的导入面、Hook 和动态 API 契约。""" - roots = [plugin_repo / "plugins.v2", plugin_repo / "plugins.v3"] + """扫描独立官方插件仓中 V3 可见实现的导入面、Hook 和动态 API 契约。""" + versioned_roots = [plugin_repo / "plugins.v2", plugin_repo / "plugins.v3"] + default_roots = _v3_default_plugin_roots(plugin_repo) paths = sorted( - path - for root in roots - if root.exists() - for path in root.rglob("*.py") + { + path + for root in [*versioned_roots, *default_roots.values()] + if root.exists() + for path in root.rglob("*.py") + } ) import_files: dict[str, set[str]] = defaultdict(set) hook_files: dict[str, set[str]] = defaultdict(set) @@ -1019,7 +1142,8 @@ def collect_official_plugin_baseline(plugin_repo: Path) -> dict[str, Any]: "schema_version": 3, "scope": { "repository": "MoviePilot-Plugins", - "roots": [root.name for root in roots], + "roots": [*[root.name for root in versioned_roots], "plugins"], + "default_plugins": sorted(default_roots), }, "provenance": { "head": git_head(plugin_repo), diff --git a/scripts/startup/performance.py b/scripts/startup/performance.py index abe87199c..8bbe8bcfb 100644 --- a/scripts/startup/performance.py +++ b/scripts/startup/performance.py @@ -46,7 +46,10 @@ importlib.import_module({target!r}) elapsed_ms = (time.perf_counter() - started_at) * 1000 print({RESULT_PREFIX!r} + json.dumps({{ 'elapsed_ms': elapsed_ms, - 'loaded_module_count': len(set(sys.modules) - before), + 'loaded_app_module_count': len([ + name for name in set(sys.modules) - before + if name == 'app' or name.startswith('app.') + ]), }})) """ environment = os.environ.copy() @@ -98,17 +101,22 @@ from app.startup import lifecycle def _noop(): + '''替代无需执行的真实同步组件回调。''' return None async def _async_noop(): + '''替代无需执行的真实异步组件回调。''' return None def _isolated_start(component, probe_app): + '''保留探针所需基础状态,其余组件启动替换为空操作。''' # 保留 readiness 所需状态转换,其余真实启动回调替换为空操作。 if component.start is None: return None + if component.name == '后台任务登记器': + return component.start if component.name == '数据库准备': return lambda: lifecycle.get_application_health( probe_app @@ -116,7 +124,16 @@ def _isolated_start(component, probe_app): return _noop +def _isolated_stop(component): + '''真实释放探针基础设施,其余组件关闭替换为空操作。''' + # TaskRegistry 是后续生命周期代码的基础设施,探针必须验证其真实释放路径。 + if component.name == '后台任务登记器': + return component.stop + return _noop if component.stop is not None else None + + async def _probe(): + '''执行一次隔离生命周期并输出资源与耗时样本。''' lifecycle.settings.MOVIEPILOT_SAFE_MODE = {safe_mode!r} lifecycle.init_extra = _async_noop lifecycle.global_vars.set_loop = lambda loop: None @@ -128,7 +145,7 @@ async def _probe(): dataclasses.replace( component, start=_isolated_start(component, probe_app), - stop=_noop if component.stop is not None else None, + stop=_isolated_stop(component), ) for component in original_components ) @@ -137,6 +154,7 @@ async def _probe(): original_step = lifecycle.run_startup_step async def timed_step(name, callback, timeout_seconds=None): + '''执行隔离启动步骤并记录耗时。''' started = time.perf_counter() result = await original_step(name, callback, timeout_seconds) stage_ms[name] = round((time.perf_counter() - started) * 1000, 3) @@ -151,12 +169,14 @@ async def _probe(): started_threads = threading.active_count() started_tasks = len(asyncio.all_tasks()) finished_ms = (time.perf_counter() - started) * 1000 + enabled_components = [ + component.name for component in isolated_components + if component.enabled({safe_mode!r}) + ] print({LIFECYCLE_RESULT_PREFIX!r} + json.dumps({{ 'mode': 'safe' if {safe_mode!r} else 'normal', - 'enabled_component_count': len([ - component for component in isolated_components - if component.enabled({safe_mode!r}) - ]), + 'enabled_components': enabled_components, + 'enabled_component_count': len(enabled_components), 'startup_ms': round(startup_ms, 3), 'full_lifespan_ms': round(finished_ms, 3), 'stage_ms': stage_ms, @@ -206,9 +226,9 @@ def collect_baseline(repeat: int) -> dict[str, Any]: samples = [measure_import(target) for _ in range(repeat)] elapsed = [sample["elapsed_ms"] for sample in samples] targets[target] = { - "loaded_module_count": int( + "loaded_app_module_count": int( statistics.median( - sample["loaded_module_count"] for sample in samples + sample["loaded_app_module_count"] for sample in samples ) ), "max_ms": round(max(elapsed), 3), @@ -219,8 +239,21 @@ def collect_baseline(repeat: int) -> dict[str, Any]: lifecycle_modes: dict[str, Any] = {} for safe_mode, mode_name in ((False, "normal"), (True, "safe")): samples = [measure_lifecycle(safe_mode) for _ in range(repeat)] + enabled_components = samples[0]["enabled_components"] + if any( + sample["enabled_components"] != enabled_components + for sample in samples[1:] + ): + raise RuntimeError(f"{mode_name} 模式生命周期组件在重复采样间发生变化") lifecycle_modes[mode_name] = { - "samples": samples, + "samples": [ + { + key: value + for key, value in sample.items() + if key != "enabled_components" + } + for sample in samples + ], "median_startup_ms": round( statistics.median(sample["startup_ms"] for sample in samples), 3, @@ -230,9 +263,10 @@ def collect_baseline(repeat: int) -> dict[str, Any]: 3, ), "enabled_component_count": samples[0]["enabled_component_count"], + "enabled_components": enabled_components, } return { - "schema_version": 1, + "schema_version": 2, "generated_at": datetime.now(timezone.utc).isoformat(), "platform": platform.platform(), "python": platform.python_version(), @@ -251,6 +285,11 @@ def check_baseline( ) -> list[str]: """比较稳定资源契约与宽松耗时预算,返回所有不符合项。""" errors: list[str] = [] + if expected.get("schema_version") != actual.get("schema_version"): + errors.append( + "启动性能基线 schema 版本变化:" + f"{expected.get('schema_version')} -> {actual.get('schema_version')}" + ) expected_targets = expected.get("targets", {}) actual_targets = actual.get("targets", {}) if set(expected_targets) != set(actual_targets): @@ -258,14 +297,13 @@ def check_baseline( for target in sorted(set(expected_targets) & set(actual_targets)): expected_target = expected_targets[target] actual_target = actual_targets[target] - if ( - actual_target["loaded_module_count"] - != expected_target["loaded_module_count"] + if actual_target.get("loaded_app_module_count") != expected_target.get( + "loaded_app_module_count" ): errors.append( - f"{target} 加载模块数变化:" - f"{expected_target['loaded_module_count']} -> " - f"{actual_target['loaded_module_count']}" + f"{target} 加载宿主模块数变化:" + f"{expected_target.get('loaded_app_module_count')} -> " + f"{actual_target.get('loaded_app_module_count')}" ) budget_ms = max( expected_target["max_ms"] * PERFORMANCE_FACTOR, @@ -283,6 +321,10 @@ def check_baseline( for mode_name in sorted(set(expected_modes) & set(actual_modes)): expected_mode = expected_modes[mode_name] actual_mode = actual_modes[mode_name] + if actual_mode.get("enabled_components") != expected_mode.get( + "enabled_components" + ): + errors.append(f"{mode_name} 模式生命周期组件集合或顺序已变化") if ( actual_mode["enabled_component_count"] != expected_mode["enabled_component_count"] diff --git a/tests/fixtures/architecture/async-blocking-baseline.json b/tests/fixtures/architecture/async-blocking-baseline.json index 22e985d53..4fcc9c251 100644 --- a/tests/fixtures/architecture/async-blocking-baseline.json +++ b/tests/fixtures/architecture/async-blocking-baseline.json @@ -1,12 +1,3 @@ { - "app/agent/tools/impl/_plugin_tool_utils.py:uninstall_plugin_runtime:shutil.rmtree": 1, - "app/agent/tools/impl/scrape_metadata.py:ScrapeMetadataTool.run:Path.exists": 1, - "app/agent/tools/impl/scrape_metadata.py:ScrapeMetadataTool.run:Path.is_dir": 1, - "app/chain/media.py:MediaChain._async_music_album_dir_fallback:Path.exists": 1, - "app/chain/media.py:MediaChain._async_music_album_dir_fallback:Path.is_file": 1, - "app/chain/media.py:MediaChain.async_recognize_music_album_directory:Path.is_dir": 1, - "app/modules/acoustid/__init__.py:AcoustIdModule.async_identify_music_by_fingerprint:Path.is_file": 1, - "app/modules/discord/discord.py:Discord._send_file:Path.exists": 1, - "app/modules/discord/discord.py:Discord._send_file:Path.is_file": 1, "app/scheduler.py:Scheduler.execute_agent_task:AgentTaskOper.get": 1 } diff --git a/tests/fixtures/architecture/official-plugin-baseline.json b/tests/fixtures/architecture/official-plugin-baseline.json index af9dc49bc..367010707 100644 --- a/tests/fixtures/architecture/official-plugin-baseline.json +++ b/tests/fixtures/architecture/official-plugin-baseline.json @@ -2679,6 +2679,45 @@ "response_class": null, "response_model": "schemas.Response[List[schemas.MediaInfo]]" } + ], + "plugins/cloudflarespeedtest/__init__.py": [ + { + "auth": null, + "endpoint": "cloudflare_speedtest", + "endpoint_return": "schemas.Response", + "methods": [ + "GET" + ], + "path": "/cloudflare_speedtest", + "response_class": null, + "response_model": null + } + ], + "plugins/linkmonitor/__init__.py": [ + { + "auth": null, + "endpoint": "sync", + "endpoint_return": "schemas.Response", + "methods": [ + "GET" + ], + "path": "/realtime_link", + "response_class": null, + "response_model": null + } + ], + "plugins/wxpushermsg/__init__.py": [ + { + "auth": null, + "endpoint": "run_once", + "endpoint_return": "Dict[str, Any]", + "methods": [ + "GET" + ], + "path": "/run", + "response_class": null, + "response_model": null + } ] }, "hooks": { @@ -2695,7 +2734,7 @@ ] }, "get_api": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -2780,7 +2819,38 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "get_auth_provider": { @@ -2788,7 +2858,7 @@ "files": [] }, "get_command": { - "file_count": 82, + "file_count": 113, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -2871,11 +2941,42 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "get_dashboard": { - "file_count": 10, + "file_count": 13, "files": [ "plugins.v2/agenttokens/__init__.py", "plugins.v2/brushflow/__init__.py", @@ -2886,11 +2987,14 @@ "plugins.v2/sitestatistic/__init__.py", "plugins.v2/tobypasstrackers/__init__.py", "plugins.v3/brushflow/__init__.py", - "plugins.v3/imdbsource/__init__.py" + "plugins.v3/imdbsource/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/trendingshow/__init__.py" ] }, "get_form": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -2975,7 +3079,38 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "get_module": { @@ -2989,7 +3124,7 @@ ] }, "get_page": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -3074,7 +3209,38 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "get_render_mode": { @@ -3095,7 +3261,7 @@ ] }, "get_service": { - "file_count": 48, + "file_count": 55, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/autoauction/__init__.py", @@ -3144,7 +3310,14 @@ "plugins.v3/libraryscraper/__init__.py", "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", - "plugins.v3/rsssubscribe/__init__.py" + "plugins.v3/rsssubscribe/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "get_sidebar": { @@ -3152,7 +3325,7 @@ "files": [] }, "get_state": { - "file_count": 86, + "file_count": 117, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -3239,11 +3412,42 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "init_plugin": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -3328,11 +3532,42 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "stop_service": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -3417,7 +3652,38 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] } }, @@ -3481,7 +3747,7 @@ ] }, "app.chain.download": { - "file_count": 11, + "file_count": 12, "files": [ "plugins.v2/doubanrank/__init__.py", "plugins.v2/doubansync/__init__.py", @@ -3493,11 +3759,12 @@ "plugins.v3/doubansync/__init__.py", "plugins.v3/maoyanrank/__init__.py", "plugins.v3/neodbsync/__init__.py", - "plugins.v3/rsssubscribe/__init__.py" + "plugins.v3/rsssubscribe/__init__.py", + "plugins/downloadingmsg/__init__.py" ] }, "app.chain.media": { - "file_count": 10, + "file_count": 11, "files": [ "plugins.v2/agentresourceofficer/services/hdhive_openapi.py", "plugins.v2/airecognizerenhancer/__init__.py", @@ -3508,7 +3775,8 @@ "plugins.v2/libraryscraper/__init__.py", "plugins.v3/agentresourceofficer/services/hdhive_openapi.py", "plugins.v3/doubanrank/__init__.py", - "plugins.v3/doubansync/__init__.py" + "plugins.v3/doubansync/__init__.py", + "plugins/downloadingmsg/__init__.py" ] }, "app.chain.mediaserver": { @@ -3547,9 +3815,10 @@ ] }, "app.chain.site": { - "file_count": 1, + "file_count": 2, "files": [ - "plugins.v2/sitestatistic/__init__.py" + "plugins.v2/sitestatistic/__init__.py", + "plugins/siterefresh/__init__.py" ] }, "app.chain.storage": { @@ -3581,12 +3850,13 @@ ] }, "app.chain.tmdb": { - "file_count": 4, + "file_count": 5, "files": [ "plugins.v2/maoyanrank/__init__.py", "plugins.v2/personmeta/__init__.py", "plugins.v3/maoyanrank/__init__.py", - "plugins.v3/personmeta/__init__.py" + "plugins.v3/personmeta/__init__.py", + "plugins/trendingshow/__init__.py" ] }, "app.chain.torrents": { @@ -3631,7 +3901,7 @@ ] }, "app.core.config": { - "file_count": 73, + "file_count": 84, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agentresourceofficer/services/hdhive_openapi.py", @@ -3705,11 +3975,22 @@ "plugins.v2/tvfirstwatch/__init__.py", "plugins.v2/updatewechatip/__init__.py", "plugins.v2/wechatclawbot/__init__.py", - "plugins.v3/chatgpt/__init__.py" + "plugins.v3/chatgpt/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/webhook/__init__.py", + "plugins/zvideohelper/DoubanHelper.py", + "plugins/zvideohelper/__init__.py" ] }, "app.core.context": { - "file_count": 17, + "file_count": 18, "files": [ "plugins.v2/blurayremux/__init__.py", "plugins.v2/brushflow/__init__.py", @@ -3727,11 +4008,12 @@ "plugins.v2/playletcategory/__init__.py", "plugins.v2/promotiontag/__init__.py", "plugins.v2/rsssubscribe/__init__.py", - "plugins.v2/tvfirstwatch/__init__.py" + "plugins.v2/tvfirstwatch/__init__.py", + "plugins/autosubv2/__init__.py" ] }, "app.core.event": { - "file_count": 41, + "file_count": 61, "files": [ "plugins.v2/agenttokens/__init__.py", "plugins.v2/airecognizerenhancer/__init__.py", @@ -3773,15 +4055,36 @@ "plugins.v2/updatewechatip/__init__.py", "plugins.v2/wechatclawbot/__init__.py", "plugins.v2/wxpusher/__init__.py", - "plugins.v3/chatgpt/__init__.py" + "plugins.v3/chatgpt/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "app.core.meta": { - "file_count": 3, + "file_count": 4, "files": [ "plugins.v2/blurayremux/__init__.py", "plugins.v2/imdbsource/__init__.py", - "plugins.v2/personmeta/__init__.py" + "plugins.v2/personmeta/__init__.py", + "plugins/zvideohelper/DoubanHelper.py" ] }, "app.core.meta.words": { @@ -3791,7 +4094,7 @@ ] }, "app.core.metainfo": { - "file_count": 11, + "file_count": 12, "files": [ "plugins.v2/airecognizerenhancer/__init__.py", "plugins.v2/blurayremux/__init__.py", @@ -3803,17 +4106,25 @@ "plugins.v2/maoyanrank/__init__.py", "plugins.v2/rsssubscribe/__init__.py", "plugins.v2/traktcleaner/__init__.py", - "plugins.v2/tvfirstwatch/__init__.py" + "plugins.v2/tvfirstwatch/__init__.py", + "plugins/downloadingmsg/__init__.py" + ] + }, + "app.core.module": { + "file_count": 1, + "files": [ + "plugins/ipdetect/__init__.py" ] }, "app.core.plugin": { - "file_count": 5, + "file_count": 6, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agentresourceofficer/agenttool.py", "plugins.v2/agentresourceofficer/services/p115_transfer.py", "plugins.v2/hrblocker/__init__.py", - "plugins.v2/lexiannot/agenttool.py" + "plugins.v2/lexiannot/agenttool.py", + "plugins/cleaninvalidplugin/__init__.py" ] }, "app.db": { @@ -3828,13 +4139,14 @@ ] }, "app.db.downloadhistory_oper": { - "file_count": 5, + "file_count": 6, "files": [ "plugins.v2/autoclean/__init__.py", "plugins.v2/downloadsitetag/__init__.py", "plugins.v2/episodetag/__init__.py", "plugins.v2/qbuploadlimiter/__init__.py", - "plugins.v2/subscribeclear/__init__.py" + "plugins.v2/subscribeclear/__init__.py", + "plugins/downloadingmsg/__init__.py" ] }, "app.db.message_oper": { @@ -3929,7 +4241,7 @@ ] }, "app.db.site_oper": { - "file_count": 10, + "file_count": 11, "files": [ "plugins.v2/autosignin/__init__.py", "plugins.v2/brushflow/__init__.py", @@ -3940,7 +4252,8 @@ "plugins.v2/qbcommand/__init__.py", "plugins.v2/sitestatistic/__init__.py", "plugins.v2/synccookiecloud/__init__.py", - "plugins.v2/tobypasstrackers/__init__.py" + "plugins.v2/tobypasstrackers/__init__.py", + "plugins/siterefresh/__init__.py" ] }, "app.db.subscribe_oper": { @@ -3952,18 +4265,20 @@ ] }, "app.db.systemconfig_oper": { - "file_count": 1, + "file_count": 2, "files": [ - "plugins.v2/airecognizerenhancer/__init__.py" + "plugins.v2/airecognizerenhancer/__init__.py", + "plugins/cleaninvalidplugin/__init__.py" ] }, "app.db.transferhistory_oper": { - "file_count": 4, + "file_count": 5, "files": [ "plugins.v2/autoclean/__init__.py", "plugins.v2/dailysummary/__init__.py", "plugins.v2/libraryscraper/__init__.py", - "plugins.v2/scrapefileclean/__init__.py" + "plugins.v2/scrapefileclean/__init__.py", + "plugins/removelink/__init__.py" ] }, "app.db.user_oper": { @@ -3992,9 +4307,11 @@ ] }, "app.helper.cookiecloud": { - "file_count": 1, + "file_count": 3, "files": [ - "plugins.v2/dynamicwechat/__init__.py" + "plugins.v2/dynamicwechat/__init__.py", + "plugins/customsites/__init__.py", + "plugins/zvideohelper/DoubanHelper.py" ] }, "app.helper.directory": { @@ -4080,7 +4397,7 @@ ] }, "app.helper.sites": { - "file_count": 11, + "file_count": 12, "files": [ "plugins.v2/autoauction/__init__.py", "plugins.v2/autosignin/__init__.py", @@ -4092,7 +4409,8 @@ "plugins.v2/promotiontag/__init__.py", "plugins.v2/qbcommand/__init__.py", "plugins.v2/qbuploadlimiter/__init__.py", - "plugins.v2/sitestatistic/__init__.py" + "plugins.v2/sitestatistic/__init__.py", + "plugins/customindexer/__init__.py" ] }, "app.helper.system": { @@ -4124,7 +4442,7 @@ ] }, "app.log": { - "file_count": 99, + "file_count": 128, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agentresourceofficer/feishu_channel.py", @@ -4224,7 +4542,36 @@ "plugins.v2/wechatclawbot/__init__.py", "plugins.v2/wechatclawbot/ilink/client.py", "plugins.v2/wxpusher/__init__.py", - "plugins.v3/chatgpt/__init__.py" + "plugins.v3/chatgpt/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/DoubanHelper.py", + "plugins/zvideohelper/__init__.py" ] }, "app.modules.bangumi": { @@ -4258,19 +4605,21 @@ ] }, "app.modules.qbittorrent": { - "file_count": 3, + "file_count": 4, "files": [ "plugins.v2/brushflow/__init__.py", "plugins.v2/torrenttransfer/__init__.py", - "plugins.v3/brushflow/__init__.py" + "plugins.v3/brushflow/__init__.py", + "plugins/trackereditor/__init__.py" ] }, "app.modules.themoviedb": { - "file_count": 3, + "file_count": 4, "files": [ "plugins.v2/mediaservermsg/__init__.py", "plugins.v3/bangumicoll/__init__.py", - "plugins.v3/mediaservermsg/__init__.py" + "plugins.v3/mediaservermsg/__init__.py", + "plugins/categoryeditor/__init__.py" ] }, "app.modules.themoviedb.tmdbapi": { @@ -4286,11 +4635,12 @@ ] }, "app.modules.transmission": { - "file_count": 3, + "file_count": 4, "files": [ "plugins.v2/brushflow/__init__.py", "plugins.v2/torrenttransfer/__init__.py", - "plugins.v3/brushflow/__init__.py" + "plugins.v3/brushflow/__init__.py", + "plugins/trackereditor/__init__.py" ] }, "app.modules.wechat": { @@ -4300,7 +4650,7 @@ ] }, "app.plugins": { - "file_count": 84, + "file_count": 115, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -4385,7 +4735,38 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/categoryeditor/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customindexer/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/iyuuauth/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/trackereditor/__init__.py", + "plugins/trendingshow/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "app.plugins.autosignin.sites": { @@ -4445,6 +4826,12 @@ "plugins.v2/imdbsource/__init__.py" ] }, + "app.plugins.iyuuauth.iyuu_helper": { + "file_count": 1, + "files": [ + "plugins/iyuuauth/__init__.py" + ] + }, "app.plugins.iyuuautoseed.iyuu_helper": { "file_count": 1, "files": [ @@ -4458,6 +4845,12 @@ "plugins.v3/agentresourceofficer/services/p115_transfer.py" ] }, + "app.plugins.zvideohelper.DoubanHelper": { + "file_count": 1, + "files": [ + "plugins/zvideohelper/__init__.py" + ] + }, "app.runtime.thread": { "file_count": 2, "files": [ @@ -4466,17 +4859,18 @@ ] }, "app.scheduler": { - "file_count": 5, + "file_count": 6, "files": [ "plugins.v2/brushflow/__init__.py", "plugins.v2/imdbsource/__init__.py", "plugins.v2/tobypasstrackers/__init__.py", "plugins.v3/brushflow/__init__.py", - "plugins.v3/imdbsource/__init__.py" + "plugins.v3/imdbsource/__init__.py", + "plugins/ipdetect/__init__.py" ] }, "app.schemas": { - "file_count": 50, + "file_count": 56, "files": [ "plugins.v2/autoclean/__init__.py", "plugins.v2/blurayremux/__init__.py", @@ -4527,7 +4921,13 @@ "plugins.v3/mediaservermsg/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/ipdetect/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/removelink/__init__.py", + "plugins/trackereditor/__init__.py" ] }, "app.schemas.context": { @@ -4549,7 +4949,7 @@ ] }, "app.schemas.types": { - "file_count": 67, + "file_count": 89, "files": [ "plugins.v2/agentresourceofficer/__init__.py", "plugins.v2/agenttokens/__init__.py", @@ -4617,7 +5017,29 @@ "plugins.v3/neodbsync/__init__.py", "plugins.v3/personmeta/__init__.py", "plugins.v3/rsssubscribe/__init__.py", - "plugins.v3/tvdbdiscover/__init__.py" + "plugins.v3/tvdbdiscover/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/cleaninvalidplugin/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/customsites/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/iyuumsg/__init__.py", + "plugins/linkmonitor/__init__.py", + "plugins/ntfymsg/__init__.py", + "plugins/pushdeermsg/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/removelink/__init__.py", + "plugins/siterefresh/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/__init__.py" ] }, "app.sdk.browser": { @@ -4784,11 +5206,12 @@ ] }, "app.utils.common": { - "file_count": 3, + "file_count": 4, "files": [ "plugins.v2/imdbsource/imdbapi.py", "plugins.v2/imdbsource/officialapi.py", - "plugins.v2/personmeta/__init__.py" + "plugins.v2/personmeta/__init__.py", + "plugins/remoteidentifiers/__init__.py" ] }, "app.utils.crypto": { @@ -4805,7 +5228,7 @@ ] }, "app.utils.http": { - "file_count": 45, + "file_count": 61, "files": [ "plugins.v2/animeupscale/__init__.py", "plugins.v2/autoauction/__init__.py", @@ -4851,13 +5274,31 @@ "plugins.v2/tvdbdiscover/__init__.py", "plugins.v2/wechatclawbot/__init__.py", "plugins.v2/wechatclawbot/ilink/client.py", - "plugins.v2/wxpusher/__init__.py" + "plugins.v2/wxpusher/__init__.py", + "plugins/apprisemsg/__init__.py", + "plugins/barkmsg/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/dailyword/__init__.py", + "plugins/dingdingmsg/__init__.py", + "plugins/feishumsg/__init__.py", + "plugins/gotifymsg/__init__.py", + "plugins/iyuuauth/iyuu_helper.py", + "plugins/iyuumsg/__init__.py", + "plugins/mpserverstatus/__init__.py", + "plugins/pushplusmsg/__init__.py", + "plugins/remoteidentifiers/__init__.py", + "plugins/webhook/__init__.py", + "plugins/workwechatmsg/__init__.py", + "plugins/wxpushermsg/__init__.py", + "plugins/zvideohelper/DoubanHelper.py" ] }, "app.utils.ip": { - "file_count": 1, + "file_count": 3, "files": [ - "plugins.v2/speedlimiter/__init__.py" + "plugins.v2/speedlimiter/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py" ] }, "app.utils.singleton": { @@ -4874,7 +5315,7 @@ ] }, "app.utils.string": { - "file_count": 43, + "file_count": 45, "files": [ "plugins.v2/autosignin/__init__.py", "plugins.v2/autosignin/sites/52pt.py", @@ -4918,17 +5359,23 @@ "plugins.v2/sitestatistic/__init__.py", "plugins.v2/torrentremover/__init__.py", "plugins.v2/torrenttransfer/__init__.py", - "plugins.v2/wechatclawbot/__init__.py" + "plugins.v2/wechatclawbot/__init__.py", + "plugins/downloadingmsg/__init__.py", + "plugins/siterefresh/__init__.py" ] }, "app.utils.system": { - "file_count": 5, + "file_count": 9, "files": [ "plugins.v2/chinesesubfinder/__init__.py", "plugins.v2/ffmpegthumb/__init__.py", "plugins.v2/ffmpegthumb/ffmpeg_helper.py", "plugins.v2/libraryscraper/__init__.py", - "plugins.v2/playletcategory/__init__.py" + "plugins.v2/playletcategory/__init__.py", + "plugins/autosubv2/__init__.py", + "plugins/cloudflarespeedtest/__init__.py", + "plugins/customhosts/__init__.py", + "plugins/linkmonitor/__init__.py" ] }, "app.utils.timer": { @@ -4948,15 +5395,49 @@ }, "provenance": { "head": "7d2d676d6f5139227050e9e71bd579943f45c0e8", - "python_file_count": 238, - "source_sha256": "f07ff2e8c95080cbbf6d229376464e61dc74061c1a8d371b5ea2cf00148413de" + "python_file_count": 274, + "source_sha256": "b1e3a6d7bfdd78eec1377a7b42f7482b9720324a3403b21ad6767788b054b727" }, "schema_version": 3, "scope": { + "default_plugins": [ + "AppriseMsg", + "AutoSubv2", + "BarkMsg", + "CategoryEditor", + "CleanInvalidPlugin", + "CloudflareSpeedTest", + "CustomHosts", + "CustomIndexer", + "CustomSites", + "DailyWord", + "DingdingMsg", + "DownloadingMsg", + "FeiShuMsg", + "GotifyMsg", + "IpDetect", + "IyuuAuth", + "IyuuMsg", + "LinkMonitor", + "MPServerStatus", + "NtfyMsg", + "PushDeerMsg", + "PushPlusMsg", + "RemoteIdentifiers", + "RemoveLink", + "SiteRefresh", + "TrackerEditor", + "TrendingShow", + "WebHook", + "WorkWechatMsg", + "WxPusherMsg", + "ZvideoHelper" + ], "repository": "MoviePilot-Plugins", "roots": [ "plugins.v2", - "plugins.v3" + "plugins.v3", + "plugins" ] } } diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 660b32ec4..3fe8cdd28 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -7443,16 +7443,6 @@ "sdk_exports": { "app.sdk": [], "app.sdk.browser": [ - { - "kind": "import", - "name": "Any", - "target": "typing.Any" - }, - { - "kind": "import", - "name": "annotations", - "target": "__future__.annotations" - }, { "kind": "FunctionDef", "name": "launch_browser_context", @@ -8040,275 +8030,10 @@ } ], "app.sdk.string": [ - { - "kind": "import", - "name": "Callable", - "target": "typing.Callable" - }, - { - "kind": "import", - "name": "DomUtils", - "target": "app.foundation.dom.DomUtils" - }, - { - "kind": "import", - "name": "HashUtils", - "target": "app.foundation.crypto.HashUtils" - }, { "kind": "ClassDef", "name": "StringUtils", "target": "" - }, - { - "kind": "import", - "name": "base_url", - "target": "app.foundation.url.base_url" - }, - { - "kind": "import", - "name": "common_prefix", - "target": "app.foundation.text.common_prefix" - }, - { - "kind": "import", - "name": "compact_numbers", - "target": "app.domain.episode.compact_numbers" - }, - { - "kind": "import", - "name": "compare_version", - "target": "app.foundation.version.compare_version" - }, - { - "kind": "import", - "name": "contains_chinese", - "target": "app.foundation.text.contains_chinese" - }, - { - "kind": "import", - "name": "contains_japanese", - "target": "app.foundation.text.contains_japanese" - }, - { - "kind": "import", - "name": "contains_korean", - "target": "app.foundation.text.contains_korean" - }, - { - "kind": "import", - "name": "cookiejar_to_string", - "target": "app.foundation.text.cookiejar_to_string" - }, - { - "kind": "import", - "name": "count_words", - "target": "app.foundation.text.count_words" - }, - { - "kind": "import", - "name": "escape_markdown", - "target": "app.foundation.text.escape_markdown" - }, - { - "kind": "import", - "name": "extract_domain", - "target": "app.domain.site.extract_domain" - }, - { - "kind": "import", - "name": "extract_named_ids", - "target": "app.foundation.text.extract_named_ids" - }, - { - "kind": "import", - "name": "format_amount", - "target": "app.foundation.text.format_amount" - }, - { - "kind": "import", - "name": "format_approx_duration", - "target": "app.foundation.temporal.format_approx_duration" - }, - { - "kind": "import", - "name": "format_compact_size", - "target": "app.foundation.size.format_compact_size" - }, - { - "kind": "import", - "name": "format_duration", - "target": "app.foundation.temporal.format_duration" - }, - { - "kind": "import", - "name": "format_minutes", - "target": "app.foundation.temporal.format_minutes" - }, - { - "kind": "import", - "name": "format_ranges", - "target": "app.domain.episode.format_ranges" - }, - { - "kind": "import", - "name": "format_remaining", - "target": "app.foundation.temporal.format_remaining" - }, - { - "kind": "import", - "name": "format_size", - "target": "app.foundation.size.format_size" - }, - { - "kind": "import", - "name": "format_timestamp", - "target": "app.foundation.temporal.format_timestamp" - }, - { - "kind": "import", - "name": "host_label", - "target": "app.foundation.url.host_label" - }, - { - "kind": "import", - "name": "is_all_chinese", - "target": "app.foundation.text.is_all_chinese" - }, - { - "kind": "import", - "name": "is_english_word", - "target": "app.foundation.text.is_english_word" - }, - { - "kind": "import", - "name": "is_link", - "target": "app.foundation.url.is_link" - }, - { - "kind": "import", - "name": "is_magnet_link", - "target": "app.domain.torrent.is_magnet_link" - }, - { - "kind": "import", - "name": "is_media_title_like", - "target": "app.domain.title.is_media_title_like" - }, - { - "kind": "import", - "name": "is_number", - "target": "app.foundation.text.is_number" - }, - { - "kind": "import", - "name": "natural_sort_key", - "target": "app.foundation.text.natural_sort_key" - }, - { - "kind": "import", - "name": "normalize_datetime", - "target": "app.foundation.temporal.normalize_datetime" - }, - { - "kind": "import", - "name": "normalize_upper", - "target": "app.foundation.text.normalize_upper" - }, - { - "kind": "import", - "name": "parse_address", - "target": "app.foundation.url.parse_address" - }, - { - "kind": "import", - "name": "parse_bool", - "target": "app.foundation.text.parse_bool" - }, - { - "kind": "import", - "name": "parse_datetime", - "target": "app.foundation.temporal.parse_datetime" - }, - { - "kind": "import", - "name": "parse_float", - "target": "app.foundation.text.parse_float" - }, - { - "kind": "import", - "name": "parse_int", - "target": "app.foundation.text.parse_int" - }, - { - "kind": "import", - "name": "parse_search_keyword", - "target": "app.domain.title.parse_search_keyword" - }, - { - "kind": "import", - "name": "parse_size", - "target": "app.foundation.size.parse_size" - }, - { - "kind": "import", - "name": "parse_timestamp", - "target": "app.foundation.temporal.parse_timestamp" - }, - { - "kind": "import", - "name": "random_string", - "target": "app.foundation.text.random_string" - }, - { - "kind": "import", - "name": "remove_punctuation", - "target": "app.foundation.text.remove_punctuation" - }, - { - "kind": "import", - "name": "sanitize_filename", - "target": "app.foundation.text.sanitize_filename" - }, - { - "kind": "import", - "name": "second_level_label", - "target": "app.foundation.url.second_level_label" - }, - { - "kind": "import", - "name": "signature", - "target": "inspect.signature" - }, - { - "kind": "import", - "name": "split_by_bytes", - "target": "app.foundation.text.split_by_bytes" - }, - { - "kind": "import", - "name": "split_netloc", - "target": "app.foundation.url.split_netloc" - }, - { - "kind": "import", - "name": "strip_optional", - "target": "app.foundation.text.strip_optional" - }, - { - "kind": "import", - "name": "title_case", - "target": "app.foundation.text.title_case" - }, - { - "kind": "import", - "name": "urls_match", - "target": "app.domain.site.urls_match" - }, - { - "kind": "import", - "name": "wraps", - "target": "functools.wraps" } ], "app.sdk.utilities": [ @@ -8362,6 +8087,16 @@ "name": "cut", "target": "app.foundation.text.cut" }, + { + "kind": "alias", + "name": "decrypt", + "target": "app.foundation.crypto.CryptoJsUtils.decrypt" + }, + { + "kind": "alias", + "name": "encrypt", + "target": "app.foundation.crypto.CryptoJsUtils.encrypt" + }, { "kind": "import", "name": "log_execution_time", diff --git a/tests/fixtures/architecture/startup-performance-baseline.json b/tests/fixtures/architecture/startup-performance-baseline.json index ba949f20e..fa0230eed 100644 --- a/tests/fixtures/architecture/startup-performance-baseline.json +++ b/tests/fixtures/architecture/startup-performance-baseline.json @@ -1,41 +1,41 @@ { - "schema_version": 1, - "generated_at": "2026-08-21T12:02:30.246931+00:00", - "platform": "macOS-26.5.2-arm64-arm-64bit", - "python": "3.12.6", + "schema_version": 2, + "generated_at": "2026-08-23T08:11:40.406030+00:00", + "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O", + "python": "3.14.3", "repeat": 3, "targets": { "app.startup.lifecycle": { - "loaded_module_count": 1810, - "max_ms": 987.312, - "median_ms": 984.741, - "min_ms": 983.917, + "loaded_app_module_count": 362, + "max_ms": 944.347, + "median_ms": 935.035, + "min_ms": 933.625, "samples_ms": [ - 987.312, - 984.741, - 983.917 + 944.347, + 935.035, + 933.625 ] }, "app.factory": { - "loaded_module_count": 1820, - "max_ms": 1015.426, - "median_ms": 999.369, - "min_ms": 996.521, + "loaded_app_module_count": 374, + "max_ms": 950.655, + "median_ms": 950.429, + "min_ms": 947.964, "samples_ms": [ - 996.521, - 999.369, - 1015.426 + 950.655, + 947.964, + 950.429 ] }, "app.main": { - "loaded_module_count": 1844, - "max_ms": 1057.647, - "median_ms": 1037.08, - "min_ms": 1034.011, + "loaded_app_module_count": 376, + "max_ms": 1150.269, + "median_ms": 1103.976, + "min_ms": 1097.978, "samples_ms": [ - 1057.647, - 1034.011, - 1037.08 + 1103.976, + 1097.978, + 1150.269 ] } }, @@ -46,55 +46,26 @@ "samples": [ { "mode": "normal", - "enabled_component_count": 15, - "startup_ms": 0.569, - "full_lifespan_ms": 0.669, + "enabled_component_count": 16, + "startup_ms": 0.721, + "full_lifespan_ms": 0.907, "stage_ms": { - "数据库准备": 0.08, - "HTTP 基础能力": 0.035, - "领域依赖装配": 0.028, - "数据库引擎预热": 0.029, - "数据库连接预算": 0.025, - "路由": 0.024, + "后台任务登记器": 0.084, + "数据库准备": 0.038, + "HTTP 基础能力": 0.028, + "领域依赖装配": 0.027, + "数据库引擎预热": 0.024, + "数据库连接预算": 0.024, + "路由": 0.023, "模块服务": 0.024, "插件备份恢复": 0.023, - "插件": 0.021, - "定时器": 0.019, - "监控器": 0.022, - "待处理整理回放": 0.021, - "命令服务": 0.019, - "工作流": 0.021, - "插件同步与启动收尾": 0.032 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 2, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "normal", - "enabled_component_count": 15, - "startup_ms": 0.548, - "full_lifespan_ms": 0.641, - "stage_ms": { - "数据库准备": 0.071, - "HTTP 基础能力": 0.033, - "领域依赖装配": 0.032, - "数据库引擎预热": 0.028, - "数据库连接预算": 0.025, - "路由": 0.022, - "模块服务": 0.023, - "插件备份恢复": 0.019, - "插件": 0.02, - "定时器": 0.022, + "插件": 0.029, + "定时器": 0.023, "监控器": 0.024, - "待处理整理回放": 0.019, - "命令服务": 0.022, - "工作流": 0.022, - "插件同步与启动收尾": 0.028 + "待处理整理回放": 0.021, + "命令服务": 0.025, + "工作流": 0.02, + "插件同步与启动收尾": 0.031 }, "threads_before": 2, "threads_started": 2, @@ -106,25 +77,57 @@ }, { "mode": "normal", - "enabled_component_count": 15, - "startup_ms": 0.687, - "full_lifespan_ms": 0.795, + "enabled_component_count": 16, + "startup_ms": 0.614, + "full_lifespan_ms": 0.788, "stage_ms": { - "数据库准备": 0.083, - "HTTP 基础能力": 0.035, - "领域依赖装配": 0.028, - "数据库引擎预热": 0.033, - "数据库连接预算": 0.027, - "路由": 0.021, - "模块服务": 0.019, - "插件备份恢复": 0.064, - "插件": 0.032, + "后台任务登记器": 0.075, + "数据库准备": 0.037, + "HTTP 基础能力": 0.029, + "领域依赖装配": 0.027, + "数据库引擎预热": 0.023, + "数据库连接预算": 0.024, + "路由": 0.023, + "模块服务": 0.023, + "插件备份恢复": 0.023, + "插件": 0.024, "定时器": 0.025, - "监控器": 0.028, - "待处理整理回放": 0.025, + "监控器": 0.023, + "待处理整理回放": 0.022, + "命令服务": 0.021, + "工作流": 0.022, + "插件同步与启动收尾": 0.03 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 2, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "normal", + "enabled_component_count": 16, + "startup_ms": 0.611, + "full_lifespan_ms": 0.785, + "stage_ms": { + "后台任务登记器": 0.072, + "数据库准备": 0.033, + "HTTP 基础能力": 0.026, + "领域依赖装配": 0.028, + "数据库引擎预热": 0.023, + "数据库连接预算": 0.022, + "路由": 0.024, + "模块服务": 0.02, + "插件备份恢复": 0.023, + "插件": 0.024, + "定时器": 0.026, + "监控器": 0.024, + "待处理整理回放": 0.023, "命令服务": 0.024, - "工作流": 0.024, - "插件同步与启动收尾": 0.037 + "工作流": 0.022, + "插件同步与启动收尾": 0.031 }, "threads_before": 2, "threads_started": 2, @@ -135,20 +138,63 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.569, - "median_full_lifespan_ms": 0.669, - "enabled_component_count": 15 + "median_startup_ms": 0.614, + "median_full_lifespan_ms": 0.788, + "enabled_component_count": 16, + "enabled_components": [ + "后台任务登记器", + "数据库准备", + "HTTP 基础能力", + "领域依赖装配", + "数据库引擎预热", + "数据库连接预算", + "路由", + "模块服务", + "插件备份恢复", + "插件", + "定时器", + "监控器", + "待处理整理回放", + "命令服务", + "工作流", + "插件备份" + ] }, "safe": { "samples": [ { "mode": "safe", - "enabled_component_count": 7, - "startup_ms": 0.408, - "full_lifespan_ms": 0.531, + "enabled_component_count": 8, + "startup_ms": 0.463, + "full_lifespan_ms": 0.667, "stage_ms": { - "数据库准备": 0.077, - "HTTP 基础能力": 0.035, + "后台任务登记器": 0.075, + "数据库准备": 0.035, + "HTTP 基础能力": 0.03, + "领域依赖装配": 0.032, + "数据库引擎预热": 0.024, + "数据库连接预算": 0.022, + "路由": 0.025, + "模块服务": 0.022, + "插件同步与启动收尾": 0.058 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 2, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "safe", + "enabled_component_count": 8, + "startup_ms": 0.474, + "full_lifespan_ms": 0.68, + "stage_ms": { + "后台任务登记器": 0.076, + "数据库准备": 0.043, + "HTTP 基础能力": 0.029, "领域依赖装配": 0.028, "数据库引擎预热": 0.026, "数据库连接预算": 0.024, @@ -166,41 +212,19 @@ }, { "mode": "safe", - "enabled_component_count": 7, - "startup_ms": 0.416, - "full_lifespan_ms": 0.534, + "enabled_component_count": 8, + "startup_ms": 0.454, + "full_lifespan_ms": 0.657, "stage_ms": { - "数据库准备": 0.079, - "HTTP 基础能力": 0.036, - "领域依赖装配": 0.029, - "数据库引擎预热": 0.027, - "数据库连接预算": 0.026, - "路由": 0.026, - "模块服务": 0.021, - "插件同步与启动收尾": 0.054 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 2, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "safe", - "enabled_component_count": 7, - "startup_ms": 0.397, - "full_lifespan_ms": 0.52, - "stage_ms": { - "数据库准备": 0.07, - "HTTP 基础能力": 0.033, - "领域依赖装配": 0.026, - "数据库引擎预热": 0.027, - "数据库连接预算": 0.029, + "后台任务登记器": 0.075, + "数据库准备": 0.036, + "HTTP 基础能力": 0.028, + "领域依赖装配": 0.028, + "数据库引擎预热": 0.025, + "数据库连接预算": 0.024, "路由": 0.024, - "模块服务": 0.022, - "插件同步与启动收尾": 0.051 + "模块服务": 0.024, + "插件同步与启动收尾": 0.06 }, "threads_before": 2, "threads_started": 2, @@ -211,9 +235,19 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.408, - "median_full_lifespan_ms": 0.531, - "enabled_component_count": 7 + "median_startup_ms": 0.463, + "median_full_lifespan_ms": 0.667, + "enabled_component_count": 8, + "enabled_components": [ + "后台任务登记器", + "数据库准备", + "HTTP 基础能力", + "领域依赖装配", + "数据库引擎预热", + "数据库连接预算", + "路由", + "模块服务" + ] } } } diff --git a/tests/test_architecture_baseline_cli.py b/tests/test_architecture_baseline_cli.py index 7069fdfb1..098e6cf7d 100644 --- a/tests/test_architecture_baseline_cli.py +++ b/tests/test_architecture_baseline_cli.py @@ -9,16 +9,20 @@ from scripts.architecture import baseline as architecture_baseline from scripts.startup import performance as startup_performance -def _performance_sample(*, loaded_module_count: int = 10) -> dict: +def _performance_sample( + *, + loaded_app_module_count: int = 5, +) -> dict: + """构造区分环境诊断值与稳定宿主导入契约的性能样本。""" return { - "schema_version": 1, + "schema_version": 2, "generated_at": "2026-08-21T00:00:00+00:00", "platform": "test", "python": "3.12", "repeat": 1, "targets": { "app.factory": { - "loaded_module_count": loaded_module_count, + "loaded_app_module_count": loaded_app_module_count, "max_ms": 100.0, "median_ms": 90.0, "min_ms": 80.0, @@ -30,6 +34,7 @@ def _performance_sample(*, loaded_module_count: int = 10) -> dict: "modes": { "normal": { "enabled_component_count": 2, + "enabled_components": ["后台任务登记器", "路由"], "samples": [ { "threads_before": 1, @@ -215,6 +220,111 @@ def test_plugin_provenance_does_not_participate_in_semantic_gate(tmp_path: Path) ) == architecture_baseline.semantic_baseline(baseline_path, second) +def test_collect_sdk_exports_uses_explicit_all_and_resolves_aliases( + tmp_path: Path, + monkeypatch, +) -> None: + """SDK 快照只记录 __all__,并把顶层赋值恢复为真实导出目标。""" + sdk_root = tmp_path / "app" / "sdk" + sdk_root.mkdir(parents=True) + (sdk_root / "demo.py").write_text( + "from __future__ import annotations\n" + "from typing import Any\n" + "from app.foundation.crypto import CryptoJsUtils\n\n" + "encrypt = CryptoJsUtils.encrypt\n\n" + "def launch():\n" + " return None\n\n" + "def accidental():\n" + " return None\n\n" + "__all__ = ['CryptoJsUtils', 'encrypt', 'launch']\n", + encoding="utf-8", + ) + monkeypatch.setattr(architecture_baseline, "APP_ROOT", tmp_path / "app") + + exports = architecture_baseline.collect_sdk_exports()["app.sdk.demo"] + + assert exports == [ + { + "name": "CryptoJsUtils", + "kind": "import", + "target": "app.foundation.crypto.CryptoJsUtils", + }, + { + "name": "encrypt", + "kind": "alias", + "target": "app.foundation.crypto.CryptoJsUtils.encrypt", + }, + {"name": "launch", "kind": "FunctionDef", "target": ""}, + ] + + +def test_collect_sdk_exports_treats_missing_all_as_no_public_contract( + tmp_path: Path, + monkeypatch, +) -> None: + """未显式声明 __all__ 的 SDK 模块不得泄漏实现期导入。""" + sdk_root = tmp_path / "app" / "sdk" + sdk_root.mkdir(parents=True) + (sdk_root / "internal.py").write_text( + "from typing import Any\n\n" + "def helper():\n" + " return Any\n", + encoding="utf-8", + ) + monkeypatch.setattr(architecture_baseline, "APP_ROOT", tmp_path / "app") + + assert architecture_baseline.collect_sdk_exports() == {"app.sdk.internal": []} + + +def test_official_plugin_baseline_includes_effective_v3_default_fallback( + tmp_path: Path, +) -> None: + """V3 插件快照应补入专用索引未接管的 package.json 兼容实现。""" + plugin_repo = tmp_path / "MoviePilot-Plugins" + sources = { + "plugins.v3/current/__init__.py": "from app.sdk.events import Event\n", + "plugins.v2/legacy/__init__.py": "from app.core.event import Event\n", + "plugins/shared/__init__.py": "from app.log import logger\n", + "plugins/current/__init__.py": "from app.unused.current import Value\n", + "plugins/legacy/__init__.py": "from app.unused.legacy import Value\n", + "plugins/rejected/__init__.py": "from app.unused.rejected import Value\n", + } + for relative, source in sources.items(): + path = plugin_repo / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source, encoding="utf-8") + (plugin_repo / "package.v3.json").write_text( + json.dumps({"Current": {"version": "3.0.0"}}), encoding="utf-8" + ) + (plugin_repo / "package.v2.json").write_text( + json.dumps({"Legacy": {"version": "2.0.0"}}), encoding="utf-8" + ) + (plugin_repo / "package.json").write_text( + json.dumps( + { + "Current": {"v2": True}, + "Legacy": {"v2": True}, + "Shared": {"v2": True}, + "Rejected": {"v2": True, "v3": False}, + } + ), + encoding="utf-8", + ) + + baseline = architecture_baseline.collect_official_plugin_baseline(plugin_repo) + + assert baseline["scope"] == { + "repository": "MoviePilot-Plugins", + "roots": ["plugins.v2", "plugins.v3", "plugins"], + "default_plugins": ["Shared"], + } + assert baseline["provenance"]["python_file_count"] == 3 + assert baseline["imports"]["app.log"]["files"] == [ + "plugins/shared/__init__.py" + ] + assert not any(module.startswith("app.unused") for module in baseline["imports"]) + + def test_plugin_v2_fixture_migrates_before_semantic_comparison(tmp_path: Path): """旧插件 fixture 应在内存中迁移,不能因 schema 排列变化误报 ABI。""" baseline_path = tmp_path / "official-plugin-baseline.json" @@ -506,11 +616,47 @@ def test_performance_write_requires_explicit_action(tmp_path: Path, monkeypatch) assert json.loads(output.read_text(encoding="utf-8")) == sample -def test_performance_check_reports_loaded_module_drift(): - """稳定的冷导入模块数量变化必须产生可诊断失败。""" - expected = _performance_sample(loaded_module_count=10) - actual = _performance_sample(loaded_module_count=11) +def test_performance_check_reports_loaded_app_module_drift(): + """稳定的宿主冷导入模块数量变化必须产生可诊断失败。""" + expected = _performance_sample(loaded_app_module_count=5) + actual = _performance_sample(loaded_app_module_count=6) errors = startup_performance.check_baseline(expected, actual) - assert errors == ["app.factory 加载模块数变化:10 -> 11"] + assert errors == ["app.factory 加载宿主模块数变化:5 -> 6"] + + +def test_performance_check_ignores_environment_provenance_drift(): + """Python 与平台 provenance 差异不应改变宿主冷导入语义契约。""" + expected = _performance_sample() + actual = _performance_sample() + actual["python"] = "3.14" + actual["platform"] = "another-platform" + + assert startup_performance.check_baseline(expected, actual) == [] + + +def test_performance_check_reports_lifecycle_component_replacement(): + """组件数量未变时,生命周期成员或顺序变化仍必须失败。""" + expected = _performance_sample() + actual = _performance_sample() + actual["lifecycle"]["modes"]["normal"]["enabled_components"] = [ + "后台任务登记器", + "数据库准备", + ] + + errors = startup_performance.check_baseline(expected, actual) + + assert errors == ["normal 模式生命周期组件集合或顺序已变化"] + + +@pytest.mark.parametrize("safe_mode", (False, True)) +def test_performance_lifecycle_probe_preserves_task_registry(safe_mode: bool): + """隔离探针必须真实装配并释放 lifespan 依赖的 TaskRegistry。""" + sample = startup_performance.measure_lifecycle(safe_mode) + + assert "后台任务登记器" in sample["enabled_components"] + assert sample["enabled_component_count"] == len(sample["enabled_components"]) + assert sample["threads_after"] == sample["threads_before"] + assert sample["tasks_after"] == sample["tasks_before"] + assert sample["database_connections_started"] == 0 diff --git a/tests/test_architecture_ci.py b/tests/test_architecture_ci.py index 73a5f73a4..a06f44955 100644 --- a/tests/test_architecture_ci.py +++ b/tests/test_architecture_ci.py @@ -33,6 +33,7 @@ def test_unit_test_workflow_has_independent_host_architecture_gate(): assert "tests/test_architecture_dependencies.py" in commands assert "tests/test_architecture_contract_baseline.py" in commands assert "scripts/architecture/baseline.py --check-host" in commands + assert "scripts/startup/performance.py --check --repeat 3" in commands def test_official_plugin_observation_is_scheduled_and_never_writes_fixture(): diff --git a/tests/test_architecture_contract_baseline.py b/tests/test_architecture_contract_baseline.py index e0f42b8fa..1e3902c93 100644 --- a/tests/test_architecture_contract_baseline.py +++ b/tests/test_architecture_contract_baseline.py @@ -46,7 +46,8 @@ def test_official_plugin_baseline_records_external_source(): assert baseline["schema_version"] == 3 assert baseline["scope"]["repository"] == "MoviePilot-Plugins" - assert baseline["scope"]["roots"] == ["plugins.v2", "plugins.v3"] + assert baseline["scope"]["roots"] == ["plugins.v2", "plugins.v3", "plugins"] + assert baseline["scope"]["default_plugins"] assert len(baseline["provenance"]["head"]) == 40 assert all( not path.startswith("app/plugins/") @@ -99,6 +100,7 @@ def test_startup_performance_baseline_records_all_cold_import_targets(): baseline_path = BASELINE_ROOT / "startup-performance-baseline.json" baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + assert baseline["schema_version"] == 2 assert baseline["repeat"] >= 3 assert set(baseline["targets"]) == { "app.startup.lifecycle", @@ -108,7 +110,7 @@ def test_startup_performance_baseline_records_all_cold_import_targets(): for contract in baseline["targets"].values(): assert len(contract["samples_ms"]) == baseline["repeat"] assert contract["min_ms"] <= contract["median_ms"] <= contract["max_ms"] - assert contract["loaded_module_count"] > 0 + assert contract["loaded_app_module_count"] > 0 def test_runtime_contract_baseline_excludes_diagnostic_line_numbers(): @@ -202,6 +204,8 @@ def test_startup_performance_baseline_records_normal_and_safe_lifecycle_resource safe = lifecycle["modes"]["safe"] assert normal["enabled_component_count"] > safe["enabled_component_count"] for mode in (normal, safe): + assert "后台任务登记器" in mode["enabled_components"] + assert mode["enabled_component_count"] == len(mode["enabled_components"]) assert len(mode["samples"]) == baseline["repeat"] for sample in mode["samples"]: assert sample["threads_after"] == sample["threads_before"]