813bd586d6
v0.3 milestone merged to main. Mastery scoring + competency rubrics + verifiable credentials (formative-tier) shipped. 13/13 REQ-IDs covered. Next milestone: v0.4 (operator tier — cohort dashboard + auth + Postgres). ---ci--- project: praxis phase: 2 milestone: v0.3 status: complete milestone_complete: true milestone_merged_to_main: true ---/ci---
232 lines
7.0 KiB
Python
232 lines
7.0 KiB
Python
"""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 |