Add sentiment_engine with CryptoSentimentCalibrator fixes - improved keyword lists, lowered FinBERT threshold, added neutral handling
This commit is contained in:
0
sentiment_engine/tests/integration/__init__.py
Normal file
0
sentiment_engine/tests/integration/__init__.py
Normal file
@@ -0,0 +1,564 @@
|
||||
"""
|
||||
Comprehensive integration tests for full pipeline.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from sentiment_engine.nlp.pipeline import NLPProcessingPipeline
|
||||
from sentiment_engine.schemas.payload import NormalizedPayload, SourceType, AssetMention, EngagementMetrics
|
||||
from sentiment_engine.schemas.processed import ProcessedItem
|
||||
|
||||
|
||||
class TestFullPipelineIntegration:
|
||||
"""Full pipeline integration tests"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return NLPProcessingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_initializes_all_components(self, pipeline):
|
||||
"""Pipeline should initialize all NLP components"""
|
||||
await pipeline.initialize()
|
||||
|
||||
assert pipeline._initialized is True
|
||||
assert pipeline.entity_extractor is not None
|
||||
assert pipeline.sentiment_analyzer is not None
|
||||
assert pipeline.event_classifier is not None
|
||||
assert pipeline.temporal_anchorer is not None
|
||||
assert pipeline.credibility_scorer is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_bullish_news(self, pipeline):
|
||||
"""Should process bullish news correctly"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="coindesk",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.9,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=200,
|
||||
raw_text="Bitcoin surges to $108,000 as institutional inflows surge. BlackRock IBIT ETF sees record $1.2B daily inflow!",
|
||||
metadata={"author": "analyst", "engagement_metrics": {"likes": 1000, "retweets": 100}}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert isinstance(result, ProcessedItem)
|
||||
assert result.source_id == "coindesk"
|
||||
assert "BTC" in result.sentiment_per_asset or "IBIT" in result.sentiment_per_asset
|
||||
|
||||
# Should be bullish
|
||||
for asset, sentiment in result.sentiment_per_asset.items():
|
||||
assert sentiment.polarity > 0.3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_bearish_news(self, pipeline):
|
||||
"""Should process bearish news correctly"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="peckshield",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.95,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=200,
|
||||
raw_text="Major hack: Radiant Capital loses $50M in exploit. Attacker exploits rounding error. Funds moved to Tornado Cash.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert isinstance(result, ProcessedItem)
|
||||
|
||||
# Should detect hack event
|
||||
hack_events = [e for e in result.events if e.event_type.value == "hack"]
|
||||
assert len(hack_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_regulatory_news(self, pipeline):
|
||||
"""Should process regulatory news"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="sec_gov",
|
||||
source_type=SourceType.REGULATORY,
|
||||
source_credibility_base=1.0,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="SEC sues Kraken for operating unregistered securities exchange. BTC, ETH, SOL decline on fears.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should detect regulatory event
|
||||
reg_events = [e for e in result.events if e.event_type.value == "regulatory"]
|
||||
assert len(reg_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_upgrade_news(self, pipeline):
|
||||
"""Should process protocol upgrade news"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="ethereum_foundation",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.98,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="Ethereum Dencun upgrade goes live. Proto-Danksharding (EIP-4844) activates reducing L2 fees 90%.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should detect upgrade event
|
||||
upgrade_events = [e for e in result.events if e.event_type.value == "upgrade"]
|
||||
assert len(upgrade_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_listing_news(self, pipeline):
|
||||
"""Should process exchange listing news"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="coinbase",
|
||||
source_type=SourceType.EXCHANGE_ANN,
|
||||
source_credibility_base=0.9,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="Coinbase lists PEPE and BONK memecoins. Trading opens with 100x volume spike.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should detect listing event
|
||||
listing_events = [e for e in result.events if e.event_type.value == "listing"]
|
||||
assert len(listing_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_whale_activity(self, pipeline):
|
||||
"""Should process whale activity"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="whale_alert",
|
||||
source_type=SourceType.ON_CHAIN,
|
||||
source_credibility_base=0.95,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="Whale moves 10,000 BTC after 5 years dormancy. $1.08B transaction spotted on-chain.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should detect whale event
|
||||
whale_events = [e for e in result.events if e.event_type.value == "whale"]
|
||||
assert len(whale_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_market_crash(self, pipeline):
|
||||
"""Should process market crash"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="market_watch",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.9,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="Bitcoin crashes 50% in hours. Massive liquidation cascade wipes out $500M in longs.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should detect liquidation event
|
||||
liq_events = [e for e in result.events if e.event_type.value == "liquidation"]
|
||||
assert len(liq_events) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_stablecoin_depeg(self, pipeline):
|
||||
"""Should process stablecoin depeg"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="circle",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.95,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=150,
|
||||
raw_text="Circle USDC depegs to $0.97 after SVB exposure. $3.3B reserves stuck at SVB.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
# Should be bearish
|
||||
for asset, sentiment in result.sentiment_per_asset.items():
|
||||
assert sentiment.polarity < -0.3
|
||||
|
||||
|
||||
class TestPipelinePerformance:
|
||||
"""Performance tests for pipeline"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return NLPProcessingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_latency_under_threshold(self, pipeline):
|
||||
"""Process should complete within latency threshold"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=200,
|
||||
raw_text="Bitcoin surges to $100k as institutional inflows surge.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
result = await pipeline.process(payload)
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
assert elapsed < 5000 # 5 seconds max
|
||||
assert result.processing_latency_ms < 5000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_processing_throughput(self, pipeline):
|
||||
"""Batch processing should achieve good throughput"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payloads = [
|
||||
NormalizedPayload(
|
||||
source_id=f"source_{i}",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=100,
|
||||
raw_text=f"Bitcoin news item {i}",
|
||||
metadata={}
|
||||
)
|
||||
for i in range(20)
|
||||
]
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
results = await pipeline.process_batch(payloads)
|
||||
elapsed = time.time() - start
|
||||
|
||||
assert len(results) == 20
|
||||
assert elapsed < 10 # 20 items in under 10 seconds
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_processing(self, pipeline):
|
||||
"""Should handle concurrent processing correctly"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=100,
|
||||
raw_text="Bitcoin surges to new high!",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
# Run multiple processes concurrently
|
||||
tasks = [pipeline.process(payload) for _ in range(10)]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
assert len(results) == 10
|
||||
assert all(isinstance(r, ProcessedItem) for r in results)
|
||||
|
||||
|
||||
class TestPipelineDataFlow:
|
||||
"""Tests for data flow through pipeline"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return NLPProcessingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_extraction_output(self, pipeline):
|
||||
"""Entity extraction should produce valid entities"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="BTC and ETH surge. Vitalik buys more ETH.",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert len(result.entities) >= 2
|
||||
entity_assets = [e.asset_id for e in result.entities]
|
||||
assert "BTC" in entity_assets
|
||||
assert "ETH" in entity_assets
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sentiment_output_structure(self, pipeline):
|
||||
"""Sentiment output should have correct structure"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Bitcoin surges to new high!",
|
||||
asset_mentions=[
|
||||
AssetMention(asset_id="BTC", mention_span=(0,3), confidence=0.9, source_text="BTC", mention_type="ticker")
|
||||
],
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert "BTC" in result.sentiment_per_asset
|
||||
sentiment = result.sentiment_per_asset["BTC"]
|
||||
assert hasattr(sentiment, 'polarity')
|
||||
assert hasattr(sentiment, 'confidence')
|
||||
assert hasattr(sentiment, 'positive_prob')
|
||||
assert hasattr(sentiment, 'negative_prob')
|
||||
assert hasattr(sentiment, 'neutral_prob')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emotion_output_structure(self, pipeline):
|
||||
"""Emotion output should have correct structure"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Bitcoin surges to new high!",
|
||||
asset_mentions=[
|
||||
AssetMention(asset_id="BTC", mention_span=(0,3), confidence=0.9, source_text="BTC", mention_type="ticker")
|
||||
],
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert "BTC" in result.emotions_per_asset
|
||||
emotion = result.emotions_per_asset["BTC"]
|
||||
assert hasattr(emotion, 'joy')
|
||||
assert hasattr(emotion, 'fear')
|
||||
assert hasattr(emotion, 'anger')
|
||||
assert hasattr(emotion, 'greed')
|
||||
assert hasattr(emotion, 'sadness')
|
||||
assert hasattr(emotion, 'intensity')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_output_structure(self, pipeline):
|
||||
"""Temporal output should have correct structure"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Breaking: Bitcoin crashes now!",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert result.temporal.time_horizon == "immediate"
|
||||
assert result.temporal.is_breaking is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_credibility_output_structure(self, pipeline):
|
||||
"""Credibility output should have correct structure"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="high_cred",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.9,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=100,
|
||||
raw_text="Bitcoin surges as BlackRock ETF sees massive inflows.",
|
||||
metadata={"author": "analyst", "engagement_metrics": {"likes": 1000, "retweets": 100, "views": 10000}}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
cred = result.credibility
|
||||
assert hasattr(cred, 'composite')
|
||||
assert hasattr(cred, 'source_base')
|
||||
assert hasattr(cred, 'content_quality')
|
||||
assert hasattr(cred, 'engagement_authenticity')
|
||||
assert hasattr(cred, 'cross_source_corroboration')
|
||||
assert hasattr(cred, 'historical_accuracy')
|
||||
|
||||
|
||||
class TestPipelineErrorHandling:
|
||||
"""Error handling tests for pipeline"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return NLPProcessingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_empty_payload(self, pipeline):
|
||||
"""Should handle empty payload gracefully"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.5,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=0,
|
||||
raw_text="",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
assert isinstance(result, ProcessedItem)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_unicode(self, pipeline):
|
||||
"""Should handle unicode text"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Bitcoin 🚀 surges to $100k 💎",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
assert isinstance(result, ProcessedItem)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_special_characters(self, pipeline):
|
||||
"""Should handle special characters"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=100,
|
||||
raw_text="BTC/USD: $50,000.00 (24h: +5.2%) — Bitcoin dominance: 52.3%",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
assert isinstance(result, ProcessedItem)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_partial_failure(self, pipeline):
|
||||
"""Batch should handle partial failures"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payloads = [
|
||||
NormalizedPayload(
|
||||
source_id="good",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Bitcoin surges!",
|
||||
metadata={}
|
||||
),
|
||||
NormalizedPayload(
|
||||
source_id="bad",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.5,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=0,
|
||||
raw_text="",
|
||||
metadata={}
|
||||
)
|
||||
]
|
||||
|
||||
results = await pipeline.process_batch(payloads)
|
||||
|
||||
assert len(results) == 2
|
||||
assert all(isinstance(r, ProcessedItem) for r in results)
|
||||
|
||||
|
||||
class TestPipelineModelVersioning:
|
||||
"""Tests for model version tracking"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return NLPProcessingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_versions_in_output(self, pipeline):
|
||||
"""Processed item should include model versions"""
|
||||
await pipeline.initialize()
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="test",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=1700000000.0,
|
||||
publish_ts=1700000000.0,
|
||||
content_length=50,
|
||||
raw_text="Bitcoin surges!",
|
||||
metadata={}
|
||||
)
|
||||
|
||||
result = await pipeline.process(payload)
|
||||
|
||||
assert "model_versions" in result.__dict__
|
||||
assert isinstance(result.model_versions, dict)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
157
sentiment_engine/tests/integration/test_ingestion_pipeline.py
Normal file
157
sentiment_engine/tests/integration/test_ingestion_pipeline.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Integration tests for ingestion pipeline"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from sentiment_engine.catalogue.manager import CatalogueManager
|
||||
from sentiment_engine.ingestion.router import IngestionRouter
|
||||
from sentiment_engine.schemas.payload import NormalizedPayload, SourceType, AssetMention, EngagementMetrics
|
||||
from sentiment_engine.utils.config import get_settings
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
class TestIngestionPipeline:
|
||||
"""Integration tests for ingestion pipeline"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_catalogue_router_integration(self, catalogue, router):
|
||||
"""Test that catalogue and router work together"""
|
||||
from sentiment_engine.schemas.payload import NormalizedPayload, AssetMention
|
||||
from datetime import datetime
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="rss:test.com",
|
||||
source_type=SourceType.NEWS,
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=datetime.now().timestamp(),
|
||||
publish_ts=datetime.now().timestamp(),
|
||||
asset_mentions=[],
|
||||
raw_text="BTC surges to new highs on institutional adoption",
|
||||
title="BTC Surges",
|
||||
url="https://test.com/article",
|
||||
author="Test Author",
|
||||
content_length=100,
|
||||
language="en"
|
||||
)
|
||||
|
||||
# Route payload
|
||||
result = await router.route(payload)
|
||||
assert result is True
|
||||
|
||||
# Check metrics
|
||||
metrics = router.get_metrics()
|
||||
assert metrics["received"] == 1
|
||||
assert metrics["routed"] == 1
|
||||
assert metrics["by_source"]["rss:test.com"] == 1
|
||||
|
||||
# Verify catalogue recorded the fetch
|
||||
sources = catalogue.catalogue.get_sources()
|
||||
# Should have recorded the fetch attempt
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deduplication(self, router):
|
||||
"""Test that duplicate payloads are rejected"""
|
||||
from sentiment_engine.schemas.payload import NormalizedPayload
|
||||
from datetime import datetime
|
||||
|
||||
payload = NormalizedPayload(
|
||||
source_id="rss:test.com",
|
||||
source_type="news",
|
||||
source_credibility_base=0.8,
|
||||
ingest_ts=datetime.now().timestamp(),
|
||||
publish_ts=datetime.now().timestamp(),
|
||||
asset_mentions=[],
|
||||
raw_text="BTC surges to new highs on institutional adoption",
|
||||
title="BTC Surges",
|
||||
url="https://test.com/article",
|
||||
author="Test Author",
|
||||
content_length=100,
|
||||
language="en"
|
||||
)
|
||||
|
||||
# First submission
|
||||
result1 = await router.route(payload)
|
||||
assert result1 is True
|
||||
|
||||
# Second submission - should be deduplicated
|
||||
result2 = await router.route(payload)
|
||||
assert result2 is False
|
||||
|
||||
metrics = router.get_metrics()
|
||||
assert metrics["duplicates"] == 1
|
||||
|
||||
|
||||
class TestStaleDetectionIntegration:
|
||||
"""Test stale source detection integration"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stale_source_detection(self, catalogue):
|
||||
"""Test that stale sources are detected correctly"""
|
||||
import time
|
||||
conn = catalogue.catalogue._get_conn()
|
||||
conn.execute('UPDATE sources SET last_fetch_ts = ? WHERE source_id = ?',
|
||||
[time.time() - 900, 'rss:binance.com'])
|
||||
|
||||
stale = catalogue.catalogue.get_stale_sources(multiplier=2.0)
|
||||
assert len(stale) >= 1
|
||||
assert any(s.source_id == 'rss:binance.com' for s in stale)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_credibility_decay_detection(self, catalogue):
|
||||
"""Test that credibility decay is detected"""
|
||||
import time
|
||||
conn = catalogue.catalogue._get_conn()
|
||||
# Use an existing source with low credibility
|
||||
conn.execute('UPDATE sources SET current_credibility = 0.2, credibility_updated_ts = ? WHERE source_id = ?',
|
||||
[time.time() - 4*86400, 'reddit:CryptoCurrency'])
|
||||
|
||||
decay = catalogue.catalogue.get_credibility_decay_candidates(threshold=0.3, window_hours=72)
|
||||
assert len(decay) >= 1
|
||||
assert any(s.source_id == 'reddit:CryptoCurrency' for s in decay)
|
||||
|
||||
|
||||
class TestDashboardAggregation:
|
||||
"""Test dashboard data aggregation"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_aggregation(self, catalogue):
|
||||
"""Test dashboard data aggregation"""
|
||||
from sentiment_engine.catalogue.manager import CatalogueManager as CM
|
||||
cat_mgr = CM.__new__(CM)
|
||||
cat_mgr.catalogue = catalogue.catalogue
|
||||
dashboard = cat_mgr.get_dashboard_data()
|
||||
|
||||
assert dashboard["total_sources"] >= 14
|
||||
assert "stale_count" in dashboard
|
||||
assert "decay_count" in dashboard
|
||||
assert "avg_credibility" in dashboard
|
||||
assert "by_type" in dashboard
|
||||
assert "sources" in dashboard
|
||||
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
Comprehensive integration tests for Labeling Pipeline.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from labeling_pipeline import (
|
||||
LabelingPipeline, LabelingPipelineRunner,
|
||||
OnChainVerifier, NewsCrossReferenceVerifier, MarketDataVerifier,
|
||||
FactVerificationEngine
|
||||
)
|
||||
from sentiment_engine.schemas.processed import SentimentScores, EmotionScores, EventType
|
||||
|
||||
|
||||
class TestLabelingPipeline:
|
||||
"""Tests for LabelingPipeline"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return LabelingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_label_text_returns_complete_structure(self, pipeline):
|
||||
"""label_text should return complete labeled structure"""
|
||||
result = await pipeline.label_text("Bitcoin surges to $100k!")
|
||||
|
||||
assert "labels" in result
|
||||
assert "confidence" in result
|
||||
assert "verified" in result
|
||||
assert "verification_details" in result
|
||||
assert "labeled_at" in result
|
||||
|
||||
assert result["labels"]["sentiment"] in ["Bearish", "Bullish", "Neutral"]
|
||||
assert result["labels"]["event_type"] in [e.value for e in EventType]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_label_text_with_context(self, pipeline):
|
||||
"""label_text should use context"""
|
||||
result = await pipeline.label_text(
|
||||
"Bitcoin surges!",
|
||||
context={"source_id": "coindesk", "source_type": "news", "source_credibility": 0.9}
|
||||
)
|
||||
|
||||
assert result["confidence"]["verification"] > 0.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_label_batch(self, pipeline):
|
||||
"""Should label multiple texts"""
|
||||
texts = [
|
||||
"Bitcoin surges to $100k!",
|
||||
"Major hack on exchange!",
|
||||
"SEC approves ETF!"
|
||||
]
|
||||
|
||||
results = await pipeline.label_batch(texts)
|
||||
|
||||
assert len(results) == 3
|
||||
assert all("labels" in r for r in results)
|
||||
|
||||
def test_label_text_sync(self, pipeline):
|
||||
"""Synchronous label_text should work"""
|
||||
result = pipeline.label_text_sync("Bitcoin surges!")
|
||||
|
||||
assert "labels" in result
|
||||
|
||||
|
||||
class TestOnChainVerifier:
|
||||
"""Tests for OnChainVerifier"""
|
||||
|
||||
@pytest.fixture
|
||||
def verifier(self):
|
||||
return OnChainVerifier()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_transaction(self, verifier):
|
||||
"""Should verify transaction exists"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={"status": "confirmed"})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.verify("tx_hash_123")
|
||||
|
||||
assert result["verified"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_address_balance(self, verifier):
|
||||
"""Should verify address balance"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={"balance": "1000000000000000000"})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.verify_address("0x123...", min_balance=1e18)
|
||||
|
||||
assert result["verified"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_contract_interaction(self, verifier):
|
||||
"""Should verify contract interaction"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={"logs": [{"address": "0x..."}]})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.verify_contract_interaction("0x123...", "0x456...")
|
||||
|
||||
assert result["verified"] is True
|
||||
|
||||
|
||||
class TestNewsCrossReferenceVerifier:
|
||||
"""Tests for NewsCrossReferenceVerifier"""
|
||||
|
||||
@pytest.fixture
|
||||
def verifier(self):
|
||||
return NewsCrossReferenceVerifier()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_reference_single_source(self, verifier):
|
||||
"""Should find matching articles"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={
|
||||
"articles": [
|
||||
{"title": "Bitcoin surges", "source": "Reuters"},
|
||||
{"title": "BTC hits new high", "source": "Bloomberg"}
|
||||
]
|
||||
})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.cross_reference("Bitcoin surges to new high")
|
||||
|
||||
assert result["verified"] is True
|
||||
assert result["matches"] >= 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_reference_no_matches(self, verifier):
|
||||
"""Should return unverified when no matches"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={"articles": []})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.cross_reference("Completely unique obscure news")
|
||||
|
||||
assert result["verified"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_reference_confidence(self, verifier):
|
||||
"""Should calculate confidence based on match count"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={
|
||||
"articles": [
|
||||
{"title": "Bitcoin surges", "source": "Reuters"},
|
||||
{"title": "BTC new high", "source": "Bloomberg"},
|
||||
{"title": "Bitcoin record", "source": "CNBC"},
|
||||
{"title": "Crypto surges", "source": "WSJ"},
|
||||
{"title": "Digital gold", "source": "FT"}
|
||||
]
|
||||
})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.cross_reference("Bitcoin surges to new high")
|
||||
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
|
||||
class TestMarketDataVerifier:
|
||||
"""Tests for MarketDataVerifier"""
|
||||
|
||||
@pytest.fixture
|
||||
def verifier(self):
|
||||
return MarketDataVerifier()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_price_movement(self, verifier):
|
||||
"""Should verify price movement matches sentiment"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={
|
||||
"price_change_24h": 0.15, # 15% increase
|
||||
"volume_24h": 1000000000
|
||||
})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.verify_price_movement("BTC", "bullish")
|
||||
|
||||
assert result["verified"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_price_movement_contradiction(self, verifier):
|
||||
"""Should detect contradiction"""
|
||||
with patch('aiohttp.ClientSession.get') as mock_get:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status = 200
|
||||
mock_response.json = AsyncMock(return_value={
|
||||
"price_change_24h": -0.20, # 20% decrease
|
||||
"volume_24h": 1000000000
|
||||
})
|
||||
mock_get.return_value.__aenter__.return_value = mock_response
|
||||
|
||||
result = await verifier.verify_price_movement("BTC", "bullish")
|
||||
|
||||
assert result["verified"] is False
|
||||
|
||||
|
||||
class TestFactVerificationEngine:
|
||||
"""Tests for FactVerificationEngine"""
|
||||
|
||||
@pytest.fixture
|
||||
def engine(self):
|
||||
return FactVerificationEngine()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_combines_all_sources(self, engine):
|
||||
"""Should combine all verification sources"""
|
||||
text = "Bitcoin surges to $100k as institutional inflows surge"
|
||||
|
||||
with patch.object(engine.on_chain_verifier, 'verify', new_callable=AsyncMock) as mock_onchain, \
|
||||
patch.object(engine.news_verifier, 'cross_reference', new_callable=AsyncMock) as mock_news, \
|
||||
patch.object(engine.market_verifier, 'verify_price_movement', new_callable=AsyncMock) as mock_market:
|
||||
|
||||
mock_onchain.return_value = {"verified": True, "details": "tx confirmed"}
|
||||
mock_news.return_value = {"verified": True, "matches": 3, "confidence": 0.8}
|
||||
mock_market.return_value = {"verified": True, "price_change": 0.15}
|
||||
|
||||
result = await engine.verify(text, "BTC", "bullish")
|
||||
|
||||
assert result["verified"] is True
|
||||
assert result["confidence"] > 0.7
|
||||
assert len(result["evidence"]) >= 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_partial_sources(self, engine):
|
||||
"""Should handle partial verification"""
|
||||
text = "Some obscure news"
|
||||
|
||||
with patch.object(engine.on_chain_verifier, 'verify', new_callable=AsyncMock) as mock_onchain, \
|
||||
patch.object(engine.news_verifier, 'cross_reference', new_callable=AsyncMock) as mock_news, \
|
||||
patch.object(engine.market_verifier, 'verify_price_movement', new_callable=AsyncMock) as mock_market:
|
||||
|
||||
mock_onchain.return_value = {"verified": False, "details": "no tx"}
|
||||
mock_news.return_value = {"verified": False, "matches": 0, "confidence": 0.0}
|
||||
mock_market.return_value = {"verified": False, "price_change": 0.0}
|
||||
|
||||
result = await engine.verify(text, "UNKNOWN", "neutral")
|
||||
|
||||
assert result["verified"] is False
|
||||
assert result["confidence"] < 0.3
|
||||
|
||||
|
||||
class TestLabelingPipelineRunner:
|
||||
"""Tests for LabelingPipelineRunner"""
|
||||
|
||||
@pytest.fixture
|
||||
def runner(self):
|
||||
return LabelingPipelineRunner()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_on_dataset(self, runner):
|
||||
"""Should process dataset file"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
||||
for i in range(5):
|
||||
f.write(json.dumps({
|
||||
"id": f"sample_{i}",
|
||||
"raw_text": f"Bitcoin news {i}",
|
||||
"source_id": "test",
|
||||
"source_type": "news",
|
||||
"credibility": 0.8
|
||||
}) + '\n')
|
||||
input_file = f.name
|
||||
|
||||
output_file = input_file.replace('.jsonl', '_labeled.jsonl')
|
||||
|
||||
try:
|
||||
results = await runner.run_on_dataset(input_file, output_file)
|
||||
|
||||
assert len(results) == 5
|
||||
assert all("labels" in r for r in results)
|
||||
assert os.path.exists(output_file)
|
||||
finally:
|
||||
os.unlink(input_file)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_on_dataset_tracks_stats(self, runner):
|
||||
"""Should track verification stats"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
||||
f.write(json.dumps({"id": "1", "raw_text": "Bitcoin surges!", "source_id": "test", "source_type": "news", "credibility": 0.9}) + '\n')
|
||||
f.write(json.dumps({"id": "2", "raw_text": "Random text without crypto", "source_id": "test", "source_type": "news", "credibility": 0.5}) + '\n')
|
||||
input_file = f.name
|
||||
|
||||
output_file = input_file.replace('.jsonl', '_labeled.jsonl')
|
||||
|
||||
try:
|
||||
results = await runner.run_on_dataset(input_file, output_file)
|
||||
|
||||
assert runner.total_count == 2
|
||||
assert runner.verified_count <= 2
|
||||
finally:
|
||||
os.unlink(input_file)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
|
||||
class TestLabelingPipelineIntegration:
|
||||
"""Integration tests for labeling pipeline"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_labeling_workflow(self):
|
||||
"""Complete labeling workflow"""
|
||||
pipeline = LabelingPipeline()
|
||||
|
||||
# Test various crypto events
|
||||
test_cases = [
|
||||
("Bitcoin surges to $100k!", "Bullish", "earnings"),
|
||||
("Major hack on exchange!", "Bearish", "hack"),
|
||||
("SEC approves ETF!", "Bullish", "regulatory"),
|
||||
("Ethereum upgrade activates!", "Neutral", "upgrade"),
|
||||
("Whale moves 10000 BTC!", "Bullish", "whale"),
|
||||
]
|
||||
|
||||
for text, expected_sentiment, expected_event in test_cases:
|
||||
result = await pipeline.label_text(text)
|
||||
|
||||
assert result["labels"]["sentiment"] == expected_sentiment
|
||||
assert result["labels"]["event_type"] == expected_event
|
||||
assert result["confidence"]["overall"] > 0.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_labeling_with_verification(self):
|
||||
"""Labeling with fact verification"""
|
||||
pipeline = LabelingPipeline()
|
||||
|
||||
# High credibility source
|
||||
result = await pipeline.label_text(
|
||||
"Bitcoin surges to $100k!",
|
||||
context={"source_id": "bloomberg", "source_type": "news", "source_credibility": 0.95}
|
||||
)
|
||||
|
||||
assert result["confidence"]["verification"] > 0.5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_labeling_performance(self):
|
||||
"""Batch labeling should be efficient"""
|
||||
pipeline = LabelingPipeline()
|
||||
|
||||
texts = [f"Bitcoin news {i}" for i in range(20)]
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
results = await pipeline.label_batch(texts)
|
||||
elapsed = time.time() - start
|
||||
|
||||
assert len(results) == 20
|
||||
assert elapsed < 30 # Should complete within 30 seconds
|
||||
|
||||
|
||||
class TestLabelingPipelineEdgeCases:
|
||||
"""Edge case tests for labeling pipeline"""
|
||||
|
||||
@pytest.fixture
|
||||
def pipeline(self):
|
||||
return LabelingPipeline()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_text(self, pipeline):
|
||||
"""Should handle empty text"""
|
||||
result = await pipeline.label_text("")
|
||||
|
||||
assert result["labels"]["sentiment"] == "Neutral"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_very_long_text(self, pipeline):
|
||||
"""Should handle very long text"""
|
||||
text = "Bitcoin surges! " * 1000
|
||||
|
||||
result = await pipeline.label_text(text)
|
||||
|
||||
assert result["labels"]["sentiment"] in ["Bullish", "Bearish", "Neutral"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unicode_text(self, pipeline):
|
||||
"""Should handle unicode"""
|
||||
text = "Bitcoin 🚀 surges to $100k 💎"
|
||||
|
||||
result = await pipeline.label_text(text)
|
||||
|
||||
assert result["labels"]["sentiment"] == "Bullish"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_special_characters(self, pipeline):
|
||||
"""Should handle special characters"""
|
||||
text = "BTC/USD: $50,000.00 (24h: +5.2%)"
|
||||
|
||||
result = await pipeline.label_text(text)
|
||||
|
||||
assert result["labels"]["sentiment"] in ["Bullish", "Bearish", "Neutral"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_crypto_entities(self, pipeline):
|
||||
"""Should handle text without crypto entities"""
|
||||
text = "The weather is nice today"
|
||||
|
||||
result = await pipeline.label_text(text)
|
||||
|
||||
assert result["labels"]["sentiment"] == "Neutral"
|
||||
assert result["labels"]["event_type"] == "listing" # default
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user