from __future__ import annotations from unittest.mock import patch from yuxi.channel.lifecycle.backoff import BackoffPolicy class TestBackoffPolicy: def test_default_parameters(self): policy = BackoffPolicy() assert policy.base_ms == 1000 assert policy.max_ms == 30000 assert policy.jitter_ms == 1000 def test_custom_parameters(self): policy = BackoffPolicy(base_ms=500, max_ms=10000, jitter_ms=200) assert policy.base_ms == 500 assert policy.max_ms == 10000 assert policy.jitter_ms == 200 def test_zero_attempts_uses_base_delay(self): policy = BackoffPolicy(base_ms=1000, jitter_ms=0) assert policy.compute(0) == 1000 def test_exponential_growth(self): policy = BackoffPolicy(base_ms=1000, jitter_ms=0) assert policy.compute(1) == 2000 assert policy.compute(2) == 4000 assert policy.compute(3) == 8000 def test_max_delay_cap(self): policy = BackoffPolicy(base_ms=1000, max_ms=5000, jitter_ms=0) assert policy.compute(10) == 5000 def test_negative_attempts_clamped_to_zero(self): policy = BackoffPolicy(base_ms=1000, jitter_ms=0) assert policy.compute(-1) == 1000 assert policy.compute(-5) == 1000 def test_jitter_is_applied(self): policy = BackoffPolicy(base_ms=1000, jitter_ms=500) with patch.object(policy, "jitter_ms", 500): with patch("yuxi.channel.lifecycle.backoff.random.randint", return_value=250): assert policy.compute(0) == 1250 def test_attempts_capped_at_30(self): policy = BackoffPolicy(base_ms=1, max_ms=1_000_000_000, jitter_ms=0) # 2**30 is large but capped by attempts, not max_ms value_30 = policy.compute(30) value_31 = policy.compute(31) value_100 = policy.compute(100) assert value_30 == value_31 == value_100 def test_jitter_range(self): policy = BackoffPolicy(base_ms=1000, jitter_ms=100) for _ in range(50): delay = policy.compute(0) assert 1000 <= delay <= 1100 def test_jitter_does_not_exceed_max_ms(self): policy = BackoffPolicy(base_ms=1000, max_ms=2000, jitter_ms=500) with patch("yuxi.channel.lifecycle.backoff.random.randint", return_value=500): delay = policy.compute(10) assert delay == 2000