refactor: unify workflow runtime boundary

This commit is contained in:
jxxghp
2026-08-24 03:43:47 +08:00
parent 371383f0a8
commit 9f76fc9dec
12 changed files with 154 additions and 34 deletions
+2 -2
View File
@@ -14,9 +14,9 @@ from app.application.workflow import (
WorkflowDefinitionCommand, WorkflowDefinitionCommand,
WorkflowMutationCommand, WorkflowMutationCommand,
WorkflowQueryService, WorkflowQueryService,
get_workflow_manager,
) )
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.workflow import WorkFlowManager
from app.startup.composition.context import HostRuntime from app.startup.composition.context import HostRuntime
@@ -26,7 +26,7 @@ def get_workflow_mutation_command(
) -> WorkflowMutationCommand: ) -> WorkflowMutationCommand:
"""组装请求级工作流写用例和提交后的调度副作用。""" """组装请求级工作流写用例和提交后的调度副作用。"""
scheduler = Scheduler() scheduler = Scheduler()
workflow_manager = WorkFlowManager() workflow_manager = get_workflow_manager()
system_config = cast(WorkflowCachePort, runtime.workflow.system_config()) system_config = cast(WorkflowCachePort, runtime.workflow.system_config())
return WorkflowMutationCommand( return WorkflowMutationCommand(
repository=runtime.workflow.repository(db), repository=runtime.workflow.repository(db),
+2 -2
View File
@@ -13,10 +13,10 @@ from app.application.workflow import (
WorkflowDefinitionCommand, WorkflowDefinitionCommand,
WorkflowMutationCommand, WorkflowMutationCommand,
WorkflowQueryService, WorkflowQueryService,
get_workflow_manager,
) )
from app.chain.workflow import WorkflowChain from app.chain.workflow import WorkflowChain
from app.application.plugin.runtime import get_plugin_manager as PluginManager from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.workflow import WorkFlowManager
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_manage_user_async, get_current_active_manage_user_async,
@@ -78,7 +78,7 @@ async def list_actions(_: Any = Depends(get_current_active_manage_user_async)) -
""" """
获取所有动作 获取所有动作
""" """
return WorkFlowManager().list_actions() return get_workflow_manager().list_actions()
@router.get( @router.get(
+50
View File
@@ -17,6 +17,56 @@ SUPPORTED_WORKFLOW_TRIGGERS = {
} }
class WorkflowRuntime(Protocol):
"""声明宿主入口与 Chain 消费的工作流运行时能力。"""
def execute(self, *args: Any, **kwargs: Any) -> Any:
"""执行单个工作流动作,参数与 concrete 管理器保持一致。"""
...
def list_actions(self) -> list[dict[str, Any]]:
"""返回当前运行时登记的工作流动作定义。"""
...
def load_workflow_events(self, workflow_id: Optional[int] = None) -> None:
"""加载全部或指定工作流的事件触发器。"""
...
def remove_workflow_event(
self,
workflow_id: Optional[int] = None,
event_type_str: Optional[str] = None,
) -> None:
"""移除全部或指定工作流的事件触发器。"""
...
def update_workflow_event(self, workflow: Any) -> None:
"""按最新定义刷新工作流事件触发器。"""
...
WorkflowRuntimeProvider = Callable[[], WorkflowRuntime]
def _unconfigured_workflow_runtime() -> WorkflowRuntime:
"""拒绝在启动组合根装配前隐式创建工作流管理器。"""
raise RuntimeError("工作流运行时尚未由启动组合根装配")
_workflow_runtime_provider: WorkflowRuntimeProvider = _unconfigured_workflow_runtime
def configure_workflow_runtime(provider: WorkflowRuntimeProvider) -> None:
"""由启动组合根登记工作流运行时实例提供器。"""
global _workflow_runtime_provider
_workflow_runtime_provider = provider
def get_workflow_manager() -> WorkflowRuntime:
"""返回组合根提供的工作流运行时,避免消费者直接定位 Singleton。"""
return _workflow_runtime_provider()
class AsyncWorkflowQueryRepository(Protocol): class AsyncWorkflowQueryRepository(Protocol):
"""工作流查询用例需要的异步读取端口。""" """工作流查询用例需要的异步读取端口。"""
+2 -2
View File
@@ -17,6 +17,7 @@ from pydantic import BaseModel
from app.chain import ChainBase from app.chain import ChainBase
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager from app.runtime.events import Event, eventmanager
from app.application.workflow import get_workflow_manager
from app.application.chain.data import WorkflowPortProxy as WorkflowOper from app.application.chain.data import WorkflowPortProxy as WorkflowOper
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.workflow import ActionContext from app.schemas.workflow import ActionContext
@@ -25,7 +26,6 @@ from app.schemas.workflow import Action
from app.schemas.workflow import ActionExecution from app.schemas.workflow import ActionExecution
from app.schemas.workflow import ActionResult from app.schemas.workflow import ActionResult
from app.schemas.types import EventType from app.schemas.types import EventType
from app.workflow import WorkFlowManager
ARTIFACT_FIELDS = {"torrents", "medias", "fileitems", "downloads", "sites", "subscribes"} ARTIFACT_FIELDS = {"torrents", "medias", "fileitems", "downloads", "sites", "subscribes"}
DEFAULT_WORKFLOW_MAX_WORKERS = 4 DEFAULT_WORKFLOW_MAX_WORKERS = 4
@@ -162,7 +162,7 @@ class WorkflowExecutor:
self.flow_failed = set() self.flow_failed = set()
# 工作流管理器 # 工作流管理器
self.workflowmanager = WorkFlowManager() self.workflowmanager = get_workflow_manager()
# 线程安全队列 # 线程安全队列
self.queue = deque() self.queue = deque()
self.queued_actions = set() self.queued_actions = set()
+5
View File
@@ -1,6 +1,11 @@
from app.application.workflow import configure_workflow_runtime
from app.workflow import WorkFlowManager from app.workflow import WorkFlowManager
# 启动模块是 concrete WorkFlowManager 的唯一宿主装配边界。
configure_workflow_runtime(lambda: WorkFlowManager())
def init_workflow(): def init_workflow():
""" """
初始化工作流 初始化工作流
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`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 已统一宿主同步函数的异步线程池入口。 > 实施进度:阶段 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 已统一工作流运行时的宿主获取路径
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -106,6 +106,17 @@
- 兼容边界不变:七个渠道类、模块方法、配置字段、消息 payload、同步/异步 SDK 回调方式和 - 兼容边界不变:七个渠道类、模块方法、配置字段、消息 payload、同步/异步 SDK 回调方式和
`/api/v1/message` HTTP 合同均未改;没有修改插件仓或向 SDK/Compat 新增宿主内部入口。 `/api/v1/message` HTTP 合同均未改;没有修改插件仓或向 SDK/Compat 新增宿主内部入口。
### 长期整改阶段 9:工作流运行时获取路径统一(2026-08-24)
- API 列表端点、请求级工作流写用例装配和 `WorkflowChain` 原先分别直接构造
`app.workflow.WorkFlowManager` Singleton;现在统一通过 `app.application.workflow` 的 typed runtime
provider 获取启动组合根登记的同一实例,不再由消费者自行定位 concrete 管理器。
- 架构门禁禁止 `app.workflow/**` 实现包和 `app.startup/initializers/workflow.py` 以外的宿主模块直接依赖
`app.workflow`。依赖图总边数保持 `6544`:移除 3 条 API/Chain concrete 边,同时新增 Chain 的
Application 端口边和 startup 的两条装配边;重点禁止边与自有 SCC 均未增长。
- 兼容边界不变:`app.workflow.WorkFlowManager` 的类路径、Singleton identity、公开方法、事件监听和
action 加载保持原样,旧插件仍可直接使用 concrete 类;本阶段只收口 canonical 宿主消费者。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
@@ -162,13 +173,16 @@
配置治理基线进一步区分债务与批准边界:canonical 未批准 Settings 直连和非组合根 配置治理基线进一步区分债务与批准边界:canonical 未批准 Settings 直连和非组合根
`SystemConfigOper()` 构造均为 `0``app/db/base.py``engine.py``session.py` 的启动前数据库基础设施 `SystemConfigOper()` 构造均为 `0``app/db/base.py``engine.py``session.py` 的启动前数据库基础设施
读取及 startup 唯一 Oper 构造点以带理由的固定边界登记。四个集合都只能减少,不能新增或换位置。 读取及 startup 唯一 Oper 构造点以带理由的固定边界登记。四个集合都只能减少,不能新增或换位置。
工作流运行时也已按相同模式收口:API、请求依赖与 Chain 只依赖
`app.application.workflow.get_workflow_manager()`;只有工作流实现包和 startup 组合根可直接依赖
concrete `WorkFlowManager`,架构测试拒绝宿主重新引入第二条实例获取路径。
### P2:中长期可演进性债务 ### P2:中长期可演进性债务
- **大型职责域仍偏重。** 代表性热点包括 `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 只保证不超过当前基线,不代表这些文件已经易维护。只有在行为快照、调用命中和事务边界明确后,才值得按用例拆分。 - **大型职责域仍偏重。** 代表性热点包括 `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。 - **类型门禁覆盖面不足。** `mypy.ini` strict 文件清单目前 `39` 个文件,Agent、Chain、Module、Adapter 大量代码仍依赖动态类型。应从模块契约、生命周期、Repository/Port 和关键 Chain 返回值开始扩展,而不是直接开启全仓 strict。
- **Pylint 仍是增量硬门禁。** `.github/workflows/pylint.yml` 对改动 Python 文件执行硬检查,但全仓报告使用 `|| true` 仅作 advisory。该策略适合存量迁移,却没有形成全仓质量趋势约束;应增加按目录和新增问题数的 ratchet。 - **Pylint 仍是增量硬门禁。** `.github/workflows/pylint.yml` 对改动 Python 文件执行硬检查,但全仓报告使用 `|| true` 仅作 advisory。该策略适合存量迁移,却没有形成全仓质量趋势约束;应增加按目录和新增问题数的 ratchet。
- **测试风格存在历史混用。** 当前 `499` 个测试文件,仍有 `70``unittest.TestCase` 文件。它不是生产架构缺陷,但会增加 fixture、状态隔离和异步测试迁移成本,应在触碰相关模块时渐进迁移。 - **测试风格存在历史混用。** 当前 `527` 个测试文件,仍有 `70``unittest.TestCase` 文件。它不是生产架构缺陷,但会增加 fixture、状态隔离和异步测试迁移成本,应在触碰相关模块时渐进迁移。
- **跨仓治理链路尚未完全闭环。** 前端已有 lint、typecheck、分片 Vitest 和构建门禁;插件仓有 V1/V2/V3 索引及版本/依赖检查;资源和 Rust 仓有独立构建发布链路。但插件 CI 本地复核因插件仓环境缺少主仓依赖 `httpx2` 无法完成收集,说明“插件仓测试环境与主仓锁定依赖”的可复现性仍需加强。资源构建通过 PR 同步到 `MoviePilot-Resources`,Rust 发布后自动向主仓发依赖 bump PR,链路合理但仍是多仓异步发布,需保留版本 provenance 和回滚点。 - **跨仓治理链路尚未完全闭环。** 前端已有 lint、typecheck、分片 Vitest 和构建门禁;插件仓有 V1/V2/V3 索引及版本/依赖检查;资源和 Rust 仓有独立构建发布链路。但插件 CI 本地复核因插件仓环境缺少主仓依赖 `httpx2` 无法完成收集,说明“插件仓测试环境与主仓锁定依赖”的可复现性仍需加强。资源构建通过 PR 同步到 `MoviePilot-Resources`,Rust 发布后自动向主仓发依赖 bump PR,链路合理但仍是多仓异步发布,需保留版本 provenance 和回滚点。
### 已解决、不应重复治理的问题 ### 已解决、不应重复治理的问题
@@ -219,7 +233,7 @@ MoviePilot V3 当前不是“目录混乱、必须推倒重来”的状态。第
5. **模块与事件契约登记均已完成。**当前 212 个模块 spec 的宿主观察面已无 legacy aggregation53 个事件全部绑定 typed payload,可见性、投递等级、错误行为和敏感字段均有基线,legacy event payload 为 `0`。后续重点是保持新增能力 ratchet、观察未知第三方 fallback 命中,以及 6 个 durable-required 事件的真实持久投递,不是重复创建契约或事件 DTO。 5. **模块与事件契约登记均已完成。**当前 212 个模块 spec 的宿主观察面已无 legacy aggregation53 个事件全部绑定 typed payload,可见性、投递等级、错误行为和敏感字段均有基线,legacy event payload 为 `0`。后续重点是保持新增能力 ratchet、观察未知第三方 fallback 命中,以及 6 个 durable-required 事件的真实持久投递,不是重复创建契约或事件 DTO。
6. **后台副作用缺少统一可靠性定义。**事件队列、APScheduler、FastAPI BackgroundTasks 和线程池任务的丢失、重试、幂等、关停语义各不相同;数据库提交与事件/上报之间仍有进程崩溃窗口。 6. **后台副作用缺少统一可靠性定义。**事件队列、APScheduler、FastAPI BackgroundTasks 和线程池任务的丢失、重试、幂等、关停语义各不相同;数据库提交与事件/上报之间仍有进程崩溃窗口。
7. **核心关联与健康边界已落地,指标导出仍未收口。**HTTP/SSE correlation ID 已传播到线程池、事件、工作流、子进程、外部请求和日志;`/health/live``/health/ready` 已由部署入口消费,事件/数据库队列深度及模块/事件耗时使用低基数指标登记。当前缺口是稳定 exporter、运维查询面和跨进程聚合,而不是重新实现 request ID 或健康路由。 7. **核心关联与健康边界已落地,指标导出仍未收口。**HTTP/SSE correlation ID 已传播到线程池、事件、工作流、子进程、外部请求和日志;`/health/live``/health/ready` 已由部署入口消费,事件/数据库队列深度及模块/事件耗时使用低基数指标登记。当前缺口是稳定 exporter、运维查询面和跨进程聚合,而不是重新实现 request ID 或健康路由。
8. **质量门禁已具备增量硬约束,但覆盖面仍需扩大。**push/PR 对变更 Python 文件执行 PylintCI 同时运行 host architecture、37 个 strict mypy 文件、复杂度、async 阻塞和 task owner ratchet;全仓 Pylint 仍是 advisorystrict 类型和复杂度拆分仍应随业务切片渐进扩展。 8. **质量门禁已具备增量硬约束,但覆盖面仍需扩大。**push/PR 对变更 Python 文件执行 PylintCI 同时运行 host architecture、39 个 strict mypy 文件、复杂度、async 阻塞和 task owner ratchet;全仓 Pylint 仍是 advisorystrict 类型和复杂度拆分仍应随业务切片渐进扩展。
建议保持**模块化单体**,按以下顺序治理: 建议保持**模块化单体**,按以下顺序治理:
+2
View File
@@ -425,6 +425,7 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/initializers/agent.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used | | `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/initializers/agent.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used |
| `agent.tools -> api / scheduler / command` | Forbidden; tools use `app/application/plugin/routes.py`, `plugin/folders.py`, `scheduling.py` and `commands.py` application services | | `agent.tools -> api / scheduler / command` | Forbidden; tools use `app/application/plugin/routes.py`, `plugin/folders.py`, `scheduling.py` and `commands.py` application services |
| `api -> factory` | Forbidden; the FastAPI route adapter is injected into `app/application/plugin/routes.py` by the composition root after creation | | `api -> factory` | Forbidden; the FastAPI route adapter is injected into `app/application/plugin/routes.py` by the composition root after creation |
| `api / chain -> app.workflow` | Forbidden; workflow consumers use `app/application/workflow.py`, while only `app/workflow/**` and `app/startup/initializers/workflow.py` access the concrete runtime |
| `application -> domain / runtime contract` | Allowed | | `application -> domain / runtime contract` | Allowed |
| `application -> DB / Oper / concrete adapter` | Forbidden; define a Protocol in Application and inject an implementation | | `application -> DB / Oper / concrete adapter` | Forbidden; define a Protocol in Application and inject an implementation |
| `db.adapters -> application persistence Protocol / db.oper / UoW` | Allowed; this is dependency inversion, not an upper-layer use-case call | | `db.adapters -> application persistence Protocol / db.oper / UoW` | Allowed; this is dependency inversion, not an upper-layer use-case call |
@@ -446,6 +447,7 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration | | `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration |
| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/initializers/scheduler.py` | | `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/initializers/scheduler.py` |
| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/initializers/command.py` | | `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/initializers/command.py` |
| `app/application/workflow.py` | Workflow use cases plus the runtime port consumed by API and Chain; `WorkFlowManager` is registered by `app/startup/initializers/workflow.py` |
| `app/db/adapters/` | SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols | | `app/db/adapters/` | SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols |
| `app/startup/composition/` | HostRuntime, configuration snapshots and cross-layer adapter wiring | | `app/startup/composition/` | HostRuntime, configuration snapshots and cross-layer adapter wiring |
| `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks | | `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks |
+7 -1
View File
@@ -145,7 +145,13 @@ def configure_plugin_system_services():
configure_module_runtime(lambda: ModuleManager()) configure_module_runtime(lambda: ModuleManager())
from app.application.site.query import SiteQueryService, configure_site_query_service from app.application.site.query import SiteQueryService, configure_site_query_service
from app.application.site.health import SiteHealthService, configure_site_health_service from app.application.site.health import SiteHealthService, configure_site_health_service
from app.application.workflow import WorkflowQueryService, configure_workflow_query from app.application.workflow import (
WorkflowQueryService,
configure_workflow_query,
configure_workflow_runtime,
)
from app.workflow import WorkFlowManager
configure_workflow_runtime(lambda: WorkFlowManager())
from app.application.agentdata import configure_agent_data_ports from app.application.agentdata import configure_agent_data_ports
from app.db.oper.agentchat import AgentChatOper from app.db.oper.agentchat import AgentChatOper
from app.db.oper.downloadfailure import DownloadFailureOper from app.db.oper.downloadfailure import DownloadFailureOper
+4 -4
View File
@@ -14,7 +14,7 @@
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6544, "edge_count": 6544,
"edge_sha256": "44812359c634235d327fdc499bc0ccf93e24923fba24bed4a29fc14adc26bf84", "edge_sha256": "562471ed16680ca403b6cdb7661b20b1200c53b75da5e162efe4e7400cde174b",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -1637,7 +1637,6 @@
"app.api.dependencies.workflow -> app.startup", "app.api.dependencies.workflow -> app.startup",
"app.api.dependencies.workflow -> app.startup.composition", "app.api.dependencies.workflow -> app.startup.composition",
"app.api.dependencies.workflow -> app.startup.composition.context", "app.api.dependencies.workflow -> app.startup.composition.context",
"app.api.dependencies.workflow -> app.workflow",
"app.api.deps -> app.api", "app.api.deps -> app.api",
"app.api.deps -> app.api.dependencies", "app.api.deps -> app.api.dependencies",
"app.api.deps -> app.api.dependencies.agent", "app.api.deps -> app.api.dependencies.agent",
@@ -2417,7 +2416,6 @@
"app.api.endpoints.workflow -> app.schemas.response", "app.api.endpoints.workflow -> app.schemas.response",
"app.api.endpoints.workflow -> app.schemas.types", "app.api.endpoints.workflow -> app.schemas.types",
"app.api.endpoints.workflow -> app.schemas.workflow", "app.api.endpoints.workflow -> app.schemas.workflow",
"app.api.endpoints.workflow -> app.workflow",
"app.api.response -> app.schemas", "app.api.response -> app.schemas",
"app.api.response -> app.schemas.common", "app.api.response -> app.schemas.common",
"app.api.response -> app.schemas.response", "app.api.response -> app.schemas.response",
@@ -3480,6 +3478,7 @@
"app.chain.workflow -> app.application", "app.chain.workflow -> app.application",
"app.chain.workflow -> app.application.chain", "app.chain.workflow -> app.application.chain",
"app.chain.workflow -> app.application.chain.data", "app.chain.workflow -> app.application.chain.data",
"app.chain.workflow -> app.application.workflow",
"app.chain.workflow -> app.chain", "app.chain.workflow -> app.chain",
"app.chain.workflow -> app.runtime", "app.chain.workflow -> app.runtime",
"app.chain.workflow -> app.runtime.config", "app.chain.workflow -> app.runtime.config",
@@ -3488,7 +3487,6 @@
"app.chain.workflow -> app.schemas", "app.chain.workflow -> app.schemas",
"app.chain.workflow -> app.schemas.types", "app.chain.workflow -> app.schemas.types",
"app.chain.workflow -> app.schemas.workflow", "app.chain.workflow -> app.schemas.workflow",
"app.chain.workflow -> app.workflow",
"app.cli -> app.application", "app.cli -> app.application",
"app.cli -> app.application.backup", "app.cli -> app.application.backup",
"app.cli -> app.doctor", "app.cli -> app.doctor",
@@ -6336,6 +6334,8 @@
"app.startup.initializers.scheduler -> app.scheduler", "app.startup.initializers.scheduler -> app.scheduler",
"app.startup.initializers.transfer -> app.chain", "app.startup.initializers.transfer -> app.chain",
"app.startup.initializers.transfer -> app.chain.transfer", "app.startup.initializers.transfer -> app.chain.transfer",
"app.startup.initializers.workflow -> app.application",
"app.startup.initializers.workflow -> app.application.workflow",
"app.startup.initializers.workflow -> app.workflow", "app.startup.initializers.workflow -> app.workflow",
"app.startup.lifecycle -> app.adapters", "app.startup.lifecycle -> app.adapters",
"app.startup.lifecycle -> app.adapters.external", "app.startup.lifecycle -> app.adapters.external",
+14
View File
@@ -926,6 +926,20 @@ def test_runtime_consumers_use_command_application_facade():
assert violations == {} 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(): def test_modules_read_deployment_settings_through_runtime_port():
"""宿主 Module 不得绕过 runtime 配置端口直接依赖 Settings 实例。""" """宿主 Module 不得绕过 runtime 配置端口直接依赖 Settings 实例。"""
violations: list[str] = [] violations: list[str] = []
+19 -19
View File
@@ -142,7 +142,7 @@ def test_workflow_executor_preserves_trigger_context(monkeypatch):
[], [],
results={"A": run_action}, results={"A": run_action},
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr( monkeypatch.setattr(
workflow_module.global_vars, workflow_module.global_vars,
"workflow_resume", "workflow_resume",
@@ -189,7 +189,7 @@ def test_workflow_executor_resumes_downstream_nodes(monkeypatch):
context=_encoded_context(ActionContext()), context=_encoded_context(ActionContext()),
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -214,7 +214,7 @@ def test_workflow_executor_restores_structured_context(monkeypatch):
}, },
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -232,7 +232,7 @@ def test_workflow_executor_reports_incremental_progress(monkeypatch):
progresses = [] progresses = []
fake_manager = _FakeWorkflowManager(calls) fake_manager = _FakeWorkflowManager(calls)
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -272,7 +272,7 @@ def test_workflow_executor_skips_false_condition_branch(monkeypatch):
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -325,7 +325,7 @@ def test_workflow_executor_all_success_join_waits_parallel_branches(monkeypatch)
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -367,7 +367,7 @@ def test_workflow_executor_any_success_join_runs_after_available_branch(monkeypa
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -399,7 +399,7 @@ def test_workflow_executor_all_done_join_can_continue_after_failure(monkeypatch)
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -438,7 +438,7 @@ def test_workflow_executor_exclusive_branch_uses_first_matching_flow(monkeypatch
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -475,7 +475,7 @@ def test_workflow_executor_passes_declared_inputs(monkeypatch):
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -516,7 +516,7 @@ def test_workflow_executor_uses_contract_inputs(monkeypatch):
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -543,7 +543,7 @@ def test_workflow_executor_persists_structured_state(monkeypatch):
} }
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -576,7 +576,7 @@ def test_workflow_executor_restores_outputs_from_execution_state(monkeypatch):
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -603,7 +603,7 @@ def test_workflow_executor_keeps_execution_state_dict_for_non_json_leaf(monkeypa
} }
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -634,7 +634,7 @@ def test_workflow_chain_process_serializes_circular_context(monkeypatch):
) )
fake_oper = _FakeWorkflowOper(workflow) fake_oper = _FakeWorkflowOper(workflow)
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module, "WorkflowOper", lambda: fake_oper) monkeypatch.setattr(workflow_module, "WorkflowOper", lambda: fake_oper)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -683,7 +683,7 @@ def test_workflow_executor_concurrency_key_serializes_parallel_nodes(monkeypatch
execution_config={"max_workers": 2}, execution_config={"max_workers": 2},
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -721,7 +721,7 @@ def test_workflow_executor_filter_action_replaces_artifact_outputs(monkeypatch):
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -772,7 +772,7 @@ def test_workflow_executor_filter_action_replaces_with_empty_outputs(monkeypatch
], ],
) )
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
@@ -789,7 +789,7 @@ def test_workflow_executor_stop_is_not_success(monkeypatch):
calls = [] calls = []
fake_manager = _FakeWorkflowManager(calls) fake_manager = _FakeWorkflowManager(calls)
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager) monkeypatch.setattr(workflow_module, "get_workflow_manager", lambda: fake_manager)
monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None) monkeypatch.setattr(workflow_module.global_vars, "workflow_resume", lambda workflow_id: None)
monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: True) monkeypatch.setattr(workflow_module.global_vars, "is_workflow_stopped", lambda workflow_id: True)
+29
View File
@@ -0,0 +1,29 @@
"""工作流运行时 Application Facade 回归。"""
import pytest
from app.application import workflow as workflow_application
def test_workflow_runtime_facade_preserves_registered_identity(monkeypatch) -> None:
"""Application Facade 必须返回组合根登记的同一个运行时对象。"""
runtime = object()
monkeypatch.setattr(
workflow_application,
"_workflow_runtime_provider",
lambda: runtime,
)
assert workflow_application.get_workflow_manager() is runtime
def test_workflow_runtime_facade_fails_before_composition(monkeypatch) -> None:
"""未装配时不得隐式创建第二个 WorkFlowManager Singleton。"""
monkeypatch.setattr(
workflow_application,
"_workflow_runtime_provider",
workflow_application._unconfigured_workflow_runtime,
)
with pytest.raises(RuntimeError, match="工作流运行时尚未由启动组合根装配"):
workflow_application.get_workflow_manager()