"""Tests for Claude Quota Monitor and Metabolic Protocol.""" from datetime import UTC, datetime, timedelta from unittest.mock import patch from infrastructure.claude_quota import ( MetabolicTier, QuotaMonitor, QuotaStatus, _time_remaining, get_quota_monitor, ) def _make_status(five_hour: float = 0.0, seven_day: float = 0.0) -> QuotaStatus: """Helper: build a QuotaStatus with given utilization values.""" return QuotaStatus( five_hour_utilization=five_hour, five_hour_resets_at=None, seven_day_utilization=seven_day, seven_day_resets_at=None, raw_response={}, fetched_at=datetime.now(UTC), ) class TestMetabolicTierThresholds: """Test the three-tier metabolic protocol thresholds.""" def test_burst_when_five_hour_below_50pct(self): status = _make_status(five_hour=0.49, seven_day=0.10) assert status.recommended_tier == MetabolicTier.BURST def test_burst_at_zero_utilization(self): status = _make_status(five_hour=0.0, seven_day=0.0) assert status.recommended_tier == MetabolicTier.BURST def test_active_when_five_hour_at_50pct(self): status = _make_status(five_hour=0.50, seven_day=0.10) assert status.recommended_tier == MetabolicTier.ACTIVE def test_active_when_five_hour_between_50_and_80pct(self): status = _make_status(five_hour=0.79, seven_day=0.10) assert status.recommended_tier == MetabolicTier.ACTIVE def test_active_when_five_hour_at_80pct(self): # five_hour >= 0.80 but seven_day < 0.80 → ACTIVE (not RESTING) status = _make_status(five_hour=0.80, seven_day=0.50) assert status.recommended_tier == MetabolicTier.ACTIVE def test_resting_when_seven_day_at_80pct(self): status = _make_status(five_hour=0.30, seven_day=0.80) assert status.recommended_tier == MetabolicTier.RESTING def test_resting_when_seven_day_above_80pct(self): status = _make_status(five_hour=0.10, seven_day=0.95) assert status.recommended_tier == MetabolicTier.RESTING def test_resting_when_both_critical(self): status = _make_status(five_hour=0.90, seven_day=0.90) assert status.recommended_tier == MetabolicTier.RESTING def test_seven_day_takes_precedence_over_five_hour(self): # Weekly quota critical overrides whatever five-hour says status = _make_status(five_hour=0.10, seven_day=0.85) assert status.recommended_tier == MetabolicTier.RESTING class TestQuotaStatusProperties: """Test QuotaStatus computed properties.""" def test_five_hour_pct(self): status = _make_status(five_hour=0.42) assert status.five_hour_pct == 42 def test_seven_day_pct(self): status = _make_status(seven_day=0.75) assert status.seven_day_pct == 75 def test_summary_contains_tier(self): status = _make_status(five_hour=0.20, seven_day=0.10) summary = status.summary() assert "burst" in summary assert "20%" in summary def test_five_hour_resets_in_unknown_when_none(self): status = _make_status() assert status.five_hour_resets_in == "unknown" def test_seven_day_resets_in_unknown_when_none(self): status = _make_status() assert status.seven_day_resets_in == "unknown" class TestTimeRemaining: """Test _time_remaining helper.""" def test_none_returns_unknown(self): assert _time_remaining(None) == "unknown" def test_empty_string_returns_unknown(self): assert _time_remaining("") == "unknown" def test_past_time_returns_resetting_now(self): past = (datetime.now(UTC) - timedelta(hours=1)).isoformat() assert _time_remaining(past) == "resetting now" def test_future_time_hours_and_minutes(self): future = (datetime.now(UTC) + timedelta(hours=2, minutes=15)).isoformat() result = _time_remaining(future) assert "2h" in result # Minutes may vary ±1 due to test execution time assert "m" in result def test_future_time_minutes_only(self): future = (datetime.now(UTC) + timedelta(minutes=45)).isoformat() result = _time_remaining(future) assert "h" not in result # Minutes may vary ±1 due to test execution time assert "m" in result def test_z_suffix_handled(self): future = (datetime.now(UTC) + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ") result = _time_remaining(future) assert result != "unknown" class TestQuotaMonitorSelectModel: """Test select_model metabolic routing.""" def test_no_quota_high_complexity_returns_14b(self): monitor = QuotaMonitor() monitor._get_token = lambda: None assert monitor.select_model("high") == "qwen3:14b" def test_no_quota_low_complexity_returns_8b(self): monitor = QuotaMonitor() monitor._get_token = lambda: None assert monitor.select_model("low") == "qwen3:8b" def test_burst_tier_high_complexity_returns_cloud(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.10, seven_day=0.10) monitor._cache_seconds = 9999 result = monitor.select_model("high") assert result == "claude-sonnet-4-6" def test_burst_tier_medium_complexity_returns_14b(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.10, seven_day=0.10) monitor._cache_seconds = 9999 result = monitor.select_model("medium") assert result == "qwen3:14b" def test_active_tier_returns_14b(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.65, seven_day=0.10) monitor._cache_seconds = 9999 result = monitor.select_model("high") assert result == "qwen3:14b" def test_resting_tier_returns_8b(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.10, seven_day=0.85) monitor._cache_seconds = 9999 result = monitor.select_model("high") assert result == "qwen3:8b" class TestQuotaMonitorShouldUseCloud: """Test should_use_cloud gate.""" def test_no_credentials_always_false(self): monitor = QuotaMonitor() monitor._get_token = lambda: None assert monitor.should_use_cloud("critical") is False def test_critical_task_allowed_when_under_95pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.10, seven_day=0.94) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("critical") is True def test_critical_task_blocked_when_over_95pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.10, seven_day=0.96) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("critical") is False def test_high_task_allowed_under_60pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.59, seven_day=0.10) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("high") is True def test_high_task_blocked_at_60pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.60, seven_day=0.10) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("high") is False def test_normal_task_allowed_under_30pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.29, seven_day=0.10) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("normal") is True def test_normal_task_blocked_at_30pct(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.30, seven_day=0.10) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("normal") is False def test_routine_task_always_false(self): monitor = QuotaMonitor() monitor._last_status = _make_status(five_hour=0.0, seven_day=0.0) monitor._cache_seconds = 9999 assert monitor.should_use_cloud("routine") is False class TestQuotaMonitorCaching: """Test 30-second TTL cache.""" def test_cached_result_returned_within_ttl(self): monitor = QuotaMonitor() fresh_status = _make_status(five_hour=0.10) monitor._last_status = fresh_status monitor._cache_seconds = 30 # Should NOT re-fetch — returns cached with patch.object(monitor, "_get_token", return_value="tok") as mock_tok: result = monitor.check() mock_tok.assert_not_called() assert result is fresh_status def test_stale_cache_triggers_fetch(self): monitor = QuotaMonitor() old_time = datetime.now(UTC) - timedelta(seconds=60) stale_status = QuotaStatus( five_hour_utilization=0.10, five_hour_resets_at=None, seven_day_utilization=0.10, seven_day_resets_at=None, raw_response={}, fetched_at=old_time, ) monitor._last_status = stale_status # Token unavailable → returns None (triggers re-fetch path) with patch.object(monitor, "_get_token", return_value=None): result = monitor.check() assert result is None # No credentials after cache miss class TestGetQuotaMonitorSingleton: """Test module-level singleton.""" def test_returns_same_instance(self): m1 = get_quota_monitor() m2 = get_quota_monitor() assert m1 is m2 def test_returns_quota_monitor_instance(self): monitor = get_quota_monitor() assert isinstance(monitor, QuotaMonitor)