"""Integration tests for theta persistence (SLICE-04, TASK-04-04).""" from __future__ import annotations import asyncio import json import sqlite3 from pathlib import Path import pytest from db.migrate import apply_migrations from db.store import PraxisStore, HARDCODED_LEARNER_ID @pytest.fixture def tmp_db(tmp_path: Path) -> Path: return tmp_path / "test_praxis.db" def _await(coro): return asyncio.run(coro) def test_migrations_apply_0003(tmp_db: Path): applied = apply_migrations(tmp_db) assert "0003_mastery" in applied conn = sqlite3.connect(str(tmp_db)) tables = { r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall() } conn.close() assert {"learner_ability", "mastery_progress"} <= tables def test_migration_idempotent_run_twice(tmp_db: Path): apply_migrations(tmp_db) apply_migrations(tmp_db) conn = sqlite3.connect(str(tmp_db)) tables = { r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall() } conn.close() assert {"learner_ability", "mastery_progress"} <= tables def test_get_ability_returns_none_for_new_learner(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() return await store.get_ability(HARDCODED_LEARNER_ID, "customer_service") assert _await(_run()) is None def test_upsert_ability_round_trip(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_ability(HARDCODED_LEARNER_ID, "customer_service", 0.5, 0.8, 7) return await store.get_ability(HARDCODED_LEARNER_ID, "customer_service") row = _await(_run()) assert row is not None assert row["learner_id"] == HARDCODED_LEARNER_ID assert row["path"] == "customer_service" assert row["theta"] == pytest.approx(0.5) assert row["sigma_sq"] == pytest.approx(0.8) assert row["observations"] == 7 assert row["updated_at"] is not None def test_upsert_ability_updates_existing(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_ability(HARDCODED_LEARNER_ID, "customer_service", 0.0, 1.0, 1) await store.upsert_ability(HARDCODED_LEARNER_ID, "customer_service", 1.2, 0.4, 8) return await store.get_ability(HARDCODED_LEARNER_ID, "customer_service") row = _await(_run()) assert row is not None assert row["theta"] == pytest.approx(1.2) assert row["sigma_sq"] == pytest.approx(0.4) assert row["observations"] == 8 def test_default_values_for_new_learner_via_sql(tmp_db: Path): apply_migrations(tmp_db) conn = sqlite3.connect(str(tmp_db)) conn.execute( "INSERT INTO learner_ability (learner_id, path) VALUES (?, ?)", (HARDCODED_LEARNER_ID, "customer_service"), ) conn.commit() row = conn.execute( "SELECT theta, sigma_sq, observations FROM learner_ability " "WHERE learner_id = ? AND path = ?", (HARDCODED_LEARNER_ID, "customer_service"), ).fetchone() conn.close() assert row is not None assert row[0] == 0.0 assert row[1] == 1.0 assert row[2] == 0 def test_get_progress_returns_none_for_new_learner(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() return await store.get_progress(HARDCODED_LEARNER_ID, "customer_service") assert _await(_run()) is None def test_upsert_progress_round_trip(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_progress( HARDCODED_LEARNER_ID, "customer_service", current_week=3, scenarios_passed=["cs_refund_ca_v01", "cs_escalation_ca_v02"], mastery_score=3.7, gate_open=False, ) return await store.get_progress(HARDCODED_LEARNER_ID, "customer_service") row = _await(_run()) assert row is not None assert row["learner_id"] == HARDCODED_LEARNER_ID assert row["path"] == "customer_service" assert row["current_week"] == 3 assert json.loads(row["scenarios_passed_json"]) == [ "cs_refund_ca_v01", "cs_escalation_ca_v02", ] assert row["mastery_score"] == pytest.approx(3.7) assert row["gate_open"] == 0 assert row["updated_at"] is not None def test_upsert_progress_gate_open_true(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_progress( HARDCODED_LEARNER_ID, "customer_service", current_week=6, scenarios_passed=["s1", "s2", "s3"], mastery_score=4.0, gate_open=True, ) return await store.get_progress(HARDCODED_LEARNER_ID, "customer_service") row = _await(_run()) assert row is not None assert row["gate_open"] == 1 assert row["current_week"] == 6 def test_upsert_progress_updates_existing(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_progress( HARDCODED_LEARNER_ID, "customer_service", current_week=1, scenarios_passed=[], mastery_score=0.0, gate_open=False, ) await store.upsert_progress( HARDCODED_LEARNER_ID, "customer_service", current_week=4, scenarios_passed=["s1", "s2", "s3", "s4"], mastery_score=3.9, gate_open=True, ) return await store.get_progress(HARDCODED_LEARNER_ID, "customer_service") row = _await(_run()) assert row is not None assert row["current_week"] == 4 assert json.loads(row["scenarios_passed_json"]) == ["s1", "s2", "s3", "s4"] assert row["mastery_score"] == pytest.approx(3.9) assert row["gate_open"] == 1 def test_ability_and_progress_isolated_per_path(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() await store.upsert_ability(HARDCODED_LEARNER_ID, "customer_service", 1.0, 0.5, 10) await store.upsert_ability(HARDCODED_LEARNER_ID, "sales", -0.5, 0.9, 2) await store.upsert_progress( HARDCODED_LEARNER_ID, "customer_service", 2, ["s1"], 3.2, False ) await store.upsert_progress( HARDCODED_LEARNER_ID, "sales", 1, [], 0.0, False ) a_cs = await store.get_ability(HARDCODED_LEARNER_ID, "customer_service") a_sales = await store.get_ability(HARDCODED_LEARNER_ID, "sales") p_cs = await store.get_progress(HARDCODED_LEARNER_ID, "customer_service") p_sales = await store.get_progress(HARDCODED_LEARNER_ID, "sales") return a_cs, a_sales, p_cs, p_sales a_cs, a_sales, p_cs, p_sales = _await(_run()) assert a_cs["theta"] == pytest.approx(1.0) assert a_sales["theta"] == pytest.approx(-0.5) assert p_cs["current_week"] == 2 assert p_sales["current_week"] == 1