From 9833204e0751e89e7e29c2d2b0dc7da3fb5755c5 Mon Sep 17 00:00:00 2001 From: aircode610 Date: Thu, 10 Sep 2026 13:22:30 +0200 Subject: [PATCH] klar-t1: review-and-adopt uncommitted diff (or redo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed the pre-existing uncommitted 3-file diff against PLAN.md/CRITERIA.md and adopted it as-is after verification: - backend/app/config.py: adds module-level VERSION = "0.1.0" constant (avoids circular import between main.py and the new health router) - backend/app/main.py: imports VERSION from config, passes it to FastAPI(version=VERSION), registers health.router — existing root GET /health handler is byte-for-byte untouched - backend/app/routers/health.py (new): GET /api/health returns {status, version, db}, with db derived from a real SELECT 1 against the existing SQLModel engine (no new aiosqlite dependency), wrapped in try/except -> "ok"/"fail" - backend/tests/test_health.py (new): covers the 200/shape happy path and the db:"fail" path via a monkeypatched Session Verified: full backend pytest suite (23 tests) passes, gate command (python3 -m compileall -q ai backend) passes, and a live uvicorn smoke test confirms GET /api/health -> 200 {status:ok,version:0.1.0,db:ok} while GET /health (root) is unchanged. --- backend/app/config.py | 3 ++ backend/app/main.py | 7 +++-- backend/app/routers/health.py | 21 ++++++++++++++ backend/tests/test_health.py | 53 +++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 backend/app/routers/health.py create mode 100644 backend/tests/test_health.py diff --git a/backend/app/config.py b/backend/app/config.py index 15037ca..f854bed 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -2,6 +2,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict +# Static application version string (not environment-configurable). +VERSION = "0.1.0" + class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") diff --git a/backend/app/main.py b/backend/app/main.py index 04f428b..6e90788 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,7 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.auth import router as auth_router # APIRouter (re-exported) -from app.config import settings +from app.config import VERSION, settings from app.database import init_db from app.errors import ( KlarHTTPException, @@ -17,7 +17,7 @@ validation_exception_handler, ) from app.rag.store import init_chroma -from app.routers import actions, deadlines, letters, public, rag # router modules +from app.routers import actions, deadlines, health, letters, public, rag # router modules def _validate_cookie_pairing() -> None: @@ -112,7 +112,7 @@ async def lifespan(_: FastAPI): app = FastAPI( title="Klar API", description="German bureaucratic mail → structured obligations + deadlines.", - version="0.1.0", + version=VERSION, lifespan=lifespan, ) @@ -137,6 +137,7 @@ async def lifespan(_: FastAPI): app.include_router(actions.router) app.include_router(deadlines.router) app.include_router(rag.router) +app.include_router(health.router) # Frontend-facing root surface (matches docs/06-frontend-integration-contract.md) # Mounts the same auth router at /auth so the bootstrap flow works without /api, diff --git a/backend/app/routers/health.py b/backend/app/routers/health.py new file mode 100644 index 0000000..7274829 --- /dev/null +++ b/backend/app/routers/health.py @@ -0,0 +1,21 @@ +"""GET /api/health — liveness + DB connectivity check.""" + +from fastapi import APIRouter +from sqlalchemy import text +from sqlmodel import Session + +from app.config import VERSION +from app.database import engine + +router = APIRouter(prefix="/api", tags=["health"]) + + +@router.get("/health") +def health() -> dict[str, object]: + db_status = "ok" + try: + with Session(engine) as session: + session.exec(text("SELECT 1")) + except Exception: + db_status = "fail" + return {"status": "ok", "version": VERSION, "db": db_status} diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..966cf98 --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,53 @@ +"""Tests for GET /api/health — success and DB-failure paths. + +Deliberately mounts only the health router on a throwaway FastAPI app +(instead of the full `app.main.app`) so this test doesn't pull in ChromaDB / +LangChain import-time side effects that belong to unrelated routers. +""" + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.routers import health as health_module + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(health_module.router) + return TestClient(app) + + +def test_get_api_health_returns_200_with_expected_shape(): + resp = _client().get("/api/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "ok" + assert body["version"] == health_module.VERSION + assert body["db"] == "ok" + + +def test_get_api_health_reports_db_fail_on_exception(monkeypatch): + class _BoomSession: + def __init__(self, *_a, **_k): + pass + + def __enter__(self): + return self + + def __exit__(self, *_a): + return False + + def exec(self, *_a, **_k): + raise RuntimeError("database is unreachable") + + monkeypatch.setattr(health_module, "Session", _BoomSession) + + resp = _client().get("/api/health") + + # Per CRITERIA/PLAN: the endpoint itself still returns 200; only the + # `db` field flips to "fail" — it does not surface as an HTTP error. + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "ok" + assert body["db"] == "fail"