mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-11 10:40:18 +08:00
feat(workflow): implement action contract management for inputs and outputs
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.schemas import ActionContext, DownloadTask, FileItem
|
||||
from app.schemas.workflow import ActionResult
|
||||
from app.workflow.actions import BaseAction
|
||||
from app.workflow.actions import fetch_downloads as fetch_downloads_module
|
||||
from app.workflow.actions import scrape_file as scrape_file_module
|
||||
from app.workflow.actions.fetch_downloads import FetchDownloadsAction
|
||||
from app.workflow.actions.scrape_file import ScrapeFileAction
|
||||
from app.workflow.actions.fetch_rss import FetchRssAction
|
||||
from app.workflow import WorkFlowManager
|
||||
|
||||
|
||||
def test_fetch_downloads_updates_context_downloads(monkeypatch):
|
||||
@@ -78,3 +82,80 @@ def test_scrape_file_keeps_workflow_action_context(monkeypatch):
|
||||
assert result is context
|
||||
assert result.fileitems[0].path == "/library/movie.mkv"
|
||||
assert scraped == [("/library/movie.mkv", "meta", "media")]
|
||||
|
||||
|
||||
def test_execute_with_inputs_maps_contract_inputs_outputs_and_runtime(monkeypatch):
|
||||
"""新版动作桥接方法应按契约映射输入、输出和运行期信息。"""
|
||||
|
||||
class ContractAction(BaseAction):
|
||||
"""测试动作契约桥接。"""
|
||||
|
||||
contract = {
|
||||
"inputs": [{"name": "torrents", "label": "资源", "kind": "list"}],
|
||||
"outputs": [{"name": "downloads", "label": "下载任务", "kind": "list"}],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def name(cls) -> str:
|
||||
return "契约动作"
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def description(cls) -> str:
|
||||
return "测试契约动作"
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def data(cls) -> dict:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
return True
|
||||
|
||||
def execute(self, workflow_id: int, params: dict, context: ActionContext) -> ActionContext:
|
||||
"""执行测试动作。"""
|
||||
_ = workflow_id, params
|
||||
context.downloads = [
|
||||
DownloadTask(download_id=f"{item}-hash", downloader="qbittorrent")
|
||||
for item in context.torrents
|
||||
]
|
||||
self.job_done("完成")
|
||||
return context
|
||||
|
||||
result = ContractAction("contract").execute_with_inputs(
|
||||
workflow_id=1,
|
||||
params={},
|
||||
inputs={"torrents": ["movie"]},
|
||||
runtime={"attempt": 1, "max_attempts": 1, "cancel_token": object()},
|
||||
context=ActionContext(),
|
||||
)
|
||||
|
||||
assert isinstance(result, ActionResult)
|
||||
assert result.outputs["downloads"][0].download_id == "movie-hash"
|
||||
assert result.context.runtime_state["current_action_runtime"] == {
|
||||
"attempt": 1,
|
||||
"max_attempts": 1,
|
||||
}
|
||||
|
||||
path_result = ContractAction("contract").execute_with_inputs(
|
||||
workflow_id=1,
|
||||
params={},
|
||||
inputs={"outputs.FetchRssAction.torrents": ["legacy"]},
|
||||
runtime={},
|
||||
context=ActionContext(),
|
||||
)
|
||||
|
||||
assert path_result.outputs["downloads"][0].download_id == "legacy-hash"
|
||||
|
||||
|
||||
def test_workflow_manager_list_actions_exposes_contract():
|
||||
"""动作列表应返回固定输入输出契约。"""
|
||||
manager = object.__new__(WorkFlowManager)
|
||||
manager._actions = {"FetchRssAction": FetchRssAction}
|
||||
|
||||
actions = manager.list_actions()
|
||||
|
||||
assert actions[0]["contract"]["outputs"][0]["name"] == "torrents"
|
||||
assert actions[0]["contract"]["condition_fields"][0]["label"] == "资源"
|
||||
|
||||
@@ -40,9 +40,10 @@ def _encoded_context(context: ActionContext) -> dict:
|
||||
class _FakeWorkflowManager:
|
||||
"""记录执行动作的工作流管理器。"""
|
||||
|
||||
def __init__(self, calls, results=None):
|
||||
def __init__(self, calls, results=None, contracts=None):
|
||||
self.calls = calls
|
||||
self.results = results or {}
|
||||
self.contracts = contracts or {}
|
||||
self.received_inputs = []
|
||||
|
||||
def execute(self, workflow_id, action, context=None, inputs=None, runtime=None, cancel_token=None):
|
||||
@@ -61,6 +62,10 @@ class _FakeWorkflowManager:
|
||||
result = self.execute(workflow_id, action, context)
|
||||
return result.success, result.message, result.context
|
||||
|
||||
def get_action_contract(self, action_type):
|
||||
"""获取伪动作契约。"""
|
||||
return self.contracts.get(action_type) or {}
|
||||
|
||||
|
||||
def test_workflow_executor_resumes_downstream_nodes(monkeypatch):
|
||||
"""恢复执行时应释放已完成节点的后继节点。"""
|
||||
@@ -346,6 +351,44 @@ def test_workflow_executor_passes_declared_inputs(monkeypatch):
|
||||
}
|
||||
|
||||
|
||||
def test_workflow_executor_uses_contract_inputs(monkeypatch):
|
||||
"""未手写输入声明时应按动作契约读取上下文字段。"""
|
||||
calls = []
|
||||
fake_manager = _FakeWorkflowManager(
|
||||
calls,
|
||||
contracts={
|
||||
"NeedsTorrentsAction": {
|
||||
"inputs": [{"name": "torrents", "label": "资源", "kind": "list"}],
|
||||
"outputs": [],
|
||||
}
|
||||
},
|
||||
results={
|
||||
"A": lambda action, context: ActionResult(
|
||||
success=True,
|
||||
message=f"{action.name}完成",
|
||||
context=context,
|
||||
outputs={"torrents": ["a", "b"]}
|
||||
)
|
||||
}
|
||||
)
|
||||
workflow = _build_workflow(
|
||||
actions=[
|
||||
{"id": "A", "type": "FakeAction", "name": "动作A", "data": {}},
|
||||
{"id": "B", "type": "NeedsTorrentsAction", "name": "动作B", "data": {}},
|
||||
],
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
b_inputs = [item for action_id, item, _, _ in fake_manager.received_inputs if action_id == "B"][0]
|
||||
assert b_inputs == {"torrents": ["a", "b"]}
|
||||
|
||||
|
||||
def test_workflow_executor_persists_structured_state(monkeypatch):
|
||||
"""步骤回调应收到可持久化的结构化执行状态。"""
|
||||
calls = []
|
||||
|
||||
Reference in New Issue
Block a user