feat: Langflow authoring pipeline with tests

- LangflowClient (list/export, x-api-key auth, error mapping)
- import_from_langflow: export -> versioned definition -> LFX deploy
- POST /api/flows/import, GET /api/flows/langflow/available
- scripts/deploy_flow.py CLI
- 25 tests green (Langflow faked via dependency override + respx
  client tests; respx passthrough proven unreliable for LFX traffic)
This commit is contained in:
Jonas Miederer 2026-09-06 23:01:47 +00:00
parent 96c0da33c7
commit 1034206870
8 changed files with 451 additions and 0 deletions

View file

@ -0,0 +1,52 @@
"""LangflowClient HTTP mapping tests (Langflow mocked via respx, no LFX)."""
import httpx
import pytest
import respx
from app.flows.langflow_client import (
LangflowClient,
LangflowNotFound,
LangflowUnavailable,
)
BASE = "https://langflow.test"
def _client():
return LangflowClient(BASE, api_key="test-key")
@respx.mock
async def test_export_flow_sends_api_key_and_returns_doc():
doc = {"id": "abc", "name": "n", "data": {"nodes": [], "edges": []}}
route = respx.get(f"{BASE}/api/v1/flows/abc").mock(
return_value=httpx.Response(200, json=doc)
)
result = await _client().export_flow("abc")
assert result == doc
assert route.called
assert route.calls[0].request.headers["x-api-key"] == "test-key"
@respx.mock
async def test_export_flow_not_found():
respx.get(f"{BASE}/api/v1/flows/missing").mock(
return_value=httpx.Response(404, json={"detail": "nope"})
)
with pytest.raises(LangflowNotFound):
await _client().export_flow("missing")
@respx.mock
async def test_list_flows_unwraps_items():
respx.get(f"{BASE}/api/v1/flows/").mock(
return_value=httpx.Response(200, json={"items": [{"id": "1"}]})
)
assert await _client().list_flows() == [{"id": "1"}]
async def test_unreachable_raises_unavailable():
client = LangflowClient("https://langflow.dead", api_key="k")
with pytest.raises(LangflowUnavailable):
await client.export_flow("whatever")