feat: manual full-pass sync Langflow -> files -> DB+LFX with admin guide
This commit is contained in:
parent
acea6ae624
commit
1326cece42
11 changed files with 284 additions and 41 deletions
|
|
@ -218,22 +218,36 @@ def override_langflow():
|
|||
|
||||
Usage: override_langflow(export_doc) -- the fake export_flow() returns the
|
||||
doc, or raises LangflowNotFound/Unavailable for status=404/0.
|
||||
Pass flows=[...] + docs={id: doc} for multi-flow pull tests, or
|
||||
list_status=0/500 to fail the listing. Default list is empty so
|
||||
file-only sync tests stay hermetic.
|
||||
"""
|
||||
from app.flows.api import get_langflow
|
||||
from app.flows.langflow_client import LangflowNotFound, LangflowUnavailable
|
||||
from app.flows.langflow_client import LangflowError, LangflowNotFound, LangflowUnavailable
|
||||
from app.main import app
|
||||
|
||||
def _override(export_doc=None, status=200):
|
||||
def _override(export_doc=None, status=200, flows=None, docs=None, list_status=200):
|
||||
if flows is None:
|
||||
flows = [{"id": "fake-1", "name": "fake"}] if export_doc is not None else []
|
||||
|
||||
class FakeLangflow:
|
||||
async def export_flow(self, flow_id):
|
||||
if status == 404:
|
||||
raise LangflowNotFound(f"flow {flow_id} not found")
|
||||
if status == 0:
|
||||
raise LangflowUnavailable("Langflow unreachable")
|
||||
if docs is not None:
|
||||
if flow_id in docs:
|
||||
return docs[flow_id]
|
||||
raise LangflowNotFound(f"flow {flow_id} not found")
|
||||
return export_doc
|
||||
|
||||
async def list_flows(self):
|
||||
return [{"id": "fake-1", "name": "fake"}]
|
||||
if list_status == 0:
|
||||
raise LangflowUnavailable("Langflow unreachable")
|
||||
if list_status == 500:
|
||||
raise LangflowError("Langflow error 500")
|
||||
return flows
|
||||
|
||||
app.dependency_overrides[get_langflow] = lambda: FakeLangflow()
|
||||
|
||||
|
|
|
|||
|
|
@ -118,20 +118,102 @@ async def test_sync_rename_updates_definition_name(session, lfx_server, tmp_path
|
|||
assert definition.name == "umbenannt"
|
||||
|
||||
|
||||
async def test_sync_endpoint_returns_report(client, lfx_server, tmp_path, monkeypatch, login):
|
||||
async def test_sync_endpoint_returns_report(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
await login()
|
||||
override_langflow() # empty Langflow: file-only pass
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
_seed_dir(tmp_path)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["added"] == ["simple_rule"]
|
||||
assert body["pulled"] == []
|
||||
assert body["errors"] == {}
|
||||
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.json()["skipped"] == ["simple_rule"]
|
||||
|
||||
|
||||
def _langflow_doc(name: str, langflow_id: str) -> dict:
|
||||
doc = load_fixture("simple_rule.json")
|
||||
return {
|
||||
"id": langflow_id,
|
||||
"name": name,
|
||||
"description": "aus Langflow",
|
||||
"endpoint_name": None,
|
||||
"data": doc["data"],
|
||||
"tags": [],
|
||||
"webhook": False,
|
||||
"is_component": False,
|
||||
}
|
||||
|
||||
|
||||
async def test_sync_endpoint_pulls_from_langflow_then_syncs(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
langflow_id = "22222222-3333-4444-8555-666666666666"
|
||||
override_langflow(
|
||||
flows=[{"id": langflow_id, "name": "Gehaltserhöhung"}],
|
||||
docs={langflow_id: _langflow_doc("Gehaltserhöhung", langflow_id)},
|
||||
)
|
||||
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()
|
||||
|
||||
# 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_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_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"]
|
||||
|
||||
|
||||
async def test_sync_endpoint_langflow_unreachable(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
override_langflow(flows=[], list_status=0)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 503
|
||||
|
||||
|
||||
async def test_sync_endpoint_langflow_error(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
|
||||
override_langflow(flows=[], list_status=500)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 502
|
||||
|
||||
|
||||
async def test_watch_forever_triggers_on_file_change(tmp_path):
|
||||
import contextlib
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue