Files
praxis/tests/test_path_engine.py
T
Praxis CI 4d39596a7d 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---
2026-08-04 00:03:13 +00:00

246 lines
7.5 KiB
Python

"""Unit tests for the path engine (SLICE-05, TASK-05-04)."""
from __future__ import annotations
from pathlib import Path as FsPath
import pytest
import yaml
from pydantic import ValidationError
from server.paths.engine import PathEngine, clear_cache
from server.paths.schema import Path, PathWeek, WeekGate
_REPO_PATHS_DIR = FsPath(__file__).resolve().parent.parent / "paths"
@pytest.fixture(autouse=True)
def _clear_path_cache():
clear_cache()
yield
clear_cache()
def _passing_progress(week: int, distinct_passed: int = 3, mastery_score: float = 3.5) -> dict:
return {
"current_week": week,
"distinct_passed": distinct_passed,
"mastery_score": mastery_score,
}
def test_load_customer_service_path_has_six_weeks():
engine = PathEngine()
path = engine.load_path("customer_service")
assert path.slug == "customer_service"
assert path.skill == "customer_service"
assert len(path.weeks) == 6
assert [w.week for w in path.weeks] == [1, 2, 3, 4, 5, 6]
titles = [w.title for w in path.weeks]
assert "Foundations" in titles[0]
assert "De-escalation" in titles[1]
assert "Policy Exceptions" in titles[2]
assert "Multi-Issue Resolution" in titles[3]
assert "Recovery" in titles[4]
assert "Mastery Demonstration" in titles[5]
def test_each_week_gate_defaults_match_d032():
engine = PathEngine()
path = engine.load_path("customer_service")
for w in path.weeks:
assert w.gate.required_scenarios == 3
assert w.gate.required_score == 3.5
def test_path_scenario_ids_reference_expected_set():
engine = PathEngine()
path = engine.load_path("customer_service")
expected = [
"cs_refund_ca_v01",
"cs_escalation_ca_v02",
"cs_policy_exception_ca_v03",
"cs_multi_issue_ca_v04",
"cs_recovery_ca_v05",
"cs_mastery_demonstration_ca_v06",
]
assert path.all_scenario_ids() == expected
def test_gate_open_when_three_passed_and_score_3_5():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=1, distinct_passed=3, mastery_score=3.5)
assert engine.check_gate(progress, 1, path) is True
def test_gate_open_above_threshold():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=2, distinct_passed=4, mastery_score=4.0)
assert engine.check_gate(progress, 2, path) is True
def test_gate_closed_when_only_two_passed():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=1, distinct_passed=2, mastery_score=4.0)
assert engine.check_gate(progress, 1, path) is False
def test_gate_closed_when_score_below_threshold():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=1, distinct_passed=3, mastery_score=3.0)
assert engine.check_gate(progress, 1, path) is False
def test_advance_week_increments_current_week():
engine = PathEngine()
progress = _passing_progress(week=1)
advanced = engine.advance_week(progress)
assert advanced["current_week"] == 2
assert progress["current_week"] == 1
def test_advance_week_caps_at_six():
engine = PathEngine()
progress = _passing_progress(week=6)
advanced = engine.advance_week(progress)
assert advanced["current_week"] == 6
def test_current_week_defaults_to_one():
engine = PathEngine()
assert engine.current_week({}) == 1
assert engine.current_week({"current_week": 99}) == 6
assert engine.current_week({"current_week": 0}) == 1
def test_is_path_complete_true_when_week6_gate_open():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=6, distinct_passed=3, mastery_score=3.5)
assert engine.is_path_complete(progress, path) is True
def test_is_path_complete_false_when_week6_gate_closed():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=6, distinct_passed=2, mastery_score=4.0)
assert engine.is_path_complete(progress, path) is False
def test_check_gate_rejects_unknown_week():
engine = PathEngine()
path = engine.load_path("customer_service")
progress = _passing_progress(week=1)
with pytest.raises(ValueError):
engine.check_gate(progress, 7, path)
def test_reject_five_weeks(tmp_path: FsPath):
slug = "five_week_path"
data = {
"slug": slug,
"name": "Five Week Path",
"skill": "customer_service",
"weeks": [
{"week": i, "title": f"Week {i}", "scenario_ids": [f"s{i}"], "gate": {"required_scenarios": 3, "required_score": 3.5}}
for i in range(1, 6)
],
}
p = tmp_path / f"{slug}.yaml"
p.write_text(yaml.safe_dump(data), encoding="utf-8")
engine = PathEngine(paths_dir=tmp_path)
with pytest.raises(ValidationError):
engine.load_path(slug)
def test_reject_seven_weeks(tmp_path: FsPath):
slug = "seven_week_path"
data = {
"slug": slug,
"name": "Seven Week Path",
"skill": "customer_service",
"weeks": [
{"week": i, "title": f"Week {i}", "scenario_ids": [f"s{i}"], "gate": {"required_scenarios": 3, "required_score": 3.5}}
for i in range(1, 8)
],
}
p = tmp_path / f"{slug}.yaml"
p.write_text(yaml.safe_dump(data), encoding="utf-8")
engine = PathEngine(paths_dir=tmp_path)
with pytest.raises(ValidationError):
engine.load_path(slug)
def test_reject_non_sequential_week_numbers(tmp_path: FsPath):
slug = "nonseq_path"
data = {
"slug": slug,
"name": "Non-Sequential Path",
"skill": "customer_service",
"weeks": [
{"week": i, "title": f"W{i}", "scenario_ids": [f"s{i}"], "gate": {"required_scenarios": 3, "required_score": 3.5}}
for i in [1, 2, 3, 4, 5, 5]
],
}
p = tmp_path / f"{slug}.yaml"
p.write_text(yaml.safe_dump(data), encoding="utf-8")
engine = PathEngine(paths_dir=tmp_path)
with pytest.raises(ValidationError):
engine.load_path(slug)
def test_reject_duplicate_scenario_ids_in_week():
with pytest.raises(ValidationError):
PathWeek(week=1, title="W", scenario_ids=["s1", "s1"])
def test_week_gate_defaults():
g = WeekGate()
assert g.required_scenarios == 3
assert g.required_score == 3.5
def test_validate_scenarios_exist_passes_with_stub_library():
engine = PathEngine()
path = engine.load_path("customer_service")
class _StubLib:
def __init__(self) -> None:
self._ids = set(path.all_scenario_ids())
def get(self, sid: str):
if sid not in self._ids:
raise KeyError(sid)
return object()
refs = engine.validate_scenarios_exist(path, _StubLib())
assert set(refs) == set(path.all_scenario_ids())
def test_validate_scenarios_exist_reports_missing():
engine = PathEngine()
path = engine.load_path("customer_service")
class _EmptyLib:
def get(self, sid: str):
raise KeyError(sid)
with pytest.raises(ValueError):
engine.validate_scenarios_exist(path, _EmptyLib())
def test_load_path_caches():
engine = PathEngine()
p1 = engine.load_path("customer_service")
p2 = engine.load_path("customer_service")
assert p1 is p2
def test_load_path_missing_raises():
engine = PathEngine(paths_dir=FsPath("/nonexistent_paths_dir_xyz"))
with pytest.raises(FileNotFoundError):
engine.load_path("no_such_path")