refactor: tighten backend, flows, frontend, tests; push flows to Langflow
This commit is contained in:
parent
6688fb08c6
commit
06ab25fb47
31 changed files with 783 additions and 358 deletions
68
backend/tests/_harness.py
Normal file
68
backend/tests/_harness.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def free_port() -> int:
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
return port
|
||||
|
||||
|
||||
def wait_healthy(url: str, deadline_s: float = 240.0) -> None:
|
||||
deadline = time.time() + deadline_s
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url + "/health", timeout=3) as response:
|
||||
if response.status == 200:
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(1)
|
||||
raise RuntimeError(f"server did not become healthy: {url}")
|
||||
|
||||
|
||||
def spawn_lfx(flow_dir: Path, port: int) -> 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"}
|
||||
return subprocess.Popen(
|
||||
[
|
||||
lfx_bin,
|
||||
"serve",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
str(port),
|
||||
"--flow-dir",
|
||||
str(flow_dir),
|
||||
str(flow_dir),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
def migrate_throwaway(backend_dir: Path) -> 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"))
|
||||
alembic_command.upgrade(cfg, "head")
|
||||
|
|
@ -278,3 +278,89 @@ def live_server():
|
|||
yield f"http://127.0.0.1:{port}"
|
||||
server.should_exit = True
|
||||
thread.join(timeout=15)
|
||||
|
||||
|
||||
# --- shared declutter helpers (single place, was copy-pasted across files) ---
|
||||
|
||||
FIXTURES_DIR = Path(__file__).parent / "fixtures" / "flows"
|
||||
|
||||
|
||||
def load_flow_fixture(name: str) -> dict:
|
||||
"""Load a fixture flow JSON (shared; was load_fixture() x3)."""
|
||||
import json
|
||||
|
||||
return json.loads((FIXTURES_DIR / name).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def langflow_export_doc(name: str = "fake", flow_id: str = "fake-1") -> dict:
|
||||
"""Minimal Langflow export envelope (merges _export_doc/_langflow_doc)."""
|
||||
return {
|
||||
"id": flow_id,
|
||||
"name": name,
|
||||
"description": "",
|
||||
"data": {"nodes": [], "edges": []},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def anon_client():
|
||||
"""Unauthenticated ASGI client (was 3x copy-pasted ASGITransport blocks)."""
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
async def _client():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
return _client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def flow_helpers(client, login):
|
||||
"""API flow factory: create flow/version/deploy/deployed_flow in one place.
|
||||
|
||||
Collapses _create_flow/_create_version/_deploy x4 files (~60 lines).
|
||||
"""
|
||||
|
||||
async def _create_flow(name: str = "regeltest") -> dict:
|
||||
response = await client.post("/api/flows", json={"name": name, "description": "t"})
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
async def _create_version(flow_id: str, fixture: str = "simple_rule.json") -> dict:
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow_id}/versions",
|
||||
json={"langflow_json": load_flow_fixture(fixture), "author": "test"},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
async def _deploy(flow_id: str, version_no: int = 1) -> dict:
|
||||
response = await client.post(f"/api/flows/{flow_id}/versions/{version_no}/deploy")
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()
|
||||
|
||||
async def _deployed_flow(name: str = "regeltest", fixture: str = "simple_rule.json") -> tuple:
|
||||
await login()
|
||||
flow = await _create_flow(name)
|
||||
version = await _create_version(flow["id"], fixture)
|
||||
deployed = await _deploy(flow["id"], version["version_no"])
|
||||
return flow, deployed
|
||||
|
||||
return {
|
||||
"create_flow": _create_flow,
|
||||
"create_version": _create_version,
|
||||
"deploy": _deploy,
|
||||
"deployed_flow": _deployed_flow,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lfx_client():
|
||||
"""LFX client pointed at the real test server (was _lfx() x2)."""
|
||||
from app.flows.lfx_client import LfxClient
|
||||
from app.settings import settings
|
||||
|
||||
return LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
||||
|
|
|
|||
|
|
@ -2,18 +2,26 @@
|
|||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app.flows import export
|
||||
|
||||
|
||||
def test_filename_stem_keeps_original_names():
|
||||
assert export.filename_stem("simple_rule") == "simple_rule"
|
||||
assert export.filename_stem("test_flow") == "test_flow"
|
||||
assert export.filename_stem("Gehaltserhöhung") == "Gehaltserhöhung"
|
||||
assert export.filename_stem("Dachsanierung 200k") == "Dachsanierung 200k"
|
||||
assert export.filename_stem(" A/B\\C ") == "A-B-C"
|
||||
assert export.filename_stem('a:b*c?d"e<f>g|h') == "a-b-c-d-e-f-g-h"
|
||||
assert export.filename_stem("trailing...") == "trailing"
|
||||
assert export.filename_stem("") == "flow"
|
||||
@pytest.mark.parametrize(
|
||||
("name", "expected"),
|
||||
[
|
||||
("simple_rule", "simple_rule"),
|
||||
("test_flow", "test_flow"),
|
||||
("Gehaltserhöhung", "Gehaltserhöhung"),
|
||||
("Dachsanierung 200k", "Dachsanierung 200k"),
|
||||
(" A/B\\C ", "A-B-C"),
|
||||
('a:b*c?d"e<f>g|h', "a-b-c-d-e-f-g-h"),
|
||||
("trailing...", "trailing"),
|
||||
("", "flow"),
|
||||
],
|
||||
)
|
||||
def test_filename_stem_keeps_original_names(name, expected):
|
||||
assert export.filename_stem(name) == expected
|
||||
|
||||
|
||||
def test_canonical_bytes_stable_under_key_reorder():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue