"""Tests for adaptive sync strategy selection.""" import pytest from radicle_reticulum.adaptive import ( SyncStrategy, LinkQuality, estimate_transfer_time, select_strategy, THRESHOLD_FULL, THRESHOLD_INCREMENTAL, RTT_FAST, RTT_MEDIUM, RTT_SLOW, QR_MAX_BYTES, ) def make_quality( rtt_ms: float = 50, throughput_bps: float = 1_000_000, is_lora: bool = False, strategy: SyncStrategy = SyncStrategy.FULL, ) -> LinkQuality: return LinkQuality( rtt_ms=rtt_ms, throughput_bps=throughput_bps, packet_loss=0.0, is_lora=is_lora, strategy=strategy, ) class TestLinkQuality: def test_throughput_kbps(self): q = make_quality(throughput_bps=50_000) assert q.throughput_kbps == pytest.approx(50.0) def test_repr(self): q = make_quality(rtt_ms=100, throughput_bps=1000) r = repr(q) assert "100ms" in r assert "1.0Kbps" in r class TestEstimateTransferTime: def test_zero_throughput_is_infinite(self): q = make_quality(throughput_bps=0) assert estimate_transfer_time(1024, q) == float("inf") def test_known_value(self): # 1 MB at 1 Mbps effective = 8s, but 80% efficiency → 10s q = make_quality(throughput_bps=1_000_000) t = estimate_transfer_time(1_000_000, q) assert t == pytest.approx(10.0, rel=0.01) def test_larger_file_takes_longer(self): q = make_quality(throughput_bps=10_000) t_small = estimate_transfer_time(1_000, q) t_large = estimate_transfer_time(100_000, q) assert t_large > t_small class TestSelectStrategy: def test_fast_link_small_repo_uses_full(self): q = make_quality(rtt_ms=10, throughput_bps=10_000_000, strategy=SyncStrategy.FULL) strategy, reason = select_strategy( bundle_size=100_000, incremental_size=None, quality=q, ) assert strategy == SyncStrategy.FULL assert "full" in reason.lower() or "fast" in reason.lower() def test_lora_link_prefers_incremental_when_viable(self): q = make_quality(rtt_ms=5000, throughput_bps=2400, is_lora=True, strategy=SyncStrategy.MINIMAL) strategy, reason = select_strategy( bundle_size=500_000, incremental_size=5_000, quality=q, max_transfer_time=3600, ) assert strategy == SyncStrategy.INCREMENTAL def test_lora_link_falls_back_to_minimal_when_too_large(self): q = make_quality(rtt_ms=5000, throughput_bps=100, is_lora=True, strategy=SyncStrategy.MINIMAL) strategy, reason = select_strategy( bundle_size=10_000_000, incremental_size=5_000_000, quality=q, max_transfer_time=3600, ) assert strategy == SyncStrategy.MINIMAL def test_tiny_incremental_uses_qr(self): q = make_quality(rtt_ms=10, throughput_bps=10_000_000, strategy=SyncStrategy.FULL) strategy, reason = select_strategy( bundle_size=50_000, incremental_size=QR_MAX_BYTES - 1, quality=q, ) assert strategy == SyncStrategy.QR assert str(QR_MAX_BYTES - 1) in reason def test_qr_not_selected_when_incremental_too_large(self): q = make_quality(rtt_ms=10, throughput_bps=10_000_000, strategy=SyncStrategy.FULL) strategy, _ = select_strategy( bundle_size=50_000, incremental_size=QR_MAX_BYTES + 1, quality=q, ) assert strategy != SyncStrategy.QR def test_medium_link_prefers_incremental_over_full(self): q = make_quality( rtt_ms=500, throughput_bps=50_000, strategy=SyncStrategy.INCREMENTAL, ) strategy, _ = select_strategy( bundle_size=5_000_000, incremental_size=50_000, quality=q, max_transfer_time=3600, ) assert strategy == SyncStrategy.INCREMENTAL def test_no_incremental_available_falls_back_to_full_or_minimal(self): q = make_quality(rtt_ms=50, throughput_bps=10_000_000, strategy=SyncStrategy.FULL) strategy, _ = select_strategy( bundle_size=100_000, incremental_size=None, quality=q, ) assert strategy in (SyncStrategy.FULL, SyncStrategy.INCREMENTAL) def test_unreachably_slow_link_uses_minimal(self): q = make_quality(rtt_ms=30000, throughput_bps=10, strategy=SyncStrategy.MINIMAL) strategy, _ = select_strategy( bundle_size=10_000_000, incremental_size=None, quality=q, max_transfer_time=3600, ) assert strategy == SyncStrategy.MINIMAL def test_fast_link_large_repo_prefers_incremental_if_faster(self): q = make_quality(rtt_ms=10, throughput_bps=500_000, strategy=SyncStrategy.FULL) strategy, _ = select_strategy( bundle_size=100_000_000, incremental_size=100_000, quality=q, ) assert strategy == SyncStrategy.INCREMENTAL