"""工作流管理 API 测试。 覆盖工作流的创建、查询、校验、发布、版本列表与删除等管理接口。 """ from fastapi.testclient import TestClient from wov_app.main import app def definition() -> dict: """构造一个引用 Echo 节点的合法工作流定义。""" return { "name": "echo-flow", "version": 1, "nodes": [ { "id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}, } ], "edges": [], "entry_inputs": {"video_uri": "file"}, "final_outputs": {"result": "step.file_uri"}, } def cyclic_definition() -> dict: """构造带环的 DAG 定义(a → b → a),用于验证保存/发布拒绝无效工作流。 环上的两个节点互相引用对方输出,形成了无法拓扑排序的依赖:节点 ID 唯一、 边引用的节点都存在,因此只有环检测能拦住它(R04 复现定义)。 """ return { "name": "cyclic", "version": 1, "nodes": [ {"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}}, {"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}}, ], "edges": [{"from": "a", "to": "b"}, {"from": "b", "to": "a"}], "entry_inputs": {"video_uri": "file"}, "final_outputs": {"result": "a.file_uri"}, } def test_create_workflow_rejects_cycle() -> None: """验证保存环形 DAG 返回 422,不把无效版本写入版本表。""" with TestClient(app) as client: response = client.post( "/api/admin/workflows", json={ "id": "cyclic-flow", "name": "Cyclic Flow", "definition": cyclic_definition(), }, ) assert response.status_code == 422 assert "cycle" in response.json()["detail"] # 拒绝保存时不得留下半成品工作流/版本记录。 db = app.state.db assert db.get_workflow("cyclic-flow") is None assert db.list_workflow_versions("cyclic-flow") == [] # 已有工作流重新提交带环定义:同样拒绝,不追加新版本。 assert client.post( "/api/admin/workflows", json={"id": "cycle-check", "name": "Cycle Check", "definition": definition()}, ).status_code == 200 assert client.post( "/api/admin/workflows", json={"id": "cycle-check", "name": "Cycle Check", "definition": cyclic_definition()}, ).status_code == 422 assert db.get_workflow("cycle-check")["latest_version"] == 1 assert len(db.list_workflow_versions("cycle-check")) == 1 db.delete_workflow("cycle-check") def test_validate_workflow_rejects_cycle() -> None: """验证只校验不保存的接口同样拒绝环形 DAG。""" with TestClient(app) as client: db = app.state.db # 独立工作流 ID 并显式清理,避免与其他用例(共用同一个测试数据库)互相影响。 assert client.post( "/api/admin/workflows", json={"id": "cycle-check", "name": "Cycle Check", "definition": definition()}, ).status_code == 200 response = client.post( "/api/admin/workflows/cycle-check/validate", json=cyclic_definition(), ) assert response.status_code == 422 assert "cycle" in response.json()["detail"] db.delete_workflow("cycle-check") def test_publish_rejects_cycle_in_latest_version() -> None: """验证历史遗留的环形版本不能被发布(发布前重新校验最新版本定义)。""" with TestClient(app) as client: db = app.state.db # 直接写库模拟修复前已保存的无效版本(保存接口现在会拒绝,只能这样构造)。 db.upsert_workflow( {"id": "legacy-cyclic", "name": "Legacy", "published": 0, "latest_version": 1} ) db.create_workflow_version("legacy-cyclic", 1, cyclic_definition()) response = client.post("/api/admin/workflows/legacy-cyclic/publish") assert response.status_code == 422 assert "cycle" in response.json()["detail"] assert db.get_workflow("legacy-cyclic")["published"] == 0 db.delete_workflow("legacy-cyclic") def test_workflow_crud_and_publish() -> None: """验证工作流 CRUD、校验、发布与版本列表的完整流程。""" with TestClient(app) as client: created = client.post( "/api/admin/workflows", json={ "id": "echo-flow", "name": "Echo Flow", "description": "demo", "definition": definition(), }, ) assert created.status_code == 200 assert created.json()["id"] == "echo-flow" assert client.get("/api/admin/workflows").status_code == 200 assert client.get("/api/admin/workflows/echo-flow").status_code == 200 assert client.get("/api/admin/workflows/missing").status_code == 404 validated = client.post( "/api/admin/workflows/echo-flow/validate", json=definition(), ) assert validated.status_code == 200 assert validated.json()["valid"] is True assert client.post( "/api/admin/workflows/missing/validate", json=definition(), ).status_code == 404 published = client.post("/api/admin/workflows/echo-flow/publish") assert published.status_code == 200 assert published.json()["published"] == "echo-flow" assert client.post("/api/admin/workflows/missing/publish").status_code == 404 versions = client.get("/api/admin/workflows/echo-flow/versions") assert versions.status_code == 200 assert len(versions.json()) == 1 assert client.get("/api/admin/workflows/missing/versions").status_code == 404 assert client.delete("/api/admin/workflows/echo-flow").status_code == 200 assert client.delete("/api/admin/workflows/echo-flow").status_code == 404 def test_workflow_slug_without_id() -> None: """验证未提供 ID 时后端会从名称生成 slug。""" with TestClient(app) as client: created = client.post( "/api/admin/workflows", json={ "name": "Echo Flow", "definition": definition(), }, ) assert created.status_code == 200 assert created.json()["id"] == "echo-flow" def test_workflow_validation_error() -> None: """验证重复节点 ID 的 DAG 会被拒绝。""" with TestClient(app) as client: response = client.post( "/api/admin/workflows", json={ "id": "bad", "name": "Bad", "definition": { "name": "Bad", "version": 1, "nodes": [ {"id": "a", "node_type": "x"}, {"id": "a", "node_type": "y"}, ], "edges": [], }, }, ) assert response.status_code == 422 def test_publish_workflow_without_version() -> None: """验证没有版本记录的工作流不能发布。""" with TestClient(app) as client: db = app.state.db db.upsert_workflow( {"id": "empty", "name": "Empty", "published": 0, "latest_version": 0} ) response = client.post("/api/admin/workflows/empty/publish") assert response.status_code == 422