Files
vrsub/tests/test_workflow_api.py
T

203 lines
7.5 KiB
Python

"""工作流管理 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