"""Phase 3 tests: form-schema extraction, sync cache, input-schema endpoint.""" import json from pathlib import Path from uuid import uuid4 from app.flows import input_schema as parser from app.flows import sync from app.flows.lfx_client import LfxClient from app.settings import settings FIXTURES = Path(__file__).parent / "fixtures" / "flows" def _field(name, itype, **kw): base = { "name": name, "display_name": name.replace("_", " ").title(), "info": "", "value": None, "required": False, "advanced": False, "show": True, "_input_type": itype, } base.update(kw) return base def _doc(*fields, node_type="CustomInputForm", display="CustomInput Form"): template = {f["name"]: f for f in fields} node = { "data": { "type": node_type, "node": {"display_name": display, "template": template}, } } return {"data": {"nodes": [node], "edges": []}} def test_type_mapping_table(): doc = _doc( _field("a", "StrInput", value="x"), _field("b", "MessageTextInput"), _field("c", "MultilineInput"), _field("d", "SecretStrInput"), _field("e", "IntInput", value=3), _field("f", "FloatInput", value=1.5), _field("g", "BoolInput", value=True), _field( "h", "DropdownInput", options=["Music", "Youth"], value="Youth", ), _field( "i", "MultiselectInput", options=["Worship", "Courses"], value=["Worship"], ), _field( "j", "SliderInput", value=3, range_spec={"min": 1, "max": 5, "step": 1}, ), _field("k", "FileInput"), _field("l", "FutureFancyInput"), ) schema = parser.extract_form_schema(doc) assert "fallback" not in schema props = schema["json_schema"]["properties"] assert props["a"] == {"title": "A", "type": "string", "default": "x"} assert props["b"]["type"] == "string" assert props["e"] == {"title": "E", "type": "integer", "default": 3} assert props["f"]["type"] == "number" assert props["g"] == {"title": "G", "type": "boolean", "default": True} assert props["h"] == { "title": "H", "type": "string", "enum": ["Music", "Youth"], "default": "Youth", } assert props["i"]["type"] == "array" assert props["i"]["items"] == {"type": "string", "enum": ["Worship", "Courses"]} assert props["i"]["uniqueItems"] is True assert props["j"]["minimum"] == 1 assert props["j"]["maximum"] == 5 assert props["j"]["multipleOf"] == 1 assert props["k"]["type"] == "string" assert props["l"]["type"] == "string" ui = schema["ui_schema"] assert ui["c"] == {"ui:widget": "textarea"} assert ui["d"] == {"ui:widget": "password"} assert ui["j"] == {"ui:widget": "range"} def test_dropdown_default_outside_enum_omitted(): doc = _doc(_field("h", "DropdownInput", options=["Music"], value="")) props = parser.extract_form_schema(doc)["json_schema"]["properties"] assert props["h"] == {"title": "H", "type": "string", "enum": ["Music"]} def test_required_advanced_hidden_code(): doc = _doc( _field("a", "StrInput", required=True), _field("b", "StrInput", advanced=True), _field("c", "StrInput", show=False), ) doc["data"]["nodes"][0]["data"]["node"]["template"]["code"] = {"value": "x"} schema = parser.extract_form_schema(doc) assert schema["json_schema"]["required"] == ["a"] assert set(schema["json_schema"]["properties"]) == {"a", "b"} assert schema["ui_schema"]["ui:order"] == ["a", "b", "*"] def test_display_name_match_and_fallback(): doc = _doc(_field("a", "StrInput"), node_type="Custom", display="CustomInput X") assert set(parser.extract_form_schema(doc)["json_schema"]["properties"]) == {"a"} plain = { "data": { "nodes": [{"data": {"type": "ChatInput", "node": {"template": {}}}}], "edges": [], } } schema = parser.extract_form_schema(plain) assert schema["fallback"] == "textarea" assert schema["json_schema"]["properties"] == {} def test_real_intake_fixture_parses(): doc = json.loads((FIXTURES / "intake_form.json").read_text()) schema = parser.extract_form_schema(doc) props = schema["json_schema"]["properties"] assert "fallback" not in schema assert set(props) == { "full_name", "notes", "password", "age", "amount", "newsletter", "department", "topics", "satisfaction", "attachment", } assert "full_name" in schema["json_schema"]["required"] assert props["department"]["enum"] == ["Music", "Youth", "Seniors", "Admin"] assert schema["ui_schema"]["notes"] == {"ui:widget": "textarea"} def _lfx() -> LfxClient: return LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None) async def test_sync_caches_schema_per_version(session, lfx_server, tmp_path): (tmp_path / "intake_form.json").write_text((FIXTURES / "intake_form.json").read_text()) report = await sync.sync_from_directory(session, _lfx(), tmp_path) assert not report.errors, report.errors from app.flows import store defs = store.list_definitions(session) definition = next(d for d in defs if d.name == "intake_form") versions = store.list_versions(session, definition.id) assert len(versions) == 1 cached = versions[0].input_schema assert cached["json_schema"]["properties"]["full_name"]["type"] == "string" assert "form_values" not in cached["json_schema"]["properties"] async def test_input_schema_endpoint(client, lfx_server, login): assert (await client.get(f"/api/flows/{uuid4()}/input-schema")).status_code == 404 await login() doc = json.loads((FIXTURES / "intake_form.json").read_text()) created = await client.post("/api/flows", json={"name": "schema-e2e"}) flow_id = created.json()["id"] version = await client.post(f"/api/flows/{flow_id}/versions", json={"langflow_json": doc}) assert version.status_code == 201, version.text assert version.json()["input_schema"]["json_schema"]["properties"]["age"]["type"] == "integer" # Created but never deployed → 404. assert (await client.get(f"/api/flows/{flow_id}/input-schema")).status_code == 404 deployed = await client.post(f"/api/flows/{flow_id}/versions/1/deploy") assert deployed.status_code == 200, deployed.text response = await client.get(f"/api/flows/{flow_id}/input-schema") assert response.status_code == 200, response.text body = response.json() assert body["version_no"] == 1 assert body["fallback"] is None assert set(body["json_schema"]["properties"]) >= {"full_name", "topics"} assert body["ui_schema"]["password"] == {"ui:widget": "password"}