"""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") # 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 import time import uvicorn config = uvicorn.Config(app, host=host, port=port, log_level="warning") server = uvicorn.Server(config) thread = threading.Thread(target=server.run, daemon=True) thread.start() deadline = time.time() + 30 while not server.started and time.time() < deadline: time.sleep(0.05) assert server.started, "uvicorn test server did not start" return server, thread def plant_session_jwt(session, email: str, role: str = "admin", name: str = "Test") -> object: """Idempotent test user + session JWT (shared with smoke pattern).""" from sqlmodel import select from app.auth import service from app.models import User user = session.exec(select(User).where(User.email == email)).first() if user is None: user = User(name=name, email=email, role=role) session.add(user) session.commit() session.refresh(user) return user, service.mint_session_token(user)