"""Tests for Package 3.2 prompts router — HTTP endpoint integration tests. Uses real sqlite3 with tmp_path. TestClient hits a minimal FastAPI app wired with the prompts router. No mocks on the DB layer. """ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.core.sqlite_db import init_prompts_db, seed_default_profiles, _get_db from app.routers.prompts import router # ── Fixture ──────────────────────────────────────────────────────────────── @pytest.fixture def client(tmp_path, monkeypatch): prompts_path = str(tmp_path / "prompts.db") monkeypatch.setenv("PROMPTS_DB_PATH", prompts_path) monkeypatch.setenv("HISTORY_DB_PATH", str(tmp_path / "history.db")) from app.core.config import get_settings get_settings.cache_clear() from app.core.dependencies import get_settings_cached get_settings_cached.cache_clear() conn = _get_db(prompts_path) init_prompts_db(conn) seed_default_profiles(conn) conn.close() test_app = FastAPI() test_app.include_router(router) yield TestClient(test_app) get_settings_cached.cache_clear() get_settings.cache_clear() # ── GET /profiles ────────────────────────────────────────────────────────── def test_get_profiles_returns_200_with_three_items(client): resp = client.get("/api/v1/prompts/profiles") assert resp.status_code == 200 data = resp.json() assert len(data["profiles"]) == 3 names = [p["name"] for p in data["profiles"]] assert names == ["A", "B", "C"] active_map = {p["name"]: p["is_active"] for p in data["profiles"]} assert active_map["A"] is True assert active_map["B"] is False assert active_map["C"] is False # ── GET /profiles/{name} ────────────────────────────────────────────────── def test_get_profile_prompts_a_returns_200(client): resp = client.get("/api/v1/prompts/profiles/A") assert resp.status_code == 200 data = resp.json() assert data["profile_name"] == "A" assert set(data["prompts"].keys()) == { "decompose", "filter", "generate", "generate_per_subq", "filter_intro", "filter_section", "filter_outro", } def test_get_profile_prompts_invalid_returns_400(client): resp = client.get("/api/v1/prompts/profiles/D") assert resp.status_code == 400 # ── PUT /profiles/{name}/activate ───────────────────────────────────────── def test_activate_profile_b_then_get_confirms(client): resp = client.put("/api/v1/prompts/profiles/B/activate") assert resp.status_code == 200 assert resp.json()["active_profile"] == "B" resp = client.get("/api/v1/prompts/profiles") active_map = {p["name"]: p["is_active"] for p in resp.json()["profiles"]} assert active_map["B"] is True assert active_map["A"] is False assert active_map["C"] is False # ── PUT /profiles/{name}/{step} ─────────────────────────────────────────── def test_update_prompt_returns_200_and_persists(client): new_template = "Updated decompose for {question}" resp = client.put( "/api/v1/prompts/profiles/A/decompose", json={"template": new_template}, ) assert resp.status_code == 200 assert resp.json()["step"] == "decompose" resp = client.get("/api/v1/prompts/profiles/A") assert resp.json()["prompts"]["decompose"] == new_template # ── PUT /profiles/{name}/all ────────────────────────────────────────────── def test_update_all_prompts_batch_returns_200_and_persists(client): new_prompts = { "decompose": "Batch decompose", "filter": "Batch filter", "generate": "Batch generate", } resp = client.put( "/api/v1/prompts/profiles/A/all", json={"prompts": new_prompts}, ) assert resp.status_code == 200 assert resp.json()["profile"] == "A" resp = client.get("/api/v1/prompts/profiles/A") prompts = resp.json()["prompts"] assert prompts["decompose"] == "Batch decompose" assert prompts["filter"] == "Batch filter" assert prompts["generate"] == "Batch generate" # ── PUT /profiles/{name}/reset ──────────────────────────────────────────── def test_reset_single_step_returns_200(client): from app.core.sqlite_db import _SEED_TEMPLATES client.put( "/api/v1/prompts/profiles/A/decompose", json={"template": "MODIFIED"}, ) resp = client.put( "/api/v1/prompts/profiles/A/reset", json={"step": "decompose"}, ) assert resp.status_code == 200 assert resp.json()["reset_step"] == "decompose" resp = client.get("/api/v1/prompts/profiles/A") assert resp.json()["prompts"]["decompose"] == _SEED_TEMPLATES["decompose"] def test_reset_all_steps_returns_200(client): from app.core.sqlite_db import _SEED_TEMPLATES client.put("/api/v1/prompts/profiles/A/all", json={"prompts": { "decompose": "MODIFIED decompose", "filter": "MODIFIED filter", "generate": "MODIFIED generate", }}) resp = client.put( "/api/v1/prompts/profiles/A/reset", json={"step": None}, ) assert resp.status_code == 200 assert resp.json()["reset_step"] == "all" resp = client.get("/api/v1/prompts/profiles/A") prompts = resp.json()["prompts"] assert prompts["decompose"] == _SEED_TEMPLATES["decompose"] assert prompts["filter"] == _SEED_TEMPLATES["filter"] assert prompts["generate"] == _SEED_TEMPLATES["generate"] def test_reset_with_no_body_resets_all(client): from app.core.sqlite_db import _SEED_TEMPLATES client.put("/api/v1/prompts/profiles/A/all", json={"prompts": { "decompose": "MODIFIED", "filter": "MODIFIED", "generate": "MODIFIED", }}) resp = client.put("/api/v1/prompts/profiles/A/reset") assert resp.status_code == 200 assert resp.json()["reset_step"] == "all" resp = client.get("/api/v1/prompts/profiles/A") prompts = resp.json()["prompts"] assert prompts["decompose"] == _SEED_TEMPLATES["decompose"] assert prompts["filter"] == _SEED_TEMPLATES["filter"] assert prompts["generate"] == _SEED_TEMPLATES["generate"] # ── Validation: invalid name and step ────────────────────────────────────── def test_invalid_profile_name_returns_400(client): resp = client.get("/api/v1/prompts/profiles/D") assert resp.status_code == 400 resp = client.put("/api/v1/prompts/profiles/D/activate") assert resp.status_code == 400 def test_invalid_step_returns_400(client): resp = client.put( "/api/v1/prompts/profiles/A/nonexistent", json={"template": "test"}, ) assert resp.status_code == 400