"""Unit tests for the SQLite schema + async store (TASK-04-01, TASK-04-02).""" from __future__ import annotations import asyncio 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 test_migration_creates_all_tables(tmp_db: Path): """TASK-04-01: migration creates learner, sessions, turns, progress.""" applied = apply_migrations(tmp_db) assert "0001_init" 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", "sessions", "turns", "progress"} <= tables def test_hardcoded_learner_row_exists(tmp_db: Path): """TASK-04-01: the hardcoded learner-1 'Alex' row exists (D-007, no auth).""" apply_migrations(tmp_db) conn = sqlite3.connect(str(tmp_db)) row = conn.execute( "SELECT id, display_name FROM learner WHERE id = ?", (HARDCODED_LEARNER_ID,) ).fetchone() conn.close() assert row is not None assert row[0] == "learner-1" assert row[1] == "Alex" def test_migrations_are_idempotent(tmp_db: Path): """Re-running migrations doesn't re-apply.""" apply_migrations(tmp_db) applied = apply_migrations(tmp_db) assert applied == [] def test_store_start_log_end_session(tmp_db: Path): """TASK-04-02: start session → log 3 turns → end session → query returns full session.""" store = PraxisStore(tmp_db) async def _run(): await store.init() sid = await store.start_session(HARDCODED_LEARNER_ID, "cs_refund_ca_v01") await store.log_turn(sid, 0, "assistant", tts_text="Hi, I want a refund.", latency_ms=None) await store.log_turn(sid, 1, "user", asr_text="I'm sorry, I can help.", latency_ms=450.0) await store.log_turn(sid, 2, "assistant", tts_text="Okay, what's the issue?", latency_ms=520.0) await store.end_session( sid, branch_path=["accept_resolution"], outcome="success", cost_cents=12, cost_breakdown={"tokens": 500, "minutes": 1.2, "chars": 320}, debrief_text="You did well acknowledging the customer.", ) sess = await store.get_session(sid) turns = await store.get_turns(sid) return sess, turns sess, turns = asyncio.run(_run()) assert sess is not None assert sess.learner_id == "learner-1" assert sess.scenario_id == "cs_refund_ca_v01" assert sess.outcome == "success" assert sess.branch_path == ["accept_resolution"] assert sess.cost_estimated_cents == 12 assert sess.debrief_text == "You did well acknowledging the customer." assert sess.cost_breakdown["tokens"] == 500 assert len(turns) == 3 assert turns[0].role == "assistant" assert turns[1].asr_text == "I'm sorry, I can help." assert turns[2].latency_ms == 520.0 def test_store_update_progress(tmp_db: Path): """TASK-04-02: update_progress increments attempts + sets last_outcome.""" store = PraxisStore(tmp_db) async def _run(): await store.init() await store.update_progress(HARDCODED_LEARNER_ID, "cs_refund_ca_v01", "success") await store.update_progress(HARDCODED_LEARNER_ID, "cs_refund_ca_v01", "failure") async with store._connect() as db: cur = await db.execute( "SELECT attempts, last_outcome FROM progress WHERE learner_id = ? AND scenario_id = ?", (HARDCODED_LEARNER_ID, "cs_refund_ca_v01"), ) return await cur.fetchone() row = asyncio.run(_run()) assert row is not None assert row[0] == 2 # two attempts assert row[1] == "failure" # last outcome def test_store_get_learner(tmp_db: Path): store = PraxisStore(tmp_db) async def _run(): await store.init() return await store.get_learner() learner = asyncio.run(_run()) assert learner["id"] == "learner-1" assert learner["display_name"] == "Alex"