""" Test publisher rate limiting. """ from unittest.mock import AsyncMock import pytest from senpai.md_consumer.publisher import Publisher from senpai.md_consumer.models import FeatureSnapshot, TradeSignal @pytest.fixture def mock_nc(): """Mock NATS client.""" nc = AsyncMock() nc.publish = AsyncMock() return nc @pytest.fixture def publisher(mock_nc): return Publisher(mock_nc) @pytest.mark.asyncio async def test_publish_features_respects_rate_limit(mock_nc, publisher): """Second publish for same symbol within rate window should be skipped.""" snapshot = FeatureSnapshot( symbol="BTCUSDT", features={"mid": 70000.0}, ) # First publish should succeed result1 = await publisher.publish_features(snapshot) assert result1 is True # Immediate second publish should be rate-limited result2 = await publisher.publish_features(snapshot) assert result2 is False # rate-limited # Only one actual NATS publish assert mock_nc.publish.call_count == 1 @pytest.mark.asyncio async def test_publish_features_different_symbols(mock_nc, publisher): """Different symbols have independent rate limiters.""" snap1 = FeatureSnapshot(symbol="BTCUSDT", features={"mid": 70000.0}) snap2 = FeatureSnapshot(symbol="ETHUSDT", features={"mid": 2000.0}) r1 = await publisher.publish_features(snap1) r2 = await publisher.publish_features(snap2) assert r1 is True assert r2 is True assert mock_nc.publish.call_count == 2 @pytest.mark.asyncio async def test_publish_signal_no_rate_limit(mock_nc, publisher): """Signals are NOT rate limited.""" signal = TradeSignal( symbol="BTCUSDT", direction="long", confidence=0.8, reason="test", ) r1 = await publisher.publish_signal(signal) r2 = await publisher.publish_signal(signal) assert r1 is True assert r2 is True assert mock_nc.publish.call_count == 2 @pytest.mark.asyncio async def test_publish_features_after_rate_window(mock_nc, publisher): """After rate window passes, publish should succeed again.""" # Override min interval to something very small for testing publisher._min_interval = 0.01 # 10ms snapshot = FeatureSnapshot( symbol="BTCUSDT", features={"mid": 70000.0}, ) r1 = await publisher.publish_features(snapshot) assert r1 is True # Wait for rate window to pass import asyncio await asyncio.sleep(0.02) r2 = await publisher.publish_features(snapshot) assert r2 is True assert mock_nc.publish.call_count == 2 @pytest.mark.asyncio async def test_publish_handles_nats_error(mock_nc, publisher): """NATS publish error should not raise, just return False.""" mock_nc.publish.side_effect = Exception("NATS down") snapshot = FeatureSnapshot( symbol="BTCUSDT", features={"mid": 70000.0}, ) result = await publisher.publish_features(snapshot) assert result is False