"""Tests for Package 3.1 SQLite infrastructure — prompts.db + history.db. Covers: connection factories, WAL mode, foreign keys, table creation, seed data, idempotency. Uses tmp_path for isolated test databases — no real filesystem pollution. """ import sqlite3 import pytest # ── Helpers ──────────────────────────────────────────────────────────────── DEFAULT_PROMPTS_DB = "/tmp/test_prompts.db" DEFAULT_HISTORY_DB = "/tmp/test_history.db" def _patch_settings(monkeypatch, prompts_path: str, history_path: str): """Patch Settings to use test-specific DB paths.""" monkeypatch.setenv("PROMPTS_DB_PATH", prompts_path) monkeypatch.setenv("HISTORY_DB_PATH", history_path) from app.core.config import get_settings get_settings.cache_clear() # ── Config Tests ─────────────────────────────────────────────────────────── def test_config_default_db_paths(monkeypatch): """New config fields should have sensible defaults.""" monkeypatch.delenv("PROMPTS_DB_PATH", raising=False) monkeypatch.delenv("HISTORY_DB_PATH", raising=False) from app.core.config import Settings settings = Settings() assert settings.prompts_db_path == "./data/prompts.db" assert settings.history_db_path == "./data/history.db" def test_config_db_paths_from_env(tmp_path, monkeypatch): """DB paths should be configurable via environment variables.""" prompts_path = str(tmp_path / "my_prompts.db") history_path = str(tmp_path / "my_history.db") monkeypatch.setenv("PROMPTS_DB_PATH", prompts_path) monkeypatch.setenv("HISTORY_DB_PATH", history_path) from app.core.config import Settings settings = Settings() assert settings.prompts_db_path == prompts_path assert settings.history_db_path == history_path # ── Connection Factory Tests ─────────────────────────────────────────────── def test_get_prompts_db_creates_file_and_dir(tmp_path, monkeypatch): """get_prompts_db() should create the DB file and any missing parent dirs.""" prompts_path = str(tmp_path / "subdir" / "prompts.db") history_path = str(tmp_path / "subdir" / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db conn = get_prompts_db() assert conn is not None import os assert os.path.isfile(prompts_path) conn.close() def test_get_history_db_creates_file_and_dir(tmp_path, monkeypatch): """get_history_db() should create the DB file and any missing parent dirs.""" prompts_path = str(tmp_path / "subdir" / "prompts.db") history_path = str(tmp_path / "subdir" / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_history_db conn = get_history_db() assert conn is not None import os assert os.path.isfile(history_path) conn.close() def test_wal_mode_enabled(tmp_path, monkeypatch): """WAL journal mode should be enabled for better concurrency.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db conn = get_prompts_db() result = conn.execute("PRAGMA journal_mode").fetchone() assert result[0].upper() == "WAL" conn.close() def test_foreign_keys_enabled(tmp_path, monkeypatch): """Foreign key enforcement should be enabled.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db conn = get_prompts_db() result = conn.execute("PRAGMA foreign_keys").fetchone() assert result[0] == 1 conn.close() def test_row_factory_is_row(tmp_path, monkeypatch): """Row factory should be sqlite3.Row for dict-like access.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db conn = get_prompts_db() assert conn.row_factory is sqlite3.Row conn.close() # ── Table Creation Tests ─────────────────────────────────────────────────── def test_init_prompts_db_creates_tables(tmp_path, monkeypatch): """init_prompts_db() should create system_prompt_profiles + system_prompts tables.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db conn = get_prompts_db() init_prompts_db(conn) tables = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ).fetchall() table_names = {t["name"] for t in tables} assert "system_prompt_profiles" in table_names assert "system_prompts" in table_names conn.close() def test_init_prompts_db_idempotent(tmp_path, monkeypatch): """Calling init_prompts_db() twice should not raise errors.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db conn = get_prompts_db() init_prompts_db(conn) init_prompts_db(conn) # second call — must not raise conn.close() def test_init_history_db_creates_table_and_index(tmp_path, monkeypatch): """init_history_db() should create query_history table + created_at index.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_history_db, init_history_db conn = get_history_db() init_history_db(conn) tables = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ).fetchall() table_names = {t["name"] for t in tables} assert "query_history" in table_names indexes = conn.execute( "SELECT name FROM sqlite_master WHERE type='index' ORDER BY name" ).fetchall() index_names = {i["name"] for i in indexes} assert "idx_query_history_created_at" in index_names conn.close() def test_init_history_db_idempotent(tmp_path, monkeypatch): """Calling init_history_db() twice should not raise errors.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_history_db, init_history_db conn = get_history_db() init_history_db(conn) init_history_db(conn) # second call — must not raise conn.close() # ── Seed Data Tests ──────────────────────────────────────────────────────── def test_seed_default_profiles_creates_three_profiles(tmp_path, monkeypatch): """seed_default_profiles() should insert exactly 3 profiles: A, B, C.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) rows = conn.execute( "SELECT name, is_active FROM system_prompt_profiles ORDER BY id" ).fetchall() assert len(rows) == 3 names = [r["name"] for r in rows] assert names == ["A", "B", "C"] conn.close() def test_seed_default_profiles_A_is_active(tmp_path, monkeypatch): """Profile A should be active (is_active=1), B and C inactive (is_active=0).""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) profile_a = conn.execute( "SELECT name, is_active FROM system_prompt_profiles WHERE name='A'" ).fetchone() profile_b = conn.execute( "SELECT name, is_active FROM system_prompt_profiles WHERE name='B'" ).fetchone() profile_c = conn.execute( "SELECT name, is_active FROM system_prompt_profiles WHERE name='C'" ).fetchone() assert profile_a["is_active"] == 1 assert profile_b["is_active"] == 0 assert profile_c["is_active"] == 0 conn.close() def test_seed_default_profiles_creates_nine_prompts(tmp_path, monkeypatch): """seed_default_profiles() should insert 9 prompt rows (3 profiles × 3 steps).""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) rows = conn.execute( """SELECT sp.step_name, spp.name AS profile_name FROM system_prompts sp JOIN system_prompt_profiles spp ON sp.profile_id = spp.id ORDER BY spp.name, sp.step_name""" ).fetchall() assert len(rows) == 9 # Each profile should have all 3 steps for profile in ("A", "B", "C"): profile_rows = [r for r in rows if r["profile_name"] == profile] steps = {r["step_name"] for r in profile_rows} assert steps == {"decompose", "filter", "generate"} conn.close() def test_seed_default_profiles_contains_expected_templates(tmp_path, monkeypatch): """Templates should contain expected placeholders for each step.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) # Decompose must have {question} decompose_row = conn.execute( """SELECT sp.prompt_template FROM system_prompts sp JOIN system_prompt_profiles spp ON sp.profile_id = spp.id WHERE spp.name='A' AND sp.step_name='decompose'""" ).fetchone() assert decompose_row is not None assert "{question}" in decompose_row["prompt_template"] # Filter must have {question} and {chunks} filter_row = conn.execute( """SELECT sp.prompt_template FROM system_prompts sp JOIN system_prompt_profiles spp ON sp.profile_id = spp.id WHERE spp.name='A' AND sp.step_name='filter'""" ).fetchone() assert filter_row is not None assert "{question}" in filter_row["prompt_template"] assert "{chunks}" in filter_row["prompt_template"] # Generate must have {context_sections} generate_row = conn.execute( """SELECT sp.prompt_template FROM system_prompts sp JOIN system_prompt_profiles spp ON sp.profile_id = spp.id WHERE spp.name='A' AND sp.step_name='generate'""" ).fetchone() assert generate_row is not None assert "{context_sections}" in generate_row["prompt_template"] conn.close() def test_seed_default_profiles_idempotent(tmp_path, monkeypatch): """Calling seed_default_profiles() twice should not duplicate rows.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) seed_default_profiles(conn) profile_count = conn.execute( "SELECT COUNT(*) FROM system_prompt_profiles" ).fetchone()[0] prompt_count = conn.execute( "SELECT COUNT(*) FROM system_prompts" ).fetchone()[0] assert profile_count == 3 assert prompt_count == 9 conn.close() def test_all_three_profiles_have_identical_seed_templates(tmp_path, monkeypatch): """All 3 profiles should start with identical seed templates.""" prompts_path = str(tmp_path / "prompts.db") history_path = str(tmp_path / "history.db") _patch_settings(monkeypatch, prompts_path, history_path) from app.core.sqlite_db import get_prompts_db, init_prompts_db, seed_default_profiles conn = get_prompts_db() init_prompts_db(conn) seed_default_profiles(conn) for step in ("decompose", "filter", "generate"): rows = conn.execute( """SELECT spp.name, sp.prompt_template FROM system_prompts sp JOIN system_prompt_profiles spp ON sp.profile_id = spp.id WHERE sp.step_name = ? ORDER BY spp.name""", (step,), ).fetchall() templates = [r["prompt_template"] for r in rows] # All 3 must match assert templates[0] == templates[1] == templates[2], ( f"Step '{step}' templates differ across profiles: {templates}" ) conn.close()