refactor: finish declutter - test_flow removal, NOT NULL schema, parametrized tests, smoke harness, docs

This commit is contained in:
Jonas Miederer 2026-10-01 00:11:43 +02:00
parent 696db34c90
commit 92bb3bacba
23 changed files with 552 additions and 1261 deletions

View file

@ -4,6 +4,7 @@ import json
import sys
from pathlib import Path
import pytest
from lfx.custom import Component
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
@ -208,28 +209,212 @@ def test_bauvorhaben_collect_only_own_fields():
assert "full_name" not in collected
def test_intake_fixture_shape():
doc = json.loads((FIXTURES / "intake_form.json").read_text())
def _parse_chat_json(text: str) -> dict:
"""Parse terminal ChatOutput text: raw JSON or a ```json fenced block."""
text = text.strip()
if text.startswith("```"):
lines = text.splitlines()
assert lines[0].startswith("```"), text[:60]
assert lines[-1].startswith("```"), text[-60:]
text = "\n".join(lines[1:-1])
return json.loads(text)
@pytest.mark.parametrize(
(
"fixture_file",
"doc_name",
"node_id",
"expected_type",
"expected_display",
"display_mode",
"expected_fields",
"expected_edges",
),
[
(
"intake_form.json",
None,
"CustomInputForm-intake",
"CustomInputForm",
"CustomInput",
"startswith",
{
"full_name",
"notes",
"password",
"age",
"amount",
"newsletter",
"department",
"topics",
"satisfaction",
"attachment",
},
[
("ChatInput-entry", "CustomInputForm-intake"),
("CustomInputForm-intake", "ChatOutput-collected"),
],
),
(
"bauvorhaben.json",
"bauvorhaben",
"CustomInputBauvorhaben-bauvorhaben",
"CustomInputBauvorhaben",
"Bauvorhaben Form",
"exact",
{
"form_values",
"gebaeude",
"bestandteil",
"glockenmassnahme",
"gussjahr",
"gebaeudeart",
"denkmalschutz",
"denkmalnummer",
"code",
},
[
("ChatInput-entry", "CustomInputBauvorhaben-bauvorhaben"),
("CustomInputBauvorhaben-bauvorhaben", "ChatOutput-collected"),
],
),
(
"mitarbeiter_einstellen.json",
"mitarbeiter_einstellen",
"CustomInputMitarbeiter-mitarbeiter",
"CustomInputMitarbeiter",
"Mitarbeiter Einstellen Form",
"exact",
{
"form_values",
"rolle",
"einstufung",
"mitarbeiter_name",
"geburtsdatum",
"code",
},
[
("ChatInput-entry", "CustomInputMitarbeiter-mitarbeiter"),
("CustomInputMitarbeiter-mitarbeiter", "Gehaltsrechner-gehalt"),
("Gehaltsrechner-gehalt", "ChatOutput-collected"),
],
),
],
ids=["intake", "bauvorhaben", "mitarbeiter"],
)
def test_fixture_shape(
fixture_file,
doc_name,
node_id,
expected_type,
expected_display,
display_mode,
expected_fields,
expected_edges,
):
doc = json.loads((FIXTURES / fixture_file).read_text(encoding="utf-8"))
if doc_name is not None:
assert doc["name"] == doc_name
nodes = {n["id"]: n for n in doc["data"]["nodes"]}
form = nodes["CustomInputForm-intake"]
assert form["data"]["type"] == "CustomInputForm"
assert form["data"]["node"]["display_name"].startswith("CustomInput")
form = nodes[node_id]
assert form["data"]["type"] == expected_type
display = form["data"]["node"]["display_name"]
if display_mode == "startswith":
assert display.startswith(expected_display)
else:
assert display == expected_display
template = form["data"]["node"]["template"]
for field in FORM_FIELDS:
assert field in template, field
assert set(template) >= expected_fields
assert template["form_values"]["show"] is False
assert len(template["code"]["value"]) > 200
outputs = {o["name"]: o for o in form["data"]["node"]["outputs"]}
assert outputs["form_data"]["types"] == ["JSON"] # JSON object, not Message
edges = {(e["source"], e["target"]) for e in doc["data"]["edges"]}
assert ("ChatInput-entry", "CustomInputForm-intake") in edges
assert ("CustomInputForm-intake", "ChatOutput-collected") in edges
for edge in expected_edges:
assert edge in edges
async def test_intake_deploy_and_run_with_values(client, lfx_server, login):
async def _check_intake_result(client, flow_id, version, run):
# collect() returns Data: the terminal ChatOutput renders it as a
# fenced ```json block, so unwrap before parsing.
result = _parse_chat_json(run["output"]["result"])
assert result["full_name"] == "Maria Muster"
assert result["age"] == 41
assert result["department"] == "Music" # default preserved
async def _check_bauvorhaben_result(client, flow_id, version, run):
# Branch rules are cached per version and served via input-schema.
cached = version["input_schema"]
assert len(cached["json_schema"]["allOf"]) == 5
schema = await client.get(f"/api/flows/{flow_id}/input-schema")
assert schema.status_code == 200, schema.text
assert set(schema.json()["json_schema"]["properties"]) == {"gebaeude", "denkmalschutz"}
result = _parse_chat_json(run["output"]["result"])
assert result["gebaeude"] == "Kirche"
assert result["bestandteil"] == ["Glocke", "Orgel"]
assert result["gussjahr"] == 1890
assert result["denkmalnummer"] == "D-123"
# Only asked fields: unasked code defaults (age, amount, ...) stay out.
assert set(result) == {
"gebaeude",
"bestandteil",
"glockenmassnahme",
"gussjahr",
"denkmalschutz",
"denkmalnummer",
}
async def _check_mitarbeiter_result(client, flow_id, version, run):
result = run["output"]["result"]
assert "65000" in result
assert "Maria Muster" in result
@pytest.mark.parametrize(
("fixture_file", "flow_name", "run_input", "check_result"),
[
(
"intake_form.json",
"intake-e2e",
{"full_name": "Maria Muster", "age": 41},
_check_intake_result,
),
(
"bauvorhaben.json",
"bauvorhaben-e2e",
{
"gebaeude": "Kirche",
"bestandteil": ["Glocke", "Orgel"],
"glockenmassnahme": "Neuguss",
"gussjahr": 1890,
"denkmalschutz": True,
"denkmalnummer": "D-123",
},
_check_bauvorhaben_result,
),
(
"mitarbeiter_einstellen.json",
"mitarbeiter-e2e",
{
"rolle": "Pfarrer:in",
"einstufung": "Stufe 2",
"mitarbeiter_name": "Maria Muster",
"geburtsdatum": "1980-01-01",
},
_check_mitarbeiter_result,
),
],
ids=["intake", "bauvorhaben", "mitarbeiter"],
)
async def test_deploy_and_run(
client, lfx_server, login, fixture_file, flow_name, run_input, check_result
):
await login()
doc = json.loads((FIXTURES / "intake_form.json").read_text())
created = await client.post("/api/flows", json={"name": "intake-e2e"})
doc = json.loads((FIXTURES / fixture_file).read_text(encoding="utf-8"))
created = await client.post("/api/flows", json={"name": flow_name})
assert created.status_code == 201, created.text
flow_id = created.json()["id"]
@ -242,29 +427,10 @@ async def test_intake_deploy_and_run_with_values(client, lfx_server, login):
deployed = await client.post(f"/api/flows/{flow_id}/versions/1/deploy")
assert deployed.status_code == 200, deployed.text
run = await client.post(
f"/api/flows/{flow_id}/run",
json={"input": {"full_name": "Maria Muster", "age": 41}},
)
run = await client.post(f"/api/flows/{flow_id}/run", json={"input": run_input})
assert run.status_code == 200, run.text
assert run.json()["status"] == "completed"
# collect() returns Data: the terminal ChatOutput renders it as a
# fenced ```json block, so unwrap before parsing.
result = _parse_chat_json(run.json()["output"]["result"])
assert result["full_name"] == "Maria Muster"
assert result["age"] == 41
assert result["department"] == "Music" # default preserved
def _parse_chat_json(text: str) -> dict:
"""Parse terminal ChatOutput text: raw JSON or a ```json fenced block."""
text = text.strip()
if text.startswith("```"):
lines = text.splitlines()
assert lines[0].startswith("```"), text[:60]
assert lines[-1].startswith("```"), text[-60:]
text = "\n".join(lines[1:-1])
return json.loads(text)
await check_result(client, flow_id, version.json(), run.json())
def test_embedded_code_matches_template_fields():
@ -341,32 +507,6 @@ def test_kirche_node_inlines_core():
assert "from kirche" not in composed and "def parse_envelope" in composed
def test_bauvorhaben_fixture_shape():
doc = json.loads((FIXTURES / "bauvorhaben.json").read_text(encoding="utf-8"))
assert doc["name"] == "bauvorhaben"
nodes = {n["id"]: n for n in doc["data"]["nodes"]}
form = nodes["CustomInputBauvorhaben-bauvorhaben"]
assert form["data"]["type"] == "CustomInputBauvorhaben"
assert form["data"]["node"]["display_name"] == "Bauvorhaben Form"
template = form["data"]["node"]["template"]
assert set(template) >= {
"form_values",
"gebaeude",
"bestandteil",
"glockenmassnahme",
"gussjahr",
"gebaeudeart",
"denkmalschutz",
"denkmalnummer",
"code",
}
outputs = {o["name"]: o for o in form["data"]["node"]["outputs"]}
assert outputs["form_data"]["types"] == ["JSON"]
edges = {(e["source"], e["target"]) for e in doc["data"]["edges"]}
assert ("ChatInput-entry", "CustomInputBauvorhaben-bauvorhaben") in edges
assert ("CustomInputBauvorhaben-bauvorhaben", "ChatOutput-collected") in edges
def test_bauvorhaben_schema_branches():
from app.flows import input_schema as parser
@ -411,59 +551,6 @@ def test_bauvorhaben_schema_branches():
}
async def test_bauvorhaben_deploy_and_run_nested_values(client, lfx_server, login):
await login()
doc = json.loads((FIXTURES / "bauvorhaben.json").read_text(encoding="utf-8"))
created = await client.post("/api/flows", json={"name": "bauvorhaben-e2e"})
assert created.status_code == 201, created.text
flow_id = created.json()["id"]
version = await client.post(
f"/api/flows/{flow_id}/versions",
json={"langflow_json": doc, "author": "test"},
)
assert version.status_code == 201, version.text
# Branch rules are cached per version and served via input-schema.
cached = version.json()["input_schema"]
assert len(cached["json_schema"]["allOf"]) == 5
deployed = await client.post(f"/api/flows/{flow_id}/versions/1/deploy")
assert deployed.status_code == 200, deployed.text
schema = await client.get(f"/api/flows/{flow_id}/input-schema")
assert schema.status_code == 200, schema.text
assert set(schema.json()["json_schema"]["properties"]) == {"gebaeude", "denkmalschutz"}
run = await client.post(
f"/api/flows/{flow_id}/run",
json={
"input": {
"gebaeude": "Kirche",
"bestandteil": ["Glocke", "Orgel"],
"glockenmassnahme": "Neuguss",
"gussjahr": 1890,
"denkmalschutz": True,
"denkmalnummer": "D-123",
}
},
)
assert run.status_code == 200, run.text
assert run.json()["status"] == "completed"
result = _parse_chat_json(run.json()["output"]["result"])
assert result["gebaeude"] == "Kirche"
assert result["bestandteil"] == ["Glocke", "Orgel"]
assert result["gussjahr"] == 1890
assert result["denkmalnummer"] == "D-123"
# Only asked fields: unasked code defaults (age, amount, ...) stay out.
assert set(result) == {
"gebaeude",
"bestandteil",
"glockenmassnahme",
"gussjahr",
"denkmalschutz",
"denkmalnummer",
}
def test_mitarbeiter_component_contract():
from kirche.mitarbeiter_form import CustomInputMitarbeiter
@ -485,60 +572,3 @@ def test_mitarbeiter_collect_branch_default():
assert "einstufung" not in form.collect().data # rolle defaults to Assistenz
form.form_values = json.dumps({"payload": {"rolle": "Pfarrer:in"}})
assert form.collect().data["einstufung"] == "Stufe 1"
def test_mitarbeiter_fixture_shape():
doc = json.loads((FIXTURES / "mitarbeiter_einstellen.json").read_text(encoding="utf-8"))
assert doc["name"] == "mitarbeiter_einstellen"
nodes = {n["id"]: n for n in doc["data"]["nodes"]}
form = nodes["CustomInputMitarbeiter-mitarbeiter"]
assert form["data"]["type"] == "CustomInputMitarbeiter"
assert form["data"]["node"]["display_name"] == "Mitarbeiter Einstellen Form"
template = form["data"]["node"]["template"]
assert set(template) >= {
"form_values",
"rolle",
"einstufung",
"mitarbeiter_name",
"geburtsdatum",
"code",
}
outputs = {o["name"]: o for o in form["data"]["node"]["outputs"]}
assert outputs["form_data"]["types"] == ["JSON"]
edges = {(e["source"], e["target"]) for e in doc["data"]["edges"]}
assert ("ChatInput-entry", "CustomInputMitarbeiter-mitarbeiter") in edges
assert ("CustomInputMitarbeiter-mitarbeiter", "Gehaltsrechner-gehalt") in edges
assert ("Gehaltsrechner-gehalt", "ChatOutput-collected") in edges
async def test_mitarbeiter_deploy_and_run_salary(client, lfx_server, login):
await login()
doc = json.loads((FIXTURES / "mitarbeiter_einstellen.json").read_text(encoding="utf-8"))
created = await client.post("/api/flows", json={"name": "mitarbeiter-e2e"})
assert created.status_code == 201, created.text
flow_id = created.json()["id"]
version = await client.post(
f"/api/flows/{flow_id}/versions",
json={"langflow_json": doc, "author": "test"},
)
assert version.status_code == 201, version.text
deployed = await client.post(f"/api/flows/{flow_id}/versions/1/deploy")
assert deployed.status_code == 200, deployed.text
run = await client.post(
f"/api/flows/{flow_id}/run",
json={
"input": {
"rolle": "Pfarrer:in",
"einstufung": "Stufe 2",
"mitarbeiter_name": "Maria Muster",
"geburtsdatum": "1980-01-01",
}
},
)
assert run.status_code == 200, run.text
assert run.json()["status"] == "completed"
result = run.json()["output"]["result"]
assert "65000" in result
assert "Maria Muster" in result