feat: asked-field envelope, standalone form components, branched forms
This commit is contained in:
parent
0d43b0e8fb
commit
f27be044b4
26 changed files with 3651 additions and 104 deletions
|
|
@ -93,6 +93,121 @@ def test_collect_keeps_unknown_envelope_keys():
|
|||
assert collected["department"] == "Music" # default kept
|
||||
|
||||
|
||||
def test_collect_fields_list_restricts_defaults():
|
||||
"""Asked-field list: unasked code defaults stay out of the output."""
|
||||
form = CustomInputForm()
|
||||
form.form_values = json.dumps(
|
||||
{"payload": {"full_name": "Maria"}, "fields": ["full_name", "age"]}
|
||||
)
|
||||
collected = form.collect().data
|
||||
assert collected == {"full_name": "Maria", "age": 18}
|
||||
|
||||
|
||||
def test_collect_fields_list_fills_missing_from_defaults():
|
||||
form = CustomInputForm()
|
||||
form.form_values = json.dumps(
|
||||
{"payload": {"full_name": "Maria"}, "fields": ["full_name", "department"]}
|
||||
)
|
||||
collected = form.collect().data
|
||||
assert collected == {"full_name": "Maria", "department": "Music"}
|
||||
|
||||
|
||||
def test_collect_fields_list_ignores_undeclared_names():
|
||||
"""Asked-but-undeclared template fields contribute no Nones."""
|
||||
form = CustomInputForm()
|
||||
form.form_values = json.dumps({"payload": {"room": "Chapel"}, "fields": ["full_name", "room"]})
|
||||
collected = form.collect().data
|
||||
assert collected == {"full_name": "", "room": "Chapel"}
|
||||
|
||||
|
||||
def test_collect_raw_dict_ignores_fields_key():
|
||||
"""A raw dict is payload, not envelope: 'fields' is just a value."""
|
||||
form = CustomInputForm()
|
||||
form.form_values = json.dumps({"fields": ["x"], "full_name": "Maria"})
|
||||
collected = form.collect().data
|
||||
assert collected["fields"] == ["x"]
|
||||
assert collected["full_name"] == "Maria"
|
||||
assert collected["age"] == 18 # all defaults apply without envelope
|
||||
|
||||
|
||||
def test_collect_hides_branch_default_until_trigger_matches():
|
||||
"""Hidden branch fields contribute no defaults (frontend parity)."""
|
||||
form = CustomInputForm()
|
||||
form._inputs["notes"].info = "Anything [visible_if department=Youth]"
|
||||
assert "notes" not in form.collect().data # department defaults to Music
|
||||
form.form_values = json.dumps({"payload": {"department": "Youth"}})
|
||||
assert form.collect().data["notes"] == ""
|
||||
|
||||
|
||||
def test_collect_branch_visibility_uses_submitted_triggers():
|
||||
form = CustomInputForm()
|
||||
form._inputs["notes"].info = "[visible_if age=41]"
|
||||
form.form_values = json.dumps({"payload": {"age": 41}, "fields": ["age", "notes"]})
|
||||
assert form.collect().data["notes"] == ""
|
||||
form.form_values = json.dumps({"payload": {"age": 18}, "fields": ["age", "notes"]})
|
||||
assert "notes" not in form.collect().data
|
||||
|
||||
|
||||
def test_collect_dangling_marker_stays_visible():
|
||||
form = CustomInputForm()
|
||||
form._inputs["notes"].info = "[visible_if nope=x]"
|
||||
assert "notes" in form.collect().data
|
||||
|
||||
|
||||
def test_bauvorhaben_component_contract():
|
||||
from kirche.bauvorhaben_form import CustomInputBauvorhaben
|
||||
|
||||
assert CustomInputBauvorhaben.display_name == "Bauvorhaben Form"
|
||||
inputs = {i.name: i for i in CustomInputBauvorhaben.inputs}
|
||||
assert sorted(n for n in inputs if n != "form_values") == sorted(
|
||||
[
|
||||
"gebaeude",
|
||||
"bestandteil",
|
||||
"glockenmassnahme",
|
||||
"gussjahr",
|
||||
"gebaeudeart",
|
||||
"denkmalschutz",
|
||||
"denkmalnummer",
|
||||
]
|
||||
)
|
||||
assert inputs["form_values"].show is False
|
||||
outputs = {o.name: o for o in CustomInputBauvorhaben.outputs}
|
||||
assert set(outputs) == {"form_data"}
|
||||
|
||||
|
||||
def test_bauvorhaben_collect_only_own_fields():
|
||||
"""Standalone component: no intake leftovers, hidden branches dropped."""
|
||||
from kirche.bauvorhaben_form import CustomInputBauvorhaben
|
||||
|
||||
form = CustomInputBauvorhaben()
|
||||
default = form.collect().data
|
||||
assert "full_name" not in default and "age" not in default
|
||||
assert default["gebaeude"] == "" # class default (template ships " " sentinel)
|
||||
assert "bestandteil" not in default # hidden (gebaeude default "").
|
||||
assert "gebaeudeart" not in default
|
||||
|
||||
form.form_values = json.dumps(
|
||||
{
|
||||
"payload": {"gebaeude": "Kirche", "bestandteil": ["Glocke"]},
|
||||
"fields": [
|
||||
"gebaeude",
|
||||
"bestandteil",
|
||||
"glockenmassnahme",
|
||||
"gussjahr",
|
||||
"gebaeudeart",
|
||||
"denkmalschutz",
|
||||
"denkmalnummer",
|
||||
],
|
||||
}
|
||||
)
|
||||
collected = form.collect().data
|
||||
assert collected["bestandteil"] == ["Glocke"]
|
||||
assert collected["glockenmassnahme"] == "Reinigung" # visible branch default
|
||||
assert "gussjahr" not in collected # hidden branch
|
||||
assert "gebaeudeart" not in collected
|
||||
assert "full_name" not in collected
|
||||
|
||||
|
||||
def test_intake_fixture_shape():
|
||||
doc = json.loads((FIXTURES / "intake_form.json").read_text())
|
||||
nodes = {n["id"]: n for n in doc["data"]["nodes"]}
|
||||
|
|
@ -152,12 +267,44 @@ def _parse_chat_json(text: str) -> dict:
|
|||
return json.loads(text)
|
||||
|
||||
|
||||
def test_embedded_code_matches_template_fields():
|
||||
"""Node template fields == code-declared inputs (no unasked defaults).
|
||||
|
||||
The envelope tolerates drift at runtime, but authoring must stay
|
||||
honest: what Langflow shows in the code editor must be what the form
|
||||
asks. Building blocks: lfx's own create_class on the embedded code.
|
||||
"""
|
||||
from lfx.custom import validate as validate_mod
|
||||
|
||||
cases = {
|
||||
"intake_form.json": ("CustomInputForm-intake", "CustomInputForm"),
|
||||
"bauvorhaben.json": ("CustomInputBauvorhaben-bauvorhaben", "CustomInputBauvorhaben"),
|
||||
"mitarbeiter_einstellen.json": (
|
||||
"CustomInputMitarbeiter-mitarbeiter",
|
||||
"CustomInputMitarbeiter",
|
||||
),
|
||||
}
|
||||
for filename, (node_id, class_name) in cases.items():
|
||||
doc = json.loads((FIXTURES / filename).read_text(encoding="utf-8"))
|
||||
node = next(n for n in doc["data"]["nodes"] if n["id"] == node_id)
|
||||
assert node["data"]["type"] == class_name, filename
|
||||
template = node["data"]["node"]["template"]
|
||||
template_names = {
|
||||
field["name"] for field in template.values() if isinstance(field, dict)
|
||||
} - {"code"}
|
||||
code = template["code"]["value"]
|
||||
cls = validate_mod.create_class(code, class_name)
|
||||
code_names = {inp.name for inp in cls.inputs}
|
||||
assert code_names == template_names, filename
|
||||
|
||||
|
||||
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["CustomInputForm-bauvorhaben"]
|
||||
assert form["data"]["type"] == "CustomInputForm"
|
||||
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",
|
||||
|
|
@ -173,8 +320,8 @@ def test_bauvorhaben_fixture_shape():
|
|||
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", "CustomInputForm-bauvorhaben") in edges
|
||||
assert ("CustomInputForm-bauvorhaben", "ChatOutput-collected") in edges
|
||||
assert ("ChatInput-entry", "CustomInputBauvorhaben-bauvorhaben") in edges
|
||||
assert ("CustomInputBauvorhaben-bauvorhaben", "ChatOutput-collected") in edges
|
||||
|
||||
|
||||
def test_bauvorhaben_schema_branches():
|
||||
|
|
@ -262,3 +409,92 @@ async def test_bauvorhaben_deploy_and_run_nested_values(client, lfx_server, logi
|
|||
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
|
||||
|
||||
assert CustomInputMitarbeiter.display_name == "Mitarbeiter Einstellen Form"
|
||||
inputs = {i.name: i for i in CustomInputMitarbeiter.inputs}
|
||||
assert sorted(n for n in inputs if n != "form_values") == sorted(
|
||||
["rolle", "einstufung", "mitarbeiter_name", "geburtsdatum"]
|
||||
)
|
||||
assert inputs["form_values"].show is False
|
||||
outputs = {o.name: o for o in CustomInputMitarbeiter.outputs}
|
||||
assert set(outputs) == {"form_data"}
|
||||
|
||||
|
||||
def test_mitarbeiter_collect_branch_default():
|
||||
"""Einstufung default applies only when Pfarrer:in is selected."""
|
||||
from kirche.mitarbeiter_form import CustomInputMitarbeiter
|
||||
|
||||
form = CustomInputMitarbeiter()
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue