"""Unit tests for decision fusion module. Tests the DecisionFusion, AgentOpinion, and FusionResult classes. """ import pytest from openclaw.fusion.decision_fusion import ( AgentOpinion, AgentRole, DecisionFusion, FusionConfig, FusionResult, SignalType, WeightedVote, ) class TestSignalType: """Test SignalType enum.""" def test_signal_values(self): """Test signal type values.""" assert SignalType.STRONG_BUY.value == 2 assert SignalType.BUY.value == 1 assert SignalType.HOLD.value == 0 assert SignalType.SELL.value == -1 assert SignalType.STRONG_SELL.value == -2 class TestAgentOpinion: """Test AgentOpinion dataclass.""" def test_opinion_creation(self): """Test creating an agent opinion.""" opinion = AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Technical breakout", factors=["trend", "volume"], ) assert opinion.agent_id == "market-1" assert opinion.role == AgentRole.MARKET_ANALYST assert opinion.signal == SignalType.BUY assert opinion.confidence == 0.8 def test_confidence_clamping(self): """Test that confidence is clamped to 0-1 range.""" opinion_high = AgentOpinion( agent_id="test", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=1.5, reasoning="Test", ) assert opinion_high.confidence == 1.0 opinion_low = AgentOpinion( agent_id="test", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=-0.5, reasoning="Test", ) assert opinion_low.confidence == 0.0 def test_opinion_to_dict(self): """Test converting opinion to dictionary.""" opinion = AgentOpinion( agent_id="fund-1", role=AgentRole.FUNDAMENTAL_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Strong earnings", ) d = opinion.to_dict() assert d["agent_id"] == "fund-1" assert d["role"] == "fundamental_analyst" assert d["signal"] == "STRONG_BUY" assert "timestamp" in d class TestFusionConfig: """Test FusionConfig validation.""" def test_default_weights(self): """Test default role weights.""" config = FusionConfig() assert config.role_weights[AgentRole.RISK_MANAGER] == 1.5 assert config.role_weights[AgentRole.FUNDAMENTAL_ANALYST] == 1.2 assert config.role_weights[AgentRole.MARKET_ANALYST] == 1.0 def test_invalid_confidence_threshold(self): """Test invalid confidence threshold raises error.""" with pytest.raises(ValueError): FusionConfig(confidence_threshold=1.5) def test_invalid_consensus_threshold(self): """Test invalid consensus threshold raises error.""" with pytest.raises(ValueError): FusionConfig(consensus_threshold=-0.5) class TestDecisionFusion: """Test DecisionFusion functionality.""" @pytest.fixture def fusion(self): """Create a decision fusion instance.""" config = FusionConfig() return DecisionFusion(config) def test_start_fusion(self, fusion): """Test starting fusion process.""" fusion.start_fusion("AAPL") assert fusion.symbol == "AAPL" def test_add_opinion(self, fusion): """Test adding opinions.""" fusion.start_fusion("AAPL") opinion = AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Breakout", ) fusion.add_opinion(opinion) assert len(fusion._current_opinions) == 1 def test_fuse_single_opinion(self, fusion): """Test fusion with single opinion.""" fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Technical breakout", ) ) result = fusion.fuse() assert result.symbol == "AAPL" assert result.final_signal == SignalType.BUY assert result.final_confidence > 0 def test_fuse_multiple_opinions(self, fusion): """Test fusion with multiple opinions.""" fusion.start_fusion("AAPL") # Bullish technical fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Breakout", ) ) # Bullish fundamental fusion.add_opinion( AgentOpinion( agent_id="fund-1", role=AgentRole.FUNDAMENTAL_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Strong earnings", ) ) # Bearish sentiment fusion.add_opinion( AgentOpinion( agent_id="sent-1", role=AgentRole.SENTIMENT_ANALYST, signal=SignalType.SELL, confidence=0.5, reasoning="Negative news", ) ) result = fusion.fuse() # Should be bullish overall due to strong fundamental + technical assert result.final_signal.value > 0 assert len(result.weighted_votes) == 3 def test_risk_manager_override(self, fusion): """Test risk manager override functionality.""" fusion.config.enable_risk_override = True fusion.start_fusion("AAPL") # Bullish opinions fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Perfect setup", ) ) # Risk manager strongly warns fusion.add_opinion( AgentOpinion( agent_id="risk-1", role=AgentRole.RISK_MANAGER, signal=SignalType.STRONG_SELL, confidence=0.9, # High confidence triggers override reasoning="High volatility, position too large", ) ) result = fusion.fuse() # Risk manager should override assert result.final_signal == SignalType.SELL def test_consensus_calculation(self, fusion): """Test consensus level calculation.""" fusion.start_fusion("AAPL") # All bullish = high consensus for i, role in enumerate([AgentRole.MARKET_ANALYST, AgentRole.FUNDAMENTAL_ANALYST]): fusion.add_opinion( AgentOpinion( agent_id=f"agent-{i}", role=role, signal=SignalType.BUY, confidence=0.8, reasoning="Agreed", ) ) result = fusion.fuse() assert result.consensus_level > 0.5 def test_low_confidence_filtering(self, fusion): """Test that low confidence opinions are filtered.""" fusion.config.confidence_threshold = 0.5 fusion.start_fusion("AAPL") # High confidence - included fusion.add_opinion( AgentOpinion( agent_id="high", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Strong signal", ) ) # Low confidence - filtered out fusion.add_opinion( AgentOpinion( agent_id="low", role=AgentRole.SENTIMENT_ANALYST, signal=SignalType.SELL, confidence=0.2, reasoning="Weak signal", ) ) result = fusion.fuse() # Only high confidence opinion should be in votes assert len(result.weighted_votes) == 1 def test_supporting_vs_opposing(self, fusion): """Test categorization of supporting vs opposing opinions.""" fusion.start_fusion("AAPL") # Supporting buy fusion.add_opinion( AgentOpinion( agent_id="bull-1", role=AgentRole.BULL_RESEARCHER, signal=SignalType.BUY, confidence=0.8, reasoning="Growth", ) ) # Supporting buy fusion.add_opinion( AgentOpinion( agent_id="bull-2", role=AgentRole.MARKET_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Breakout", ) ) # Opposing fusion.add_opinion( AgentOpinion( agent_id="bear-1", role=AgentRole.BEAR_RESEARCHER, signal=SignalType.SELL, confidence=0.6, reasoning="Overvalued", ) ) result = fusion.fuse() # Should have more supporting than opposing (bullish consensus) assert len(result.supporting_opinions) >= len(result.opposing_opinions) class TestWeightedVote: """Test WeightedVote dataclass.""" def test_weighted_vote_creation(self): """Test creating a weighted vote.""" opinion = AgentOpinion( agent_id="test", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Test", ) vote = WeightedVote( opinion=opinion, weight=1.2, weighted_score=0.96, ) assert vote.weight == 1.2 assert vote.weighted_score == 0.96 class TestFusionResult: """Test FusionResult functionality.""" def test_result_creation(self): """Test creating fusion result.""" result = FusionResult( symbol="AAPL", final_signal=SignalType.BUY, final_confidence=0.75, consensus_level=0.8, ) assert result.symbol == "AAPL" assert result.final_confidence == 0.75 def test_get_recommendation_text(self): """Test human-readable recommendation.""" result = FusionResult( symbol="AAPL", final_signal=SignalType.STRONG_BUY, final_confidence=0.9, consensus_level=0.8, ) text = result.get_recommendation_text() assert "买入" in text or "Buy" in text def test_result_to_dict(self): """Test converting result to dictionary.""" result = FusionResult( symbol="AAPL", final_signal=SignalType.BUY, final_confidence=0.75, consensus_level=0.8, ) d = result.to_dict() assert d["symbol"] == "AAPL" assert d["final_signal"] == "BUY" assert "timestamp" in d class TestDecisionFusionHistory: """Test fusion history tracking.""" def test_get_fusion_history(self): """Test retrieving fusion history.""" fusion = DecisionFusion() # Run multiple fusions for symbol in ["AAPL", "GOOGL"]: fusion.start_fusion(symbol) fusion.add_opinion( AgentOpinion( agent_id="test", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Test", ) ) fusion.fuse() history = fusion.get_fusion_history() assert len(history) == 2 def test_get_latest_fusion(self): """Test retrieving latest fusion for symbol.""" fusion = DecisionFusion() # Two fusions for AAPL for _ in range(2): fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="test", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Test", ) ) fusion.fuse() latest = fusion.get_latest_fusion("AAPL") assert latest is not None assert latest.symbol == "AAPL" class TestExecutionPlan: """Test execution plan generation.""" def test_strong_signal_plan(self): """Test execution plan for strong signals.""" fusion = DecisionFusion() fusion.start_fusion("AAPL") # Very high confidence and consensus fusion.add_opinion( AgentOpinion( agent_id="fund", role=AgentRole.FUNDAMENTAL_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.95, reasoning="Exceptional", ) ) result = fusion.fuse() assert result.execution_plan["urgency"] == "high" assert result.execution_plan["position_size"] == "full" def test_weak_signal_plan(self): """Test execution plan for weak signals.""" fusion = DecisionFusion() fusion.start_fusion("AAPL") # Conflicting signals with low confidence = reduced position fusion.add_opinion( AgentOpinion( agent_id="market", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.4, reasoning="Weak buy signal", ) ) fusion.add_opinion( AgentOpinion( agent_id="sentiment", role=AgentRole.SENTIMENT_ANALYST, signal=SignalType.SELL, confidence=0.4, reasoning="Weak sell signal", ) ) result = fusion.fuse() # Conflicting low confidence signals should result in reduced position assert result.execution_plan["position_size"] in ["reduced", "standard"]