Spaces:
Running
Running
| """ | |
| tests/test_phase1_sector_and_tasks.py | |
| Phase 1 validation: sector_profiles.py + tasks.py | |
| Run: pytest tests/test_phase1_sector_and_tasks.py -v | |
| """ | |
| import pytest | |
| from app.models import ServiceType, ScenarioMode, EventType | |
| from app.sector_profiles import ( | |
| get_sector_profile, | |
| SECTOR_REGISTRY, | |
| INCOME_CERTIFICATE_PROFILE, | |
| LAND_REGISTRATION_PROFILE, | |
| BIRTH_CERTIFICATE_PROFILE, | |
| PASSPORT_PROFILE, | |
| GST_REGISTRATION_PROFILE, | |
| CASTE_CERTIFICATE_PROFILE, | |
| DRIVING_LICENSE_PROFILE, | |
| ) | |
| from app.tasks import ( | |
| get_task, | |
| list_tasks, | |
| list_benchmark_tasks, | |
| TASK_EASY, | |
| TASK_MEDIUM, | |
| TASK_HARD, | |
| TASK_REGISTRY, | |
| make_extreme_variant, | |
| ) | |
| # ─── Sector Profiles Registry ──────────────────────────────────────────────── | |
| class TestSectorRegistry: | |
| def test_all_services_have_profiles(self): | |
| for svc in ServiceType: | |
| assert svc in SECTOR_REGISTRY, f"Missing profile for {svc}" | |
| def test_get_sector_profile_all_services(self): | |
| for svc in ServiceType: | |
| profile = get_sector_profile(svc) | |
| assert profile.service_type == svc | |
| def test_unknown_service_raises_key_error(self): | |
| with pytest.raises(KeyError): | |
| get_sector_profile("nonexistent_service") # type: ignore | |
| def test_registry_has_seven_entries(self): | |
| assert len(SECTOR_REGISTRY) == 8 | |
| # ─── Individual Sector Profile Values ──────────────────────────────────────── | |
| class TestIncomeCertificateProfile: | |
| def test_sla_days(self): | |
| assert INCOME_CERTIFICATE_PROFILE.sla_days == 21 | |
| def test_missing_docs_probability_range(self): | |
| p = INCOME_CERTIFICATE_PROFILE.missing_docs_probability | |
| assert 0.0 <= p <= 1.0 | |
| def test_field_verification_probability_range(self): | |
| p = INCOME_CERTIFICATE_PROFILE.field_verification_probability | |
| assert 0.0 <= p <= 1.0 | |
| def test_base_processing_rate_positive(self): | |
| assert INCOME_CERTIFICATE_PROFILE.base_processing_rate > 0 | |
| def test_field_verification_days_positive(self): | |
| assert INCOME_CERTIFICATE_PROFILE.field_verification_days >= 1 | |
| def test_doc_defect_rate_paper_higher_than_digital(self): | |
| assert (INCOME_CERTIFICATE_PROFILE.doc_defect_rate_paper > | |
| INCOME_CERTIFICATE_PROFILE.doc_defect_rate_digital) | |
| class TestLandRegistrationProfile: | |
| def test_sla_days_thirty(self): | |
| assert LAND_REGISTRATION_PROFILE.sla_days == 30 | |
| def test_field_verification_heavy(self): | |
| # Land registration has the highest field verification probability | |
| assert LAND_REGISTRATION_PROFILE.field_verification_probability > 0.5 | |
| def test_field_verification_days_longer(self): | |
| # Land should require more field verification days than income cert | |
| assert (LAND_REGISTRATION_PROFILE.field_verification_days >= | |
| INCOME_CERTIFICATE_PROFILE.field_verification_days) | |
| class TestBirthCertificateProfile: | |
| def test_sla_days_seven(self): | |
| assert BIRTH_CERTIFICATE_PROFILE.sla_days == 7 | |
| def test_fast_processing_rate(self): | |
| # Birth certificate should process faster than land registration | |
| assert (BIRTH_CERTIFICATE_PROFILE.base_processing_rate > | |
| LAND_REGISTRATION_PROFILE.base_processing_rate) | |
| def test_low_missing_docs_probability(self): | |
| assert BIRTH_CERTIFICATE_PROFILE.missing_docs_probability < 0.30 | |
| class TestGSTProfile: | |
| def test_sla_days_seven(self): | |
| assert GST_REGISTRATION_PROFILE.sla_days == 7 | |
| def test_all_probabilities_in_range(self): | |
| p = GST_REGISTRATION_PROFILE | |
| for attr in ["missing_docs_probability", "doc_defect_rate_digital", | |
| "doc_defect_rate_paper", "field_verification_probability"]: | |
| val = getattr(p, attr) | |
| assert 0.0 <= val <= 1.0, f"{attr} out of range: {val}" | |
| class TestAllProfileConstraints: | |
| def test_probabilities_in_range(self, service): | |
| p = get_sector_profile(service) | |
| for attr in ["missing_docs_probability", "doc_defect_rate_digital", | |
| "doc_defect_rate_paper", "field_verification_probability", | |
| "manual_scrutiny_intensity", "decision_backlog_sensitivity", | |
| "system_dependency_risk"]: | |
| val = getattr(p, attr) | |
| assert 0.0 <= val <= 1.0, ( | |
| f"{service.value}.{attr} = {val} is outside [0, 1]" | |
| ) | |
| def test_sla_days_positive(self, service): | |
| p = get_sector_profile(service) | |
| assert p.sla_days >= 1 | |
| def test_processing_rate_positive(self, service): | |
| p = get_sector_profile(service) | |
| assert p.base_processing_rate >= 0.1 | |
| def test_field_verification_days_positive(self, service): | |
| p = get_sector_profile(service) | |
| assert p.field_verification_days >= 1 | |
| def test_paper_defect_rate_higher_than_digital(self, service): | |
| p = get_sector_profile(service) | |
| assert p.doc_defect_rate_paper >= p.doc_defect_rate_digital, ( | |
| f"{service.value}: paper defect rate should be >= digital" | |
| ) | |
| # ─── Tasks ──────────────────────────────────────────────────────────────────── | |
| class TestTaskRegistry: | |
| def test_three_benchmark_tasks_exist(self): | |
| tasks = list_benchmark_tasks() | |
| assert len(tasks) == 3 | |
| def test_benchmark_task_ids(self): | |
| tasks = set(list_benchmark_tasks()) | |
| assert "district_backlog_easy" in tasks | |
| assert "mixed_urgency_medium" in tasks | |
| assert "cross_department_hard" in tasks | |
| def test_all_tasks_retrievable(self): | |
| for tid in list_tasks(): | |
| task = get_task(tid) | |
| assert task.task_id == tid | |
| def test_unknown_task_raises_value_error(self): | |
| with pytest.raises(ValueError): | |
| get_task("nonexistent_task_id_xyz") | |
| def test_registry_has_at_least_three_entries(self): | |
| assert len(TASK_REGISTRY) >= 3 | |
| class TestTaskEasy: | |
| def test_task_id(self): | |
| assert TASK_EASY.task_id == "district_backlog_easy" | |
| def test_difficulty(self): | |
| assert TASK_EASY.difficulty == "easy" | |
| def test_scenario_mode_normal(self): | |
| assert TASK_EASY.scenario_mode == ScenarioMode.NORMAL | |
| def test_seed_deterministic(self): | |
| assert TASK_EASY.seed == 42 | |
| def test_max_days_thirty(self): | |
| assert TASK_EASY.max_days == 30 | |
| def test_single_service(self): | |
| assert len(TASK_EASY.enabled_services) == 1 | |
| assert ServiceType.INCOME_CERTIFICATE in TASK_EASY.enabled_services | |
| def test_arrival_rate_positive(self): | |
| for svc, rate in TASK_EASY.arrival_rate_per_day.items(): | |
| assert rate > 0, f"Arrival rate for {svc} should be positive" | |
| def test_officer_pool_valid(self): | |
| pool = TASK_EASY.initial_officer_pool | |
| assert pool.total_officers >= 1 | |
| assert pool.available_officers >= 1 | |
| def test_escalation_budget_nonnegative(self): | |
| assert TASK_EASY.escalation_budget >= 0 | |
| def test_no_fairness_threshold(self): | |
| assert TASK_EASY.fairness_threshold is None | |
| def test_low_event_probability(self): | |
| assert TASK_EASY.event_probability <= 0.10 | |
| class TestTaskMedium: | |
| def test_task_id(self): | |
| assert TASK_MEDIUM.task_id == "mixed_urgency_medium" | |
| def test_difficulty(self): | |
| assert TASK_MEDIUM.difficulty == "medium" | |
| def test_five_services(self): | |
| assert len(TASK_MEDIUM.enabled_services) == 5 | |
| assert ServiceType.PASSPORT in TASK_MEDIUM.enabled_services | |
| assert ServiceType.DRIVING_LICENSE in TASK_MEDIUM.enabled_services | |
| assert ServiceType.AADHAAR_CARD in TASK_MEDIUM.enabled_services | |
| def test_max_days_forty_five(self): | |
| assert TASK_MEDIUM.max_days == 45 | |
| def test_higher_event_probability_than_easy(self): | |
| assert TASK_MEDIUM.event_probability > TASK_EASY.event_probability | |
| def test_arrival_rates_for_all_services(self): | |
| for svc in TASK_MEDIUM.enabled_services: | |
| key = svc if svc in TASK_MEDIUM.arrival_rate_per_day else svc.value | |
| rate = TASK_MEDIUM.arrival_rate_per_day.get(svc, | |
| TASK_MEDIUM.arrival_rate_per_day.get(svc.value, None)) | |
| assert rate is not None and rate > 0 | |
| def test_officer_pool_covers_both_services(self): | |
| pool = TASK_MEDIUM.initial_officer_pool | |
| allocated_services = set(pool.allocated.keys()) | |
| # At least one service should have officers | |
| assert len(allocated_services) >= 1 | |
| class TestTaskHard: | |
| def test_task_id(self): | |
| assert TASK_HARD.task_id == "cross_department_hard" | |
| def test_difficulty(self): | |
| assert TASK_HARD.difficulty == "hard" | |
| def test_scenario_mode_crisis(self): | |
| assert TASK_HARD.scenario_mode == ScenarioMode.CRISIS | |
| def test_max_days_sixty(self): | |
| assert TASK_HARD.max_days == 60 | |
| def test_fairness_threshold_set(self): | |
| assert TASK_HARD.fairness_threshold is not None | |
| assert 0.0 <= TASK_HARD.fairness_threshold <= 1.0 | |
| def test_has_escalation_events(self): | |
| assert EventType.SLA_ESCALATION_ORDER in TASK_HARD.allowed_events | |
| def test_event_probability_highest(self): | |
| assert TASK_HARD.event_probability > TASK_MEDIUM.event_probability | |
| def test_escalation_budget_higher_than_easy(self): | |
| assert TASK_HARD.escalation_budget >= TASK_EASY.escalation_budget | |
| class TestExtremeVariant: | |
| def test_extreme_variant_creation(self): | |
| extreme = make_extreme_variant(TASK_EASY) | |
| assert "_extreme" in extreme.task_id | |
| def test_extreme_scenario_mode(self): | |
| extreme = make_extreme_variant(TASK_MEDIUM) | |
| assert extreme.scenario_mode == ScenarioMode.EXTREME_OVERLOAD | |
| def test_extreme_event_probability_higher(self): | |
| extreme = make_extreme_variant(TASK_EASY) | |
| assert extreme.event_probability > TASK_EASY.event_probability | |
| def test_extreme_does_not_mutate_original(self): | |
| original_mode = TASK_EASY.scenario_mode | |
| make_extreme_variant(TASK_EASY) | |
| assert TASK_EASY.scenario_mode == original_mode | |
| class TestTaskDeterminism: | |
| def test_same_seed_same_task(self): | |
| t1 = get_task("district_backlog_easy") | |
| t2 = get_task("district_backlog_easy") | |
| assert t1.seed == t2.seed | |
| assert t1.max_days == t2.max_days | |
| def test_tasks_have_different_seeds(self): | |
| seeds = {get_task(tid).seed for tid in list_benchmark_tasks()} | |
| assert len(seeds) == 3, "Each benchmark task must have a unique seed" | |