"""E2E tests for the full sentiment engine pipeline""" import pytest import asyncio import time import tempfile import os from datetime import datetime from sentiment_engine.catalogue.manager import CatalogueManager from sentiment_engine.ingestion.router import IngestionRouter from sentiment_engine.utils.config import get_settings from sentiment_engine.schemas.payload import NormalizedPayload, SourceType, AssetMention, EngagementMetrics @pytest.fixture async def catalogue(): cat = CatalogueManager() await cat.initialize() yield cat await cat.stop() @pytest.fixture async def router(catalogue): settings = get_settings() router = IngestionRouter( nats_servers=settings.nats_servers, stream_name=settings.nats_stream_ingestion, subject_map={ "news": "sentiment.ingest.news", "social": "sentiment.ingest.social", "regulatory": "sentiment.ingest.regulatory", "exchange": "sentiment.ingest.exchange", }, catalogue=catalogue ) await router.connect() yield router await router._nc.close() @pytest.mark.asyncio async def test_rss_to_nats_pipeline(): """E2E test: RSS ingestion -> NATS -> catalogue""" import yaml import asyncio import sys sys.path.insert(0, '/mnt/dolphinng5_predict/sentiment_engine/src') from sentiment_engine.catalogue.manager import CatalogueManager from sentiment_engine.ingestion.router import IngestionRouter # Load settings from YAML with open('/mnt/dolphinng5_predict/sentiment_engine/config/settings.yaml') as f: settings = yaml.safe_load(f) # 1. Catalogue cat = CatalogueManager() await cat.initialize() assert len(cat.catalogue.get_sources()) == 14 # 2. Ingestion Router - pass CatalogueManager, not store router = IngestionRouter( nats_servers=settings['nats']['servers'], stream_name=settings['nats']['stream_ingestion'], subject_map={ 'news': 'sentiment.ingest.news', 'social': 'sentiment.ingest.social', 'regulatory': 'sentiment.ingest.regulatory', 'exchange': 'sentiment.ingest.exchange', }, catalogue=cat # Pass CatalogueManager, not store ) await router.connect() # 3. Route a test payload test_payload = NormalizedPayload( source_id="rss:coindesk.com", source_type=SourceType.NEWS, source_credibility_base=0.85, ingest_ts=time.time(), publish_ts=time.time(), asset_mentions=[AssetMention(asset_id="BTC", mention_span=(0, 3), confidence=0.9, source_text="BTC", mention_type="ticker")], raw_text="BTC surges to new highs as institutional adoption accelerates. Bitcoin breaks $70k resistance with massive volume.", title="Bitcoin Breaks $70k", url="https://coindesk.com/btc-surge", author="Test Author", content_length=200, language="en", metadata={"test": True} ) # Route the payload result = await router.route(test_payload) assert result is True # Verify fetch was recorded source = cat.catalogue.get_source("rss:coindesk.com") assert source is not None assert source.total_fetches >= 1 assert source.successful_fetches >= 1 await router._nc.close() await cat.stop() class TestFullIngestionFlow: """Test complete ingestion flow with multiple sources""" @pytest.fixture(autouse=True) async def setup_teardown(self): """Setup and teardown for each test""" self.cat = CatalogueManager() await self.cat.initialize() settings = get_settings() self.router = IngestionRouter( nats_servers=settings.nats_servers, stream_name=settings.nats_stream_ingestion, subject_map={ "news": "sentiment.ingest.news", "social": "sentiment.ingest.social", "regulatory": "sentiment.ingest.regulatory", "exchange": "sentiment.ingest.exchange", }, catalogue=self.cat # Pass CatalogueManager, not store ) await self.router.connect() yield await self.router._nc.close() await self.cat.stop() @pytest.mark.asyncio async def test_multi_source_ingestion(self): """Test ingestion from multiple source types""" import time payloads = [ NormalizedPayload( source_id="rss:coindesk.com", source_type=SourceType.NEWS, source_credibility_base=0.85, ingest_ts=time.time(), publish_ts=time.time(), asset_mentions=[AssetMention(asset_id="BTC", mention_span=(0, 3), confidence=0.9, source_text="BTC", mention_type="ticker")], raw_text="Bitcoin reaches new all-time high as ETF inflows continue to surge.", title="BTC New ATH", url="https://coindesk.com/btc-ath", author="Author 1", content_length=150, language="en", metadata={} ), NormalizedPayload( source_id="reddit:Bitcoin", source_type=SourceType.SOCIAL, source_credibility_base=0.65, ingest_ts=time.time(), publish_ts=time.time(), asset_mentions=[AssetMention(asset_id="ETH", mention_span=(0, 3), confidence=0.85, source_text="ETH", mention_type="ticker")], raw_text="Ethereum merge successful, staking rewards now live.", title="ETH Merge Complete", url="https://reddit.com/r/bitcoin/comments/xxx", author="redditor123", content_length=200, language="en", metadata={} ), NormalizedPayload( source_id="api:fred_vix", source_type=SourceType.REGULATORY, source_credibility_base=0.95, ingest_ts=time.time(), publish_ts=time.time(), asset_mentions=[AssetMention(asset_id="SPX", mention_span=(0, 3), confidence=0.8, source_text="SPX", mention_type="ticker")], raw_text="VIX drops below 15 as market volatility decreases.", title="VIX Low", url="https://fred.stlouisfed.org/series/VIXCLS", author="FRED", content_length=100, language="en", metadata={} ) ] # Route all payloads routed = await self.router.route_batch(payloads) assert routed == 3 # Verify all sources recorded fetches for source_id in ["rss:coindesk.com", "reddit:Bitcoin", "api:fred_vix"]: source = self.cat.catalogue.get_source(source_id) assert source is not None, f"Source {source_id} not found" assert source.total_fetches >= 1, f"No fetches recorded for {source_id}" assert source.successful_fetches >= 1, f"No successful fetches for {source_id}" @pytest.mark.asyncio async def test_duplicate_detection(self): """Test that duplicate payloads are detected and not re-routed""" import time import tempfile import os # Create a fresh catalogue with temporary database for this test db_path = tempfile.mktemp(suffix='.duckdb') cat = CatalogueManager(db_path=db_path) await cat.initialize() settings = get_settings() router = IngestionRouter( nats_servers=settings.nats_servers, stream_name=settings.nats_stream_ingestion, subject_map={ "news": "sentiment.ingest.news", "social": "sentiment.ingest.social", "regulatory": "sentiment.ingest.regulatory", "exchange": "sentiment.ingest.exchange", }, catalogue=cat ) await router.connect() payload = NormalizedPayload( source_id="rss:coindesk.com", source_type=SourceType.NEWS, source_credibility_base=0.85, ingest_ts=time.time(), publish_ts=time.time(), asset_mentions=[AssetMention(asset_id="BTC", mention_span=(0, 3), confidence=0.9, source_text="BTC", mention_type="ticker")], raw_text="Duplicate test payload for deduplication check.", title="Duplicate Test", url="https://coindesk.com/duplicate", author="Test", content_length=100, language="en", metadata={} ) # Route same payload twice result1 = await router.route(payload) result2 = await router.route(payload) assert result1 is True assert result2 is False # Second should be detected as duplicate # Only one fetch should be recorded source = cat.catalogue.get_source("rss:coindesk.com") assert source.total_fetches == 1 assert source.successful_fetches == 1 await router._nc.close() await cat.stop() # Clean up temp database try: os.unlink(db_path) except: pass