""" tests/test_phase1_signal_computer.py Phase 1 validation: signal_computer.py Run: pytest tests/test_phase1_signal_computer.py -v """ import pytest from app.models import ServiceType, OfficerPool, QueueSnapshot from app.signal_computer import SignalComputer, ComputedSignals def make_snapshot( service: ServiceType, total_pending: int = 0, completed_today: int = 0, sla_breached: int = 0, urgent: int = 0, blocked_missing: int = 0, field_pending: int = 0, sla_risk: float = 0.0, ) -> QueueSnapshot: return QueueSnapshot( service_type=service, total_pending=total_pending, total_completed_today=completed_today, total_sla_breached=sla_breached, urgent_pending=urgent, blocked_missing_docs=blocked_missing, field_verification_pending=field_pending, current_sla_risk=sla_risk, ) def make_pool(total=10, available=10, allocated=None) -> OfficerPool: return OfficerPool( total_officers=total, available_officers=available, allocated=allocated or {}, ) class TestComputedSignalsDefaults: def test_defaults_all_zero_or_reasonable(self): s = ComputedSignals() assert s.backlog_pressure == 0.0 assert s.sla_risk_score == 0.0 assert s.fairness_index == 1.0 assert s.resource_utilization == 0.0 assert s.digital_intake_ratio == 0.5 assert s.blocked_cases_missing_docs == 0 assert s.field_verification_load == 0.0 class TestSignalComputerEmpty: def test_empty_snapshots_returns_defaults(self): sc = SignalComputer() pool = make_pool() signals = sc.compute({}, pool) assert signals.backlog_pressure == 0.0 assert signals.fairness_index == 1.0 assert signals.blocked_cases_missing_docs == 0 class TestBacklogPressure: def test_no_backlog_gives_zero_pressure(self): sc = SignalComputer() pool = make_pool(total=10, available=10) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=0)} signals = sc.compute(snap, pool, capacity_per_day=10.0) assert signals.backlog_pressure == 0.0 def test_high_backlog_gives_high_pressure(self): sc = SignalComputer() pool = make_pool(total=5, available=5) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=1000)} signals = sc.compute(snap, pool, capacity_per_day=5.0) assert signals.backlog_pressure > 0.8 def test_backlog_pressure_bounded_at_one(self): sc = SignalComputer() pool = make_pool(total=1, available=1) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=99999)} signals = sc.compute(snap, pool, capacity_per_day=1.0) assert signals.backlog_pressure <= 1.0 class TestSLARiskScore: def test_zero_risk_when_all_cases_fresh(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=10, sla_risk=0.0)} signals = sc.compute(snap, pool) assert signals.sla_risk_score == 0.0 def test_full_risk_when_all_cases_at_deadline(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=10, sla_risk=1.0)} signals = sc.compute(snap, pool) assert abs(signals.sla_risk_score - 1.0) < 0.01 def test_sla_risk_bounded(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5, sla_risk=0.99)} signals = sc.compute(snap, pool) assert 0.0 <= signals.sla_risk_score <= 1.0 class TestFairnessIndex: def test_single_service_fairness_is_one(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5, completed_today=3)} signals = sc.compute(snap, pool) assert signals.fairness_index == 1.0 def test_equal_completion_rates_fairness_is_one(self): sc = SignalComputer() pool = make_pool() snaps = { ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5, completed_today=5), ServiceType.LAND_REGISTRATION.value: make_snapshot(ServiceType.LAND_REGISTRATION, total_pending=5, completed_today=5), } signals = sc.compute(snaps, pool) assert abs(signals.fairness_index - 1.0) < 0.05 def test_unequal_completion_rates_reduce_fairness(self): sc = SignalComputer() pool = make_pool() snaps = { ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=10, completed_today=10), ServiceType.LAND_REGISTRATION.value: make_snapshot(ServiceType.LAND_REGISTRATION, total_pending=10, completed_today=0), } signals = sc.compute(snaps, pool) assert signals.fairness_index < 1.0 def test_fairness_bounded(self): sc = SignalComputer() pool = make_pool() snaps = { "a": make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=100, completed_today=100), "b": make_snapshot(ServiceType.LAND_REGISTRATION, total_pending=100, completed_today=0), } signals = sc.compute(snaps, pool) assert 0.0 <= signals.fairness_index <= 1.0 class TestResourceUtilization: def test_fully_allocated_gives_one(self): sc = SignalComputer() pool = make_pool(total=10, available=10, allocated={"income_certificate": 10}) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5)} signals = sc.compute(snap, pool) assert abs(signals.resource_utilization - 1.0) < 0.01 def test_zero_allocation_gives_zero_utilization(self): sc = SignalComputer() pool = make_pool(total=10, available=10, allocated={}) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5)} signals = sc.compute(snap, pool) assert signals.resource_utilization == 0.0 def test_utilization_bounded(self): sc = SignalComputer() pool = make_pool(total=10, available=10, allocated={"income_certificate": 99}) snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5)} signals = sc.compute(snap, pool) assert 0.0 <= signals.resource_utilization <= 1.0 class TestDigitalIntakeRatio: def test_all_digital_gives_one(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=5)} signals = sc.compute(snap, pool, todays_arrivals=10, digital_arrivals=10) assert signals.digital_intake_ratio == 1.0 def test_no_arrivals_gives_half(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE)} signals = sc.compute(snap, pool, todays_arrivals=0, digital_arrivals=0) assert signals.digital_intake_ratio == 0.5 def test_ratio_bounded(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE)} signals = sc.compute(snap, pool, todays_arrivals=5, digital_arrivals=5) assert 0.0 <= signals.digital_intake_ratio <= 1.0 class TestBlockedAndFieldLoad: def test_blocked_cases_aggregated_across_services(self): sc = SignalComputer() pool = make_pool() snaps = { ServiceType.INCOME_CERTIFICATE.value: make_snapshot(ServiceType.INCOME_CERTIFICATE, total_pending=10, blocked_missing=3), ServiceType.LAND_REGISTRATION.value: make_snapshot(ServiceType.LAND_REGISTRATION, total_pending=8, blocked_missing=2), } signals = sc.compute(snaps, pool) assert signals.blocked_cases_missing_docs == 5 def test_field_verification_load_fraction(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.PASSPORT.value: make_snapshot(ServiceType.PASSPORT, total_pending=10, field_pending=4)} signals = sc.compute(snap, pool) assert abs(signals.field_verification_load - 0.4) < 0.05 def test_field_load_bounded(self): sc = SignalComputer() pool = make_pool() snap = {ServiceType.PASSPORT.value: make_snapshot(ServiceType.PASSPORT, total_pending=5, field_pending=5)} signals = sc.compute(snap, pool) assert 0.0 <= signals.field_verification_load <= 1.0