refactor: unify runtime facade governance

This commit is contained in:
jxxghp
2026-08-24 12:26:49 +08:00
parent feff545b16
commit 63cc2c8d4d
7 changed files with 316 additions and 175 deletions
+3
View File
@@ -62,6 +62,9 @@ jobs:
- name: Check background task ownership
run: uv run --locked --no-sync python scripts/architecture/task_ownership.py
- name: Check process runtime service locators
run: uv run --locked --no-sync python scripts/architecture/service_locator.py
- name: Check startup performance contract
run: >-
uv run --locked --no-sync python
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `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/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务;阶段 32 已清除事件调度器绕过生命周期 owner 的投递回退;阶段 33 已统一宿主 Agent 运行时的获取路径;阶段 34 已统一 durable-required 事件与 Outbox topic 事实源;阶段 35 已统一 LLM provider 管理 API 的运行时解析路径;阶段 36 已统一 WebAgent 音频能力访问边界;阶段 37 已统一插件输入事件发布路径;阶段 38 已统一 WebAgent 通知事件监听与队列边界;阶段 39 已补齐搜索 SSE 断线时的上游任务清理;阶段 40 已补齐异步防抖取消的终态所有权;阶段 41 已统一优雅重启兜底线程的唯一所有权;阶段 42 已补齐 Telegram typing 的多实例隔离和终态 owner;阶段 43 已统一 Discord typing 的异步 owner 和 shutdown 收尾;阶段 44 已清除 WebAgent 测试临时事件循环提前关闭产生的 CI 红注解;阶段 45 已统一影视与字幕搜索的请求级逐页任务编排;阶段 46 已收口启动性能门禁的托管 runner 假失败与诊断输出;阶段 47 已补齐 Agent 渠道流式刷新任务的重入 owner;阶段 48 已统一工件上传 action 的 Node 24 主版本;阶段 49 已统一插件安装的同步/异步代际解析事实源;阶段 50 已统一插件市场 GitHub 请求降级策略;阶段 51 已统一插件索引请求与响应三态策略;阶段 52 已统一插件 Release 分页策略;阶段 53 已统一远端插件安装模式决策;阶段 54 已补齐同步安装成功后的临时回滚备份清理。
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务;阶段 32 已清除事件调度器绕过生命周期 owner 的投递回退;阶段 33 已统一宿主 Agent 运行时的获取路径;阶段 34 已统一 durable-required 事件与 Outbox topic 事实源;阶段 35 已统一 LLM provider 管理 API 的运行时解析路径;阶段 36 已统一 WebAgent 音频能力访问边界;阶段 37 已统一插件输入事件发布路径;阶段 38 已统一 WebAgent 通知事件监听与队列边界;阶段 39 已补齐搜索 SSE 断线时的上游任务清理;阶段 40 已补齐异步防抖取消的终态所有权;阶段 41 已统一优雅重启兜底线程的唯一所有权;阶段 42 已补齐 Telegram typing 的多实例隔离和终态 owner;阶段 43 已统一 Discord typing 的异步 owner 和 shutdown 收尾;阶段 44 已清除 WebAgent 测试临时事件循环提前关闭产生的 CI 红注解;阶段 45 已统一影视与字幕搜索的请求级逐页任务编排;阶段 46 已收口启动性能门禁的托管 runner 假失败与诊断输出;阶段 47 已补齐 Agent 渠道流式刷新任务的重入 owner;阶段 48 已统一工件上传 action 的 Node 24 主版本;阶段 49 已统一插件安装的同步/异步代际解析事实源;阶段 50 已统一插件市场 GitHub 请求降级策略;阶段 51 已统一插件索引请求与响应三态策略;阶段 52 已统一插件 Release 分页策略;阶段 53 已统一远端插件安装模式决策;阶段 54 已补齐同步安装成功后的临时回滚备份清理;阶段 55~56 已收口官方插件观察基线与报告保留策略;阶段 57 已统一进程级运行时 Facade 门禁并补齐 ModuleManager 边界
## 当前复核结论(2026-08-24
@@ -586,6 +586,20 @@
- `architecture-observe.yml` 现在显式使用 3 天保留期;报告内容、定时频率、失败条件、宿主及插件
契约都不变,仅清除已知的 CI 策略噪音。
### 长期整改阶段 57:进程级运行时 Facade 门禁统一(2026-08-24
- Scheduler、PluginManager、Command 和 WorkFlowManager 已经分别存在四套近似的依赖断言,但已经建立
`app.application.module` Facade 的 ModuleManager 未被同等全宿主 ratchet 保护,且新运行时边界仍可能
复制第五套测试实现。
- `scripts/architecture/service_locator.py` 现在以一份策略表统一扫描五类 concrete 运行时依赖,
ModuleManager 只允许 startup 组合根与既有 `app.sdk.plugins` 兼容导出直连;Scheduler、PluginManager、
Command 和 Workflow 保留原批准边界。
- Architecture CI 直接执行新门禁,合成回归同时证明五类越界均会失败,而 `app.sdk.plugins`
startup 组合根和 `app.workflow` 实现包仍可通过。公开 getter、Singleton identity、SDK/Compat 及
V1/V2/V3 插件 ABI 均未修改;未修改插件仓。
- 衍合远端 `feff545b` 后,新增的 `app.application.agenttask` 使三个启动入口的宿主模块数均精确增加 1;
已用仓库生成器刷新启动性能基线并保留原有严格模块计数和耗时预算,未通过放宽门禁掩盖该变更。
### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""防止宿主消费者绕过 application Facade 直接定位进程级运行时。"""
from __future__ import annotations
from dataclasses import dataclass
if __package__:
from scripts.architecture.baseline import discover_modules, resolve_imports
else: # pragma: no cover - 直接执行脚本时从同目录解析
from baseline import discover_modules, resolve_imports
@dataclass(frozen=True, slots=True)
class RuntimeFacadePolicy:
"""声明一个 concrete 运行时模块的合法装配与兼容消费者。"""
name: str
dependency: str
exact_consumers: frozenset[str]
consumer_prefixes: tuple[str, ...] = ()
def allows(self, module_name: str) -> bool:
"""判断模块是否属于明确的组合根、实现包或兼容入口。"""
if module_name in self.exact_consumers:
return True
return any(
module_name == prefix or module_name.startswith(f"{prefix}.")
for prefix in self.consumer_prefixes
)
RUNTIME_FACADE_POLICIES = (
RuntimeFacadePolicy(
name="scheduler",
dependency="app.scheduler",
exact_consumers=frozenset(
{
"app.scheduler",
"app.startup.initializers.modules",
"app.startup.initializers.scheduler",
}
),
),
RuntimeFacadePolicy(
name="module",
dependency="app.runtime.extensions.module_manager",
exact_consumers=frozenset(
{
"app.sdk.plugins",
"app.startup.initializers.modules",
}
),
),
RuntimeFacadePolicy(
name="plugin",
dependency="app.runtime.extensions.plugin_manager",
exact_consumers=frozenset(
{
"app.sdk.plugins",
"app.startup.initializers.modules",
"app.startup.initializers.plugins",
}
),
),
RuntimeFacadePolicy(
name="command",
dependency="app.command",
exact_consumers=frozenset(
{
"app.startup.initializers.command",
"app.startup.initializers.modules",
}
),
),
RuntimeFacadePolicy(
name="workflow",
dependency="app.workflow",
exact_consumers=frozenset({"app.startup.initializers.workflow"}),
consumer_prefixes=("app.workflow",),
),
)
@dataclass(frozen=True, order=True, slots=True)
class ServiceLocatorViolation:
"""描述一处绕过 application Facade 的 concrete 运行时依赖。"""
policy: str
source: str
dependency: str
def render(self) -> str:
"""返回适合 CI 输出的稳定诊断文本。"""
return (
f"{self.source}: {self.policy} 运行时必须经 application Facade 访问,"
f"不得直接依赖 {self.dependency}"
)
def collect_service_locator_violations(
graph: dict[str, set[str]] | None = None,
) -> list[ServiceLocatorViolation]:
"""扫描宿主依赖图,返回绕过已登记 Facade 的运行时直连。"""
if graph is None:
modules = discover_modules()
known_modules = set(modules)
graph = {
module_name: resolve_imports(module_name, path, known_modules)
for module_name, path in modules.items()
}
violations: list[ServiceLocatorViolation] = []
for policy in RUNTIME_FACADE_POLICIES:
for module_name, dependencies in graph.items():
if (
policy.dependency not in dependencies
or policy.allows(module_name)
):
continue
violations.append(
ServiceLocatorViolation(
policy=policy.name,
source=module_name,
dependency=policy.dependency,
)
)
return sorted(violations)
def main() -> int:
"""执行进程级运行时服务定位零债务门禁。"""
violations = collect_service_locator_violations()
if violations:
print("\n".join(violation.render() for violation in violations))
return 1
print("进程级运行时 Facade 门禁通过")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+112 -112
View File
@@ -1,41 +1,41 @@
{
"schema_version": 2,
"generated_at": "2026-08-23T15:39:20.210777+00:00",
"generated_at": "2026-08-24T04:25:54.827827+00:00",
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
"python": "3.14.3",
"repeat": 3,
"targets": {
"app.startup.lifecycle": {
"loaded_app_module_count": 356,
"max_ms": 914.989,
"median_ms": 909.991,
"min_ms": 907.871,
"loaded_app_module_count": 357,
"max_ms": 968.725,
"median_ms": 937.268,
"min_ms": 933.205,
"samples_ms": [
914.989,
907.871,
909.991
933.205,
937.268,
968.725
]
},
"app.factory": {
"loaded_app_module_count": 368,
"max_ms": 916.407,
"median_ms": 914.085,
"min_ms": 913.804,
"loaded_app_module_count": 369,
"max_ms": 943.204,
"median_ms": 941.996,
"min_ms": 937.878,
"samples_ms": [
916.407,
913.804,
914.085
941.996,
943.204,
937.878
]
},
"app.main": {
"loaded_app_module_count": 370,
"max_ms": 1096.074,
"median_ms": 1051.665,
"min_ms": 1050.168,
"loaded_app_module_count": 371,
"max_ms": 1068.791,
"median_ms": 1064.912,
"min_ms": 1053.947,
"samples_ms": [
1051.665,
1050.168,
1096.074
1053.947,
1068.791,
1064.912
]
}
},
@@ -47,24 +47,24 @@
{
"mode": "normal",
"enabled_component_count": 23,
"startup_ms": 0.672,
"full_lifespan_ms": 0.831,
"startup_ms": 0.629,
"full_lifespan_ms": 0.781,
"stage_ms": {
"后台任务登记器": 0.081,
"数据库准备": 0.037,
"HTTP 基础能力": 0.035,
"领域依赖装配": 0.029,
"数据库引擎预热": 0.025,
"后台任务登记器": 0.079,
"数据库准备": 0.038,
"HTTP 基础能力": 0.031,
"领域依赖装配": 0.028,
"数据库引擎预热": 0.024,
"数据库连接预算": 0.024,
"路由": 0.022,
"模块服务": 0.024,
"插件备份恢复": 0.022,
"插件": 0.02,
"定时器": 0.032,
"监控器": 0.022,
"待处理整理回放": 0.019,
"路由": 0.024,
"模块服务": 0.023,
"插件备份恢复": 0.02,
"插件": 0.023,
"定时器": 0.024,
"监控器": 0.021,
"待处理整理回放": 0.022,
"命令服务": 0.023,
"工作流": 0.023,
"工作流": 0.02,
"插件同步与启动收尾": 0.033
},
"threads_before": 2,
@@ -78,25 +78,25 @@
{
"mode": "normal",
"enabled_component_count": 23,
"startup_ms": 0.598,
"full_lifespan_ms": 0.742,
"startup_ms": 0.628,
"full_lifespan_ms": 0.778,
"stage_ms": {
"后台任务登记器": 0.069,
"数据库准备": 0.042,
"HTTP 基础能力": 0.033,
"领域依赖装配": 0.031,
"数据库引擎预热": 0.025,
"后台任务登记器": 0.079,
"数据库准备": 0.037,
"HTTP 基础能力": 0.029,
"领域依赖装配": 0.032,
"数据库引擎预热": 0.023,
"数据库连接预算": 0.022,
"路由": 0.021,
"路由": 0.024,
"模块服务": 0.021,
"插件备份恢复": 0.021,
"插件": 0.019,
"插件备份恢复": 0.024,
"插件": 0.021,
"定时器": 0.024,
"监控器": 0.023,
"待处理整理回放": 0.019,
"命令服务": 0.021,
"工作流": 0.023,
"插件同步与启动收尾": 0.03
"监控器": 0.019,
"待处理整理回放": 0.022,
"命令服务": 0.022,
"工作流": 0.022,
"插件同步与启动收尾": 0.033
},
"threads_before": 2,
"threads_started": 2,
@@ -109,25 +109,25 @@
{
"mode": "normal",
"enabled_component_count": 23,
"startup_ms": 0.765,
"full_lifespan_ms": 0.951,
"startup_ms": 0.658,
"full_lifespan_ms": 0.836,
"stage_ms": {
"后台任务登记器": 0.088,
"数据库准备": 0.067,
"HTTP 基础能力": 0.094,
"领域依赖装配": 0.039,
"数据库引擎预热": 0.031,
"数据库连接预算": 0.022,
"路由": 0.024,
"模块服务": 0.027,
"后台任务登记器": 0.081,
"数据库准备": 0.044,
"HTTP 基础能力": 0.029,
"领域依赖装配": 0.04,
"数据库引擎预热": 0.024,
"数据库连接预算": 0.023,
"路由": 0.025,
"模块服务": 0.02,
"插件备份恢复": 0.024,
"插件": 0.021,
"定时器": 0.026,
"插件": 0.02,
"定时器": 0.022,
"监控器": 0.023,
"待处理整理回放": 0.023,
"命令服务": 0.023,
"工作流": 0.023,
"插件同步与启动收尾": 0.062
"待处理整理回放": 0.022,
"命令服务": 0.022,
"工作流": 0.022,
"插件同步与启动收尾": 0.031
},
"threads_before": 2,
"threads_started": 2,
@@ -138,8 +138,8 @@
"database_connections_started": 0
}
],
"median_startup_ms": 0.672,
"median_full_lifespan_ms": 0.831,
"median_startup_ms": 0.629,
"median_full_lifespan_ms": 0.781,
"enabled_component_count": 23,
"enabled_components": [
"后台任务登记器",
@@ -172,41 +172,17 @@
{
"mode": "safe",
"enabled_component_count": 11,
"startup_ms": 0.465,
"full_lifespan_ms": 0.605,
"startup_ms": 0.567,
"full_lifespan_ms": 0.734,
"stage_ms": {
"后台任务登记器": 0.072,
"数据库准备": 0.047,
"HTTP 基础能力": 0.035,
"领域依赖装配": 0.03,
"数据库引擎预热": 0.024,
"数据库连接预算": 0.022,
"路由": 0.023,
"模块服务": 0.022,
"插件同步与启动收尾": 0.031
},
"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": 11,
"startup_ms": 0.478,
"full_lifespan_ms": 0.647,
"stage_ms": {
"后台任务登记器": 0.083,
"数据库准备": 0.038,
"后台任务登记器": 0.095,
"数据库准备": 0.05,
"HTTP 基础能力": 0.034,
"领域依赖装配": 0.025,
"数据库引擎预热": 0.022,
"数据库连接预算": 0.025,
"路由": 0.025,
"模块服务": 0.025,
"领域依赖装配": 0.032,
"数据库引擎预热": 0.026,
"数据库连接预算": 0.031,
"路由": 0.026,
"模块服务": 0.02,
"插件同步与启动收尾": 0.052
},
"threads_before": 2,
@@ -220,17 +196,41 @@
{
"mode": "safe",
"enabled_component_count": 11,
"startup_ms": 0.477,
"full_lifespan_ms": 0.651,
"startup_ms": 0.465,
"full_lifespan_ms": 0.61,
"stage_ms": {
"后台任务登记器": 0.074,
"数据库准备": 0.044,
"后台任务登记器": 0.078,
"数据库准备": 0.037,
"HTTP 基础能力": 0.029,
"领域依赖装配": 0.029,
"数据库引擎预热": 0.027,
"数据库连接预算": 0.023,
"数据库引擎预热": 0.026,
"数据库连接预算": 0.024,
"路由": 0.022,
"模块服务": 0.021,
"模块服务": 0.02,
"插件同步与启动收尾": 0.034
},
"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": 11,
"startup_ms": 0.456,
"full_lifespan_ms": 0.628,
"stage_ms": {
"后台任务登记器": 0.076,
"数据库准备": 0.036,
"HTTP 基础能力": 0.029,
"领域依赖装配": 0.027,
"数据库引擎预热": 0.023,
"数据库连接预算": 0.024,
"路由": 0.024,
"模块服务": 0.022,
"插件同步与启动收尾": 0.057
},
"threads_before": 2,
@@ -242,8 +242,8 @@
"database_connections_started": 0
}
],
"median_startup_ms": 0.477,
"median_full_lifespan_ms": 0.647,
"median_startup_ms": 0.465,
"median_full_lifespan_ms": 0.628,
"enabled_component_count": 11,
"enabled_components": [
"后台任务登记器",
+1
View File
@@ -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/architecture/service_locator.py" in commands
assert "scripts/startup/performance.py --check --repeat 3" in commands
-62
View File
@@ -1295,68 +1295,6 @@ def test_agent_tools_do_not_import_entrypoint_internals():
assert violations == {}
def test_runtime_consumers_use_scheduler_application_facade():
"""非组合根消费者必须经 application Facade 访问进程级 Scheduler。"""
allowed = {
"app.scheduler",
"app.startup.initializers.modules",
"app.startup.initializers.scheduler",
}
violations = {
module_name: dependencies & {"app.scheduler"}
for module_name, dependencies in _build_module_graph().items()
if module_name not in allowed and "app.scheduler" in dependencies
}
assert violations == {}
def test_runtime_consumers_use_plugin_application_facade():
"""插件 concrete 管理器只允许组合根和兼容 SDK 直接依赖。"""
allowed = {
"app.sdk.plugins",
"app.startup.initializers.modules",
"app.startup.initializers.plugins",
}
violations = {
module_name: dependencies & {"app.runtime.extensions.plugin_manager"}
for module_name, dependencies in _build_module_graph().items()
if module_name not in allowed
and "app.runtime.extensions.plugin_manager" in dependencies
}
assert violations == {}
def test_runtime_consumers_use_command_application_facade():
"""Command concrete 实现只允许 startup 组合根直接依赖。"""
allowed = {
"app.startup.initializers.command",
"app.startup.initializers.modules",
}
violations = {
module_name: dependencies & {"app.command"}
for module_name, dependencies in _build_module_graph().items()
if module_name not in allowed and "app.command" in dependencies
}
assert violations == {}
def test_runtime_consumers_use_workflow_application_facade():
"""WorkFlowManager concrete 实现只允许 startup 组合根直接依赖。"""
allowed = {"app.startup.initializers.workflow"}
violations = {
module_name: dependencies & {"app.workflow"}
for module_name, dependencies in _build_module_graph().items()
if not module_name.startswith("app.workflow")
and module_name not in allowed
and "app.workflow" in dependencies
}
assert violations == {}
def test_modules_read_deployment_settings_through_runtime_port():
"""宿主 Module 不得绕过 runtime 配置端口直接依赖 Settings 实例。"""
violations: list[str] = []
+43
View File
@@ -0,0 +1,43 @@
"""进程级运行时 Facade 静态门禁回归测试。"""
from scripts.architecture.service_locator import (
collect_service_locator_violations,
)
def test_host_runtime_consumers_use_application_facades() -> None:
"""canonical 宿主不得在批准边界外直接定位 concrete 运行时。"""
assert collect_service_locator_violations() == []
def test_service_locator_gate_detects_each_concrete_runtime_family() -> None:
"""单一扫描器必须覆盖五类运行时,并保留明确兼容边界。"""
graph = {
"app.api.scheduler_bypass": {"app.scheduler"},
"app.api.module_bypass": {"app.runtime.extensions.module_manager"},
"app.api.plugin_bypass": {"app.runtime.extensions.plugin_manager"},
"app.api.command_bypass": {"app.command"},
"app.api.workflow_bypass": {"app.workflow"},
"app.sdk.plugins": {
"app.runtime.extensions.module_manager",
"app.runtime.extensions.plugin_manager",
},
"app.startup.initializers.modules": {
"app.command",
"app.runtime.extensions.module_manager",
"app.runtime.extensions.plugin_manager",
"app.scheduler",
},
"app.workflow.manager": {"app.workflow"},
}
violations = collect_service_locator_violations(graph)
assert [violation.policy for violation in violations] == [
"command",
"module",
"plugin",
"scheduler",
"workflow",
]
assert all(violation.source.startswith("app.api.") for violation in violations)