OPENENV_RL_01 / tests /test_phase1_sector_and_tasks.py
Siddharaj Shirke
deploy: fresh snapshot to Hugging Face Space
3eae4cc
"""
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:
@pytest.mark.parametrize("service", list(ServiceType))
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]"
)
@pytest.mark.parametrize("service", list(ServiceType))
def test_sla_days_positive(self, service):
p = get_sector_profile(service)
assert p.sla_days >= 1
@pytest.mark.parametrize("service", list(ServiceType))
def test_processing_rate_positive(self, service):
p = get_sector_profile(service)
assert p.base_processing_rate >= 0.1
@pytest.mark.parametrize("service", list(ServiceType))
def test_field_verification_days_positive(self, service):
p = get_sector_profile(service)
assert p.field_verification_days >= 1
@pytest.mark.parametrize("service", list(ServiceType))
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"