feat(milestone): merge phase/01 mastery-core → milestone/v0.3-mastery-scoring
Phase 1 complete. Mastery scoring + competency rubrics + VC issuer shipped. 9 slices, 5 waves, 238 tests passing, 13/13 REQ-IDs covered. 4/4 grill MUST conditions satisfied. VERIFY: APPROVE_WITH_NOTES. ---ci--- project: praxis phase: 1 milestone: v0.3 status: complete requirements: covered: [REQ-MAST-01, REQ-MAST-02, REQ-MAST-03, REQ-SCEN-02, REQ-SCEN-03, REQ-SCEN-04, REQ-PATH-02, REQ-NFR-MAST-01, REQ-NFR-MAST-02, REQ-NFR-VC-01, REQ-NFR-VC-02, REQ-NFR-IRT-01] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user