refactor: finish declutter - test_flow removal, NOT NULL schema, parametrized tests, smoke harness, docs
This commit is contained in:
parent
696db34c90
commit
92bb3bacba
23 changed files with 552 additions and 1261 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"""Shared e2e harness: free ports, health waits, lfx spawn, migrate, sessions.
|
||||
|
||||
Single place for logic duplicated between tests/conftest.py and
|
||||
scripts/smoke_e2e.py (~60 lines). Full real-LFX runs are kept.
|
||||
Single place for logic shared between tests/conftest.py and
|
||||
scripts/smoke_e2e.py. Full real-LFX runs are kept.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -15,6 +15,10 @@ import time
|
|||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# Single constant: lfx prefers LANGFLOW_API_KEY over LFX_API_KEY, so the
|
||||
# repo .env value must be cleared wherever a test key is used.
|
||||
TEST_LFX_KEY = "test-lfx-key"
|
||||
|
||||
|
||||
def free_port() -> int:
|
||||
sock = socket.socket()
|
||||
|
|
@ -36,10 +40,14 @@ def wait_healthy(url: str, deadline_s: float = 240.0) -> None:
|
|||
raise RuntimeError(f"server did not become healthy: {url}")
|
||||
|
||||
|
||||
def spawn_lfx(flow_dir: Path, port: int) -> subprocess.Popen:
|
||||
def spawn_lfx(
|
||||
flow_dir: Path, port: int, env_extra: dict | None = None, api_key: str = TEST_LFX_KEY
|
||||
) -> subprocess.Popen:
|
||||
bindir = Path(sys.executable).parent
|
||||
lfx_bin = shutil.which("lfx", path=str(bindir)) or "lfx"
|
||||
env = {**os.environ, "LANGFLOW_API_KEY": "", "LFX_API_KEY": "test-lfx-key"}
|
||||
env = {**os.environ, "LANGFLOW_API_KEY": "", "LFX_API_KEY": api_key}
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
return subprocess.Popen(
|
||||
[
|
||||
lfx_bin,
|
||||
|
|
@ -59,20 +67,21 @@ def spawn_lfx(flow_dir: Path, port: int) -> subprocess.Popen:
|
|||
)
|
||||
|
||||
|
||||
def migrate_throwaway(backend_dir: Path) -> None:
|
||||
def migrate_throwaway(backend_dir: Path, db_url: str | None = None) -> None:
|
||||
from alembic import command as alembic_command
|
||||
from alembic.config import Config
|
||||
|
||||
cfg = Config()
|
||||
cfg.set_main_option("script_location", str(backend_dir / "migrations"))
|
||||
if db_url is not None:
|
||||
cfg.set_main_option("sqlalchemy.url", db_url)
|
||||
# migrations/env.py takes the URL from settings (not the ini value).
|
||||
from app.settings import settings as app_settings
|
||||
|
||||
app_settings.database_url = db_url
|
||||
alembic_command.upgrade(cfg, "head")
|
||||
|
||||
|
||||
# Single constant: lfx prefers LANGFLOW_API_KEY over LFX_API_KEY, so the
|
||||
# repo .env value must be cleared wherever a test key is used.
|
||||
TEST_LFX_KEY = "test-lfx-key"
|
||||
|
||||
|
||||
def spawn_uvicorn(app, host: str = "127.0.0.1", port: int = 0): # type: ignore[no-untyped-def]
|
||||
"""Real uvicorn server in a daemon thread. Returns (server, thread)."""
|
||||
import threading
|
||||
|
|
@ -91,7 +100,7 @@ def spawn_uvicorn(app, host: str = "127.0.0.1", port: int = 0): # type: ignore[
|
|||
return server, thread
|
||||
|
||||
|
||||
def plant_session_jwt(session, email: str, role: str = "admin", name: str = "Test") -> object:
|
||||
def plant_session_jwt(session, email: str, role: str = "admin", name: str = "Test") -> tuple:
|
||||
"""Idempotent test user + session JWT (shared with smoke pattern)."""
|
||||
from sqlmodel import select
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Model-level tests: defaults, constraints, JSON roundtrip, linkage."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import select
|
||||
|
|
@ -86,3 +88,48 @@ def test_run_and_task_linkage_with_tenant_default(session):
|
|||
tasks = session.exec(select(ApprovalTask).where(ApprovalTask.run_id == run.id)).all()
|
||||
assert len(tasks) == 1
|
||||
assert tasks[0].payload == {}
|
||||
|
||||
|
||||
def test_create_version_populates_input_schema(session):
|
||||
from app.flows import store
|
||||
from app.flows.schemas import FlowVersionCreate
|
||||
|
||||
d = FlowDefinition(name="cached")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
doc = {
|
||||
"name": "cached",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"data": {
|
||||
"type": "ChatInput",
|
||||
"node": {"display_name": "Chat", "template": {}},
|
||||
},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
},
|
||||
}
|
||||
version = store.create_version(session, d.id, FlowVersionCreate(langflow_json=doc))
|
||||
assert version.input_schema["fallback"] == "textarea"
|
||||
|
||||
|
||||
def test_input_schema_null_rejected(session):
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
d = FlowDefinition(name="nonnull")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
with pytest.raises(IntegrityError):
|
||||
session.execute(
|
||||
text(
|
||||
"INSERT INTO flow_versions (id, flow_id, version_no, langflow_json,"
|
||||
" author, input_schema) VALUES (:id, :flow_id, 1, :doc, :author, NULL)"
|
||||
),
|
||||
{"id": str(uuid.uuid4()), "flow_id": str(d.id), "doc": "{}", "author": "t"},
|
||||
)
|
||||
session.commit()
|
||||
session.rollback()
|
||||
|
|
|
|||
|
|
@ -136,73 +136,78 @@ async def test_sync_endpoint_returns_report(
|
|||
assert response.json()["skipped"] == ["simple_rule"]
|
||||
|
||||
|
||||
async def test_sync_endpoint_pulls_from_langflow_then_syncs(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
@pytest.mark.parametrize(
|
||||
("seed", "flow_name", "flow_id", "extra_flows", "extra_docs", "expected_file"),
|
||||
[
|
||||
(
|
||||
None,
|
||||
"Gehaltserhöhung",
|
||||
"22222222-3333-4444-8555-666666666666",
|
||||
[],
|
||||
{},
|
||||
"Gehaltserhöhung.json",
|
||||
),
|
||||
(
|
||||
# Seeded from simple_rule.json: updates in place instead of
|
||||
# duplicating as simple-rule.json (slug drift).
|
||||
"simple_rule.json",
|
||||
"simple_rule",
|
||||
"44444444-5555-4666-8777-888888888888",
|
||||
[],
|
||||
{},
|
||||
"simple_rule.json",
|
||||
),
|
||||
(
|
||||
None,
|
||||
"Gut",
|
||||
"33333333-4444-4555-8666-777777777777",
|
||||
[{"id": "kaputt-id", "name": "Kaputt"}],
|
||||
{},
|
||||
"Gut.json",
|
||||
),
|
||||
],
|
||||
ids=["pull-new", "pull-reuse", "pull-error-continues"],
|
||||
)
|
||||
async def test_sync_endpoint_pull_matrix(
|
||||
client,
|
||||
lfx_server,
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
login,
|
||||
override_langflow,
|
||||
seed,
|
||||
flow_name,
|
||||
flow_id,
|
||||
extra_flows,
|
||||
extra_docs,
|
||||
expected_file,
|
||||
):
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
langflow_id = "22222222-3333-4444-8555-666666666666"
|
||||
if seed is not None:
|
||||
_seed_dir(tmp_path, seed)
|
||||
override_langflow(
|
||||
flows=[{"id": langflow_id, "name": "Gehaltserhöhung"}],
|
||||
docs={langflow_id: langflow_export_doc("Gehaltserhöhung", langflow_id)},
|
||||
flows=[{"id": flow_id, "name": flow_name}, *extra_flows],
|
||||
docs={flow_id: langflow_export_doc(flow_name, flow_id), **extra_docs},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["pulled"] == ["Gehaltserhöhung"]
|
||||
assert body["added"] == ["Gehaltserhöhung"]
|
||||
assert body["errors"] == {}
|
||||
assert (tmp_path / "Gehaltserhöhung.json").is_file()
|
||||
assert body["pulled"] == [flow_name]
|
||||
assert body["added"] == [flow_name]
|
||||
assert (tmp_path / expected_file).is_file()
|
||||
if flow_name == "Gut":
|
||||
assert "kaputt-id" in body["errors"]
|
||||
else:
|
||||
assert body["errors"] == {}
|
||||
assert not (tmp_path / "simple-rule.json").exists()
|
||||
|
||||
# Second pass: same content pulls again but syncs nothing new.
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["pulled"] == ["Gehaltserhöhung"]
|
||||
assert body["skipped"] == ["Gehaltserhöhung"]
|
||||
|
||||
|
||||
async def test_sync_endpoint_pull_reuses_same_named_file(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
"""A pulled flow seeded from simple_rule.json updates it in place
|
||||
instead of duplicating it as simple-rule.json (slug drift)."""
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
_seed_dir(tmp_path) # fixture shape, name "simple_rule"
|
||||
langflow_id = "44444444-5555-4666-8777-888888888888"
|
||||
override_langflow(
|
||||
flows=[{"id": langflow_id, "name": "simple_rule"}],
|
||||
docs={langflow_id: langflow_export_doc("simple_rule", langflow_id)},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["pulled"] == ["simple_rule"]
|
||||
assert body["added"] == ["simple_rule"]
|
||||
assert body["errors"] == {}
|
||||
assert not (tmp_path / "simple-rule.json").exists()
|
||||
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.json()["skipped"] == ["simple_rule"]
|
||||
|
||||
|
||||
async def test_sync_endpoint_pull_error_continues(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
good_id = "33333333-4444-4555-8666-777777777777"
|
||||
override_langflow(
|
||||
flows=[{"id": good_id, "name": "Gut"}, {"id": "kaputt-id", "name": "Kaputt"}],
|
||||
docs={good_id: langflow_export_doc("Gut", good_id)},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["pulled"] == ["Gut"]
|
||||
assert body["added"] == ["Gut"]
|
||||
assert "kaputt-id" in body["errors"]
|
||||
assert body["pulled"] == [flow_name]
|
||||
assert body["skipped"] == [flow_name]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue