""" Tests for the SLA engine (app/utils/sla.py). sla_status/deadline/hours_remaining read only .status, .severity, .reported_at, so a lightweight stand-in object is enough — no DB or app context needed. Time is controlled by positioning reported_at relative to now_eastern(). """ from datetime import timedelta from types import SimpleNamespace import pytest from app.utils.sla import ( sla_status, sla_deadline, sla_hours_remaining, SLA_HOURS, AT_RISK_THRESHOLD, ) from app.utils.time_utils import now_eastern def issue(severity='high', status='open', hours_ago=0): return SimpleNamespace( severity=severity, status=status, reported_at=now_eastern() - timedelta(hours=hours_ago), ) def test_resolved_issue_has_no_sla(): assert sla_status(issue(status='resolved', hours_ago=999)) is None def test_unknown_severity_returns_none(): assert sla_status(issue(severity='nonsense', hours_ago=1)) is None def test_fresh_issue_is_ok(): # high = 24h window, at-risk at 18h; 1h in is comfortably OK assert sla_status(issue(severity='high', hours_ago=1)) == 'ok' def test_issue_past_at_risk_threshold(): # high at-risk at 18h; 20h in is at_risk but not yet breached (24h) assert sla_status(issue(severity='high', hours_ago=20)) == 'at_risk' def test_issue_past_deadline_is_breached(): assert sla_status(issue(severity='high', hours_ago=25)) == 'breached' @pytest.mark.parametrize('severity', list(SLA_HOURS.keys())) def test_at_risk_boundary_for_each_severity(severity): window = SLA_HOURS[severity] # Just past the at-risk fraction, still before the deadline. at_risk_hours = window * AT_RISK_THRESHOLD assert sla_status(issue(severity=severity, hours_ago=at_risk_hours + 0.1)) == 'at_risk' # Just before the at-risk fraction is still OK. assert sla_status(issue(severity=severity, hours_ago=at_risk_hours - 0.5)) == 'ok' def test_deadline_is_reported_at_plus_window(): iss = issue(severity='medium', hours_ago=0) expected = iss.reported_at + timedelta(hours=SLA_HOURS['medium']) assert sla_deadline(iss) == expected def test_hours_remaining_positive_when_within_window(): remaining = sla_hours_remaining(issue(severity='critical', hours_ago=1)) # critical window is 4h; ~3h should remain assert 2.5 < remaining < 3.5 def test_hours_remaining_negative_when_overdue(): remaining = sla_hours_remaining(issue(severity='critical', hours_ago=6)) assert remaining < 0 def test_hours_remaining_none_for_resolved(): assert sla_hours_remaining(issue(status='resolved', hours_ago=1)) is None