diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index 523f9d7d9..598a632d0 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -165,7 +165,7 @@ MoviePilot V3 当前不是“目录混乱、必须推倒重来”的状态。第 1. **架构基线工具的事实源分离已完成。**宿主、官方插件和启动性能使用独立 check/write;稳定语义与源码位置诊断分开,配置/事务低水位采用单向 ratchet,不能再用一次全量 `--write` 掩盖跨域变化。 2. **V3 部署拓扑边界已完成。**全功能模式在 startup、launcher 和 Doctor 共同拒绝 `API_WORKERS > 1`,生产入口固定单 worker;开发 reload/监督模式使用 `app.factory:create_app` import-string factory,不再把 app 实例交给多进程 supervisor。旧配置键继续可解析,未来只有拆出 control role 后才重新评估全功能多 worker。 3. **事务所有权已完成装饰器层收口,但 ORM 对象跨层流转仍需治理。**正式 Model 查询/写装饰器均已清零,宿主 Oper 查询统一接收显式 Session;调用方仍需继续明确 ORM 对象生命周期、懒加载和业务提交后副作用边界。 -4. **组合根之后仍存在全局服务定位,但配置直连债务已清零。**canonical 未批准 Settings 导入与非组合根 `SystemConfigOper()` 构造均为 `0`;数据库基础设施 3 处和 startup 唯一构造点作为不可扩张边界登记。Singleton、模块级 provider 和 API 数据端口注册表仍需按消费者切片收口。 +4. **组合根之后仍存在全局服务定位,但配置和 API 数据主路径已收口。**canonical 未批准 Settings 导入与非组合根 `SystemConfigOper()` 构造均为 `0`;数据库基础设施 3 处和 startup 唯一构造点作为不可扩张边界登记。正式 FastAPI 依赖只读取 AppState `HostRuntime` 的命名领域,字符串 API 数据注册表仅允许 startup 注入和旧 Facade 转发;后续对象是 Singleton 与模块级 `configure/get` provider,不应再迁移已类型化 API 依赖。 5. **模块契约仍在推进,Event Registry 已完成全量登记。**当前 212 个模块 spec 中 168 个仍使用 legacy aggregation,44 个已使用可执行聚合/接力语义;53 个事件全部绑定 typed payload,可见性、投递等级、错误行为和敏感字段均有基线,legacy event payload 为 `0`。后续重点是模块能力逐族收口和 6 个 durable-required 事件的真实持久投递,不是重复创建事件 DTO。 6. **后台副作用缺少统一可靠性定义。**事件队列、APScheduler、FastAPI BackgroundTasks 和线程池任务的丢失、重试、幂等、关停语义各不相同;数据库提交与事件/上报之间仍有进程崩溃窗口。 7. **核心关联与健康边界已落地,指标导出仍未收口。**HTTP/SSE correlation ID 已传播到线程池、事件、工作流、子进程、外部请求和日志;`/health/live`、`/health/ready` 已由部署入口消费,事件/数据库队列深度及模块/事件耗时使用低基数指标登记。当前缺口是稳定 exporter、运维查询面和跨进程聚合,而不是重新实现 request ID 或健康路由。 diff --git a/tests/test_host_runtime_context.py b/tests/test_host_runtime_context.py index f4f4dd338..72df803c7 100644 --- a/tests/test_host_runtime_context.py +++ b/tests/test_host_runtime_context.py @@ -207,28 +207,23 @@ def test_fastapi_dependencies_use_fake_runtime_without_real_services() -> None: assert response.json() == {"same_session": True, "has_persistence": True} -def test_official_api_dependencies_do_not_use_string_data_locator() -> None: - """正式业务依赖只能读取 HostRuntime 命名领域,禁止回退字符串注册表。""" - dependency_root = PROJECT_ROOT / "app" / "api" / "dependencies" - official_modules = { - "agent.py", - "auth.py", - "history.py", - "site.py", - "subscription.py", - "workflow.py", - } - for filename in official_modules: - tree = ast.parse( - (dependency_root / filename).read_text(encoding="utf-8") - ) +def test_string_api_data_locator_is_confined_to_compatibility_boundary() -> None: + """字符串数据注册表只能由 startup 注入并经旧 Facade 转发。""" + importers = set() + for path in (PROJECT_ROOT / "app").rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) imported_modules = { node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module } - assert "app.api.data" not in imported_modules - assert "app.api.dependencies.data" not in imported_modules + if imported_modules & {"app.api.data", "app.api.dependencies.data"}: + importers.add(path.relative_to(PROJECT_ROOT).as_posix()) + + assert importers == { + "app/api/dependencies/data.py", + "app/startup/initializers/modules.py", + } @pytest.mark.asyncio