"""Real session router + disposable SQLite stores for the JS bridge contract. Run by sessions-api.test.mjs. Only service dependencies are replaced; query validation, route handling, SQL filters and pagination are production code. """ import os from pathlib import Path import socket import sys import tempfile import threading import time sys.path.insert(0, str(Path(__file__).resolve().parents[3])) with tempfile.TemporaryDirectory(prefix="aiturk-chrome-api-") as directory: os.environ["HERMES_HOME"] = directory from fastapi import FastAPI, HTTPException, WebSocket from fastapi.responses import JSONResponse import uvicorn from hermes_state import SessionDB from hermes_cli.web_routers import sessions paths = {name: Path(directory) / f"{name}.db" for name in ("default", "small", "empty")} for profile, count in (("default", 230), ("small", 3), ("empty", 0)): db = SessionDB(db_path=paths[profile]) for index in range(count): sid = f"{profile}-desktop-{index:03d}" db.create_session(sid, source="desktop") db.set_session_title(sid, sid) db.append_message(sid, role="user", content="Fixture context") if count: for index in range(240): sid = f"{profile}-cron-{index:03d}" db.create_session(sid, source="cron") db.set_session_title(sid, sid) db.append_message(sid, role="user", content="Not a desktop conversation") db.create_session(f"{profile}-blank", source="desktop") db.create_session(f"{profile}-archived", source="desktop") db.append_message(f"{profile}-archived", role="user", content="Archived") db._conn.execute("UPDATE sessions SET archived=1 WHERE id=?", (f"{profile}-archived",)) db._conn.commit() db.close() def profile_home(profile): if profile not in paths: raise HTTPException(status_code=404) return profile, Path(directory) sessions._cron_profile_home = profile_home sessions._cron_default_profile = lambda: "default" sessions._maybe_auto_archive_for_profile = lambda profile: None sessions._open_session_db_for_profile = lambda profile, read_only=True: SessionDB( db_path=paths[profile or "default"], read_only=read_only ) # The fixture stores no system/model secrets; list projection is unrelated # to this contract and must not import the full web-server startup path. sessions._strip_session_list_rows = lambda rows: None app = FastAPI() app.include_router(sessions.list_router) @app.middleware("http") async def auth(request, call_next): if request.headers.get("X-Hermes-Session-Token") != "fixture-only": return JSONResponse({}, status_code=401) return await call_next(request) @app.websocket("/api/ws") async def ws(websocket: WebSocket): await websocket.accept() try: while True: await websocket.receive_text() except Exception: pass sock = socket.socket() sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] server = uvicorn.Server(uvicorn.Config(app, log_level="critical")) thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True) thread.start() deadline = time.monotonic() + 20 while not server.started: if not thread.is_alive() or time.monotonic() > deadline: raise RuntimeError("Fixture failed to start") time.sleep(0.01) print(f"READY {port}", flush=True) # EOF shuts down cleanly, including Windows venv launcher/worker pairs. sys.stdin.read() server.should_exit = True thread.join(timeout=15) sock.close()