250 lines
8.6 KiB
Python
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
|