mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-11 18:50:59 +08:00
feat(workflow): enhance workflow context serialization and execution state management
This commit is contained in:
@@ -67,6 +67,57 @@ class _FakeWorkflowManager:
|
||||
return self.contracts.get(action_type) or {}
|
||||
|
||||
|
||||
class _FakeWorkflowOper:
|
||||
"""记录工作流持久化调用。"""
|
||||
|
||||
def __init__(self, workflow):
|
||||
self.workflow = workflow
|
||||
self.steps = []
|
||||
self.started = False
|
||||
self.failed_result = None
|
||||
self.succeeded = False
|
||||
|
||||
def reset(self, wid):
|
||||
"""模拟重置工作流。"""
|
||||
_ = wid
|
||||
return True
|
||||
|
||||
def get(self, wid):
|
||||
"""返回预置工作流。"""
|
||||
_ = wid
|
||||
return self.workflow
|
||||
|
||||
def start(self, wid):
|
||||
"""记录启动调用。"""
|
||||
_ = wid
|
||||
self.started = True
|
||||
return True
|
||||
|
||||
def step(self, wid, action_id, context, execution_state=None):
|
||||
"""记录步骤持久化数据。"""
|
||||
self.steps.append(
|
||||
{
|
||||
"wid": wid,
|
||||
"action_id": action_id,
|
||||
"context": context,
|
||||
"execution_state": execution_state,
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
def fail(self, wid, result):
|
||||
"""记录失败结果。"""
|
||||
_ = wid
|
||||
self.failed_result = result
|
||||
return True
|
||||
|
||||
def success(self, wid, result=None):
|
||||
"""记录成功结果。"""
|
||||
_ = wid, result
|
||||
self.succeeded = True
|
||||
return True
|
||||
|
||||
|
||||
class _OpaqueValue:
|
||||
"""模拟无法直接 JSON 序列化的值。"""
|
||||
|
||||
@@ -97,6 +148,31 @@ def test_workflow_executor_resumes_downstream_nodes(monkeypatch):
|
||||
assert executor.context.progress == 100
|
||||
|
||||
|
||||
def test_workflow_executor_restores_structured_context(monkeypatch):
|
||||
"""恢复执行时应兼容新版结构化上下文存储格式。"""
|
||||
calls = []
|
||||
fake_manager = _FakeWorkflowManager(calls)
|
||||
workflow = _build_workflow(
|
||||
current_action="A",
|
||||
context={
|
||||
"workflow_context": {"trace_id": "wf-1"},
|
||||
"node_outputs": {"A": {"items": ["movie"]}},
|
||||
"progress": 50,
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager)
|
||||
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)
|
||||
|
||||
executor = workflow_module.WorkflowExecutor(workflow)
|
||||
executor.execute()
|
||||
|
||||
assert calls == ["B"]
|
||||
assert executor.context.workflow_context["trace_id"] == "wf-1"
|
||||
assert executor.context.node_outputs["A"]["items"] == ["movie"]
|
||||
|
||||
|
||||
def test_workflow_executor_reports_incremental_progress(monkeypatch):
|
||||
"""顺序工作流的中间进度应按已完成比例计算。"""
|
||||
calls = []
|
||||
@@ -488,6 +564,44 @@ def test_workflow_executor_keeps_execution_state_dict_for_non_json_leaf(monkeypa
|
||||
assert states[-1]["outputs"]["A"]["opaque"] == "opaque-value"
|
||||
|
||||
|
||||
def test_workflow_chain_process_serializes_circular_context(monkeypatch):
|
||||
"""工作流步骤持久化应清洗循环引用和不可序列化上下文。"""
|
||||
calls = []
|
||||
|
||||
def run_action(action, context):
|
||||
"""构造包含循环引用的上下文。"""
|
||||
context.workflow_context["self"] = context.workflow_context
|
||||
context.workflow_context["opaque"] = _OpaqueValue()
|
||||
return ActionResult(success=True, message=f"{action.name}完成", context=context)
|
||||
|
||||
fake_manager = _FakeWorkflowManager(calls, results={"A": run_action})
|
||||
workflow = _build_workflow(
|
||||
actions=[{"id": "A", "type": "FakeAction", "name": "动作A", "data": {}}],
|
||||
flows=[{"id": "flow-end", "source": "A", "target": "END", "animated": True}],
|
||||
)
|
||||
fake_oper = _FakeWorkflowOper(workflow)
|
||||
|
||||
monkeypatch.setattr(workflow_module, "WorkFlowManager", lambda: fake_manager)
|
||||
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, "is_workflow_stopped", lambda workflow_id: False)
|
||||
|
||||
success, message = workflow_module.WorkflowChain.process(workflow_id=1)
|
||||
|
||||
assert success is True
|
||||
assert message == ""
|
||||
assert fake_oper.succeeded is True
|
||||
saved_workflow_context = fake_oper.steps[-1]["context"]["workflow_context"]
|
||||
saved_self = saved_workflow_context["self"]
|
||||
|
||||
assert saved_workflow_context["opaque"] == "opaque-value"
|
||||
if isinstance(saved_self, dict):
|
||||
assert saved_self["self"] == workflow_module.CIRCULAR_REFERENCE_PLACEHOLDER
|
||||
assert saved_self["opaque"] == "opaque-value"
|
||||
else:
|
||||
assert saved_self == workflow_module.CIRCULAR_REFERENCE_PLACEHOLDER
|
||||
|
||||
|
||||
def test_workflow_executor_concurrency_key_serializes_parallel_nodes(monkeypatch):
|
||||
"""相同 concurrency_key 的并行节点不应同时运行。"""
|
||||
calls = []
|
||||
|
||||
Reference in New Issue
Block a user