"""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")