kirche-einfach/backend/tests/test_flows_api.py

250 lines
8.6 KiB
Python

"""Step 3 tests: flows CRUD, versions, deploy, sync/stream runs, handoff E2E."""
import json
from pathlib import Path
from uuid import UUID, uuid4
from app.flows.sync import lfx_id_for
FIXTURES = Path(__file__).parent / "fixtures" / "flows"
def load_fixture(name: str) -> dict:
return json.loads((FIXTURES / name).read_text())
async def _create_flow(client, name="regeltest"):
response = await client.post(
"/api/flows", json={"name": name, "description": "test flow"}
)
assert response.status_code == 201, response.text
return response.json()
async def _create_version(client, flow_id, fixture="simple_rule.json"):
response = await client.post(
f"/api/flows/{flow_id}/versions",
json={"langflow_json": load_fixture(fixture), "author": "test"},
)
assert response.status_code == 201, response.text
return response.json()
async def _deploy(client, flow_id, version_no=1):
response = await client.post(f"/api/flows/{flow_id}/versions/{version_no}/deploy")
assert response.status_code == 200, response.text
return response.json()
async def test_flow_crud(client):
created = await _create_flow(client, "crud-flow")
assert created["status"] == "active"
response = await client.get(f"/api/flows/{created['id']}")
assert response.status_code == 200
assert response.json()["name"] == "crud-flow"
response = await client.get("/api/flows")
assert response.status_code == 200
assert any(f["id"] == created["id"] for f in response.json())
response = await client.post("/api/flows", json={"name": "crud-flow"})
assert response.status_code == 409
response = await client.get(f"/api/flows/{uuid4()}")
assert response.status_code == 404
async def test_version_validation_and_numbering(client):
flow = await _create_flow(client, "versioned")
v1 = await _create_version(client, flow["id"])
assert v1["version_no"] == 1
v2 = await _create_version(client, flow["id"])
assert v2["version_no"] == 2
assert v1["deployed_at"] is None
response = await client.post(
f"/api/flows/{flow['id']}/versions", json={"langflow_json": {"nope": 1}}
)
assert response.status_code == 422
response = await client.post(
f"/api/flows/{uuid4()}/versions",
json={"langflow_json": load_fixture("simple_rule.json")},
)
assert response.status_code == 404
response = await client.get(f"/api/flows/{flow['id']}/versions")
assert [v["version_no"] for v in response.json()] == [1, 2]
async def test_deploy_and_run_standard_and_sonderfall(client, lfx_server):
flow = await _create_flow(client, "dachsanierung")
await _create_version(client, flow["id"])
deployed = await _deploy(client, flow["id"])
assert deployed["lfx_flow_id"] == lfx_id_for(UUID(flow["id"]), 1)
assert deployed["deployed_at"] is not None
response = await client.post(
f"/api/flows/{flow['id']}/run",
json={"input": {"betrag": 120000, "schwelle": 200000}},
)
assert response.status_code == 200, response.text
run = response.json()
assert run["status"] == "completed"
assert "Standardfall" in run["output"]["result"]
assert run["finished_at"] is not None
response = await client.post(
f"/api/flows/{flow['id']}/run",
json={"input": {"betrag": 250000, "schwelle": 200000}},
)
assert response.status_code == 200, response.text
run = response.json()
assert run["status"] == "completed"
assert "Sonderfall" in run["output"]["result"]
response = await client.get(f"/api/flows/runs/{run['id']}")
assert response.status_code == 200
assert response.json()["id"] == run["id"]
async def test_run_undeployed_version_fails(client):
flow = await _create_flow(client, "undeployed")
await _create_version(client, flow["id"])
response = await client.post(
f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}}
)
assert response.status_code == 422
async def test_run_explicit_version_no(client, lfx_server):
flow = await _create_flow(client, "explicit-version")
await _create_version(client, flow["id"])
await _create_version(client, flow["id"])
await _deploy(client, flow["id"], version_no=2)
response = await client.post(
f"/api/flows/{flow['id']}/run",
json={"input": {"betrag": 1}, "version_no": 2},
)
assert response.status_code == 200, response.text
assert response.json()["status"] == "completed"
async def test_run_stream_events(client, lfx_server):
flow = await _create_flow(client, "streamed")
await _create_version(client, flow["id"])
await _deploy(client, flow["id"])
events: list[tuple[str, str]] = []
async with client.stream(
"POST",
f"/api/flows/{flow['id']}/run?stream=true",
json={"input": {"betrag": 1}},
) as response:
assert response.status_code == 200, await response.aread()
current_event = "message"
async for line in response.aiter_lines():
if line.startswith("event:"):
current_event = line.split(":", 1)[1].strip()
elif line.startswith("data:"):
events.append((current_event, line.split(":", 1)[1].strip()))
current_event = "message"
assert events, "expected SSE events"
assert events[0][0] == "run"
first_payload = json.loads(events[0][1])
assert "run_id" in first_payload
assert events[-1][0] == "run_finished"
last_payload = json.loads(events[-1][1])
assert last_payload["status"] == "completed"
response = await client.get(f"/api/flows/runs/{first_payload['run_id']}")
assert response.status_code == 200
assert response.json()["status"] == "completed"
async def test_approval_handoff_end_to_end(client, lfx_server, live_server):
flow = await _create_flow(client, "freigabe")
await _create_version(client, flow["id"], fixture="approval_handoff.json")
await _deploy(client, flow["id"])
response = await client.post(
f"/api/flows/{flow['id']}/run",
json={
"input": {"betrag": 250000, "schwelle": 200000},
"callback_base_url": live_server,
},
)
assert response.status_code == 200, response.text
run = response.json()
assert run["status"] == "pending_approval"
assert "PENDING_HUMAN_APPROVAL" in run["output"]["result"]
response = await client.get("/api/approvals")
assert response.status_code == 200
tasks = response.json()
assert len(tasks) == 1
task = tasks[0]
assert task["run_id"] == run["id"]
assert task["status"] == "open"
assert task["payload"]["facts"]["betrag"] == 250000
async def test_approval_handoff_auto_creates_no_task(client, lfx_server, live_server):
flow = await _create_flow(client, "freigabe-auto")
await _create_version(client, flow["id"], fixture="approval_handoff.json")
await _deploy(client, flow["id"])
response = await client.post(
f"/api/flows/{flow['id']}/run",
json={
"input": {"betrag": 50000, "schwelle": 200000},
"callback_base_url": live_server,
},
)
assert response.status_code == 200, response.text
assert response.json()["status"] == "completed"
response = await client.get("/api/approvals")
assert response.status_code == 200
assert response.json() == []
async def test_approval_callback_contract(client, session):
from uuid import UUID
from app.flows import store as flow_store
flow = await _create_flow(client, "callback-target")
version = await _create_version(client, flow["id"])
version_obj = flow_store.get_version(session, UUID(version["id"]))
run = flow_store.create_run(session, version_obj, input={}, tenant_id="default")
response = await client.post(
"/api/approvals",
json={"run_id": str(run.id), "title": "bitte pruefen", "payload": {"x": 1}},
)
assert response.status_code == 201, response.text
task = response.json()
assert task["status"] == "open"
assert task["tenant_id"] == "default"
assert task["version_id"] == version["id"]
response = await client.get(f"/api/flows/runs/{run.id}")
assert response.json()["status"] == "pending_approval"
response = await client.post("/api/approvals", json={"run_id": str(run.id)})
assert response.status_code == 409
run2 = flow_store.create_run(session, version_obj, input={}, tenant_id="default")
response = await client.post(
"/api/approvals",
json={"run_id": str(run2.id), "continuation_flow_id": str(uuid4())},
)
assert response.status_code == 422
response = await client.post("/api/approvals", json={"run_id": str(uuid4())})
assert response.status_code == 404