"""Shared fixtures for unit + integration tests.""" from __future__ import annotations import json import os from collections.abc import Iterator import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session # In-process integration/E2E tests drive the app with *mock* embeddings # (bag-of-words, cosine ~0.1–0.8), not the live aipi model — so the honesty # gate is calibrated to the mock's distribution, mirroring tests/e2e/ # conftest.py. Must be set before ``app.main`` (below) caches settings. # The production default stays 0.62 (app/config.py, A8 revised). os.environ.setdefault("BOR_RELEVANCE_THRESHOLD", "0.30") # A8 revised 2026-09-14: lexical_support_floor must be <= relevance_threshold. os.environ.setdefault("BOR_LEXICAL_SUPPORT_FLOOR", "0.15") # Phase 113: the source usefulness bar (citation slot) is calibrated to # the mock's compressed distribution, like the threshold above — the # weakest GROUNDED fixture cosine in the in-process suites (the # kafkabridge question's static-dns.json, ~0.055) must stay citable so # the pre-phase citation pins hold, while the bar is ON so endpoint # tests exercise the tier (tests that pin bar-specific behavior set a # higher floor explicitly, the BOR_LEXICAL_SUPPORT_FLOOR pattern). # Production default stays 0.35 (app/config.py, LOCKED A2). os.environ.setdefault("BOR_SOURCE_USEFULNESS_FLOOR", "0.02") # Phase 16: single-admin auth is fail-loud — create_app() refuses to boot # without both vars, and app.main (imported below) builds the app at # import time. Set known test values first, same pattern as the threshold. ADMIN_PASSWORD = "test-admin-password" SESSION_SECRET = "test-session-secret-0123456789abcdef0123456789abcdef" os.environ.setdefault("BOR_ADMIN_PASSWORD", ADMIN_PASSWORD) os.environ.setdefault("BOR_SESSION_SECRET", SESSION_SECRET) # Phase 61 (defect fix): the app under test must see the code DEFAULTS, # not an operator's local (gitignored) ``.env`` — ``Settings`` loads # ``env_file=".env"`` from the repo root, and a machine-specific corpus # (e.g. ``BOR_SUGGESTIONS``, ``BOR_DOCS_REPO``) leaked into the tests # broke the default-metadata pins. pydantic-settings ranks process env # vars ABOVE the ``.env`` file, so force the defaults explicitly here, # before ``app.main`` (below) caches settings. The suggestions default # is derived from the class field so this can never drift from # ``app/config.py``; docs-push stays inert (empty repo). from app.config import Settings as _Settings # noqa: E402 os.environ["BOR_DOCS_REPO"] = "" os.environ["BOR_SUGGESTIONS"] = json.dumps(_Settings.model_fields["suggestions"].default) # Phase 62: the same leak class for the new UI customization settings — # an operator's local ``.env`` may legitimately carry # ``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT``, and the # default-metadata pins must see the code defaults (derived from # the class fields, same pattern as the suggestions line above). # (Phase 91, task 03: the retired CSS-file theme env var no longer # exists — nothing to pin.) os.environ["BOR_INPUT_PLACEHOLDER"] = _Settings.model_fields["input_placeholder"].default os.environ["BOR_FOOTER_TEXT"] = _Settings.model_fields["footer_text"].default # Phase 106 (task 07): the same leak class for the recency-boost # settings — an operator's local ``.env`` may legitimately carry # ``BOR_RECENCY_BOOST`` / ``BOR_RECENCY_HALF_LIFE_DAYS`` re-tuned live, # and the fine-line integration battery pins the CODE DEFAULTS # (derived from the class fields, same pattern as the lines above). os.environ["BOR_RECENCY_BOOST"] = str(_Settings.model_fields["recency_boost"].default) os.environ["BOR_RECENCY_HALF_LIFE_DAYS"] = str( _Settings.model_fields["recency_half_life_days"].default ) # Phase 122 (task 01): the same leak class for the image-document # toggle — an operator's local ``.env`` may legitimately carry # ``BOR_IMAGES``/``BOR_IMAGE_EXTENSIONS``/``BOR_IMAGE_DIR``, and the # "off by default" pins (the ``/api/config`` ``images`` flag, the # walk's images-off behavior) must see the code defaults. os.environ["BOR_IMAGES"] = str(_Settings.model_fields["images"].default) os.environ["BOR_IMAGE_EXTENSIONS"] = str(_Settings.model_fields["image_extensions"].default) os.environ["BOR_IMAGE_DIR"] = str(_Settings.model_fields["image_dir"].default) from app.db import SessionLocal, db_available # noqa: E402 from app.main import app as fastapi_app # noqa: E402 @pytest.fixture() def client() -> TestClient: return TestClient(fastapi_app) @pytest.fixture() def admin_client(client: TestClient) -> TestClient: """A client signed in as the single admin (phase 16). TestClient keeps its cookie jar across requests, so one login covers every subsequent request of the test. Use it for the admin-only surface (``GET /api/docs``, ``/api/steering``). """ r = client.post("/api/login", json={"password": ADMIN_PASSWORD}) assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}" return client @pytest.fixture() def db() -> Iterator[Session]: """Real Postgres session (``podman compose up -d db``). Skips with clear instructions when the database is not running, so the suite degrades gracefully on a machine without the stack started. """ if not db_available(): pytest.skip("Postgres not reachable — run `podman compose up -d db` first") session = SessionLocal() try: yield session finally: session.close()